@telora/daemon 0.21.7 → 0.21.26
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/build-info.json +14 -3
- package/dist/audit-hooks.d.ts +62 -3
- package/dist/audit-hooks.d.ts.map +1 -1
- package/dist/audit-hooks.js +123 -3
- package/dist/audit-hooks.js.map +1 -1
- package/dist/completion/event.d.ts +1 -0
- package/dist/completion/event.d.ts.map +1 -1
- package/dist/completion/event.js.map +1 -1
- package/dist/completion/review-exit-phase.d.ts +12 -0
- package/dist/completion/review-exit-phase.d.ts.map +1 -1
- package/dist/completion/review-exit-phase.js +45 -10
- package/dist/completion/review-exit-phase.js.map +1 -1
- package/dist/completion/team-completion.d.ts.map +1 -1
- package/dist/completion/team-completion.js +6 -0
- package/dist/completion/team-completion.js.map +1 -1
- package/dist/completion/team-lifecycle.d.ts.map +1 -1
- package/dist/completion/team-lifecycle.js +2 -1
- package/dist/completion/team-lifecycle.js.map +1 -1
- package/dist/delivery-advance-block.d.ts +9 -1
- package/dist/delivery-advance-block.d.ts.map +1 -1
- package/dist/delivery-advance-block.js +37 -1
- package/dist/delivery-advance-block.js.map +1 -1
- package/dist/delivery-lifecycle.d.ts.map +1 -1
- package/dist/delivery-lifecycle.js +9 -0
- package/dist/delivery-lifecycle.js.map +1 -1
- package/dist/delivery-reconciler.d.ts +16 -0
- package/dist/delivery-reconciler.d.ts.map +1 -1
- package/dist/delivery-reconciler.js +12 -0
- package/dist/delivery-reconciler.js.map +1 -1
- package/dist/external-hold.d.ts +42 -0
- package/dist/external-hold.d.ts.map +1 -1
- package/dist/external-hold.js +85 -11
- package/dist/external-hold.js.map +1 -1
- package/dist/focus-engine.d.ts.map +1 -1
- package/dist/focus-engine.js +10 -1
- package/dist/focus-engine.js.map +1 -1
- package/dist/focus-executor.d.ts.map +1 -1
- package/dist/focus-executor.js +5 -0
- package/dist/focus-executor.js.map +1 -1
- package/dist/focus-lifecycle.d.ts.map +1 -1
- package/dist/focus-lifecycle.js +9 -11
- package/dist/focus-lifecycle.js.map +1 -1
- package/dist/focus-provisioning.d.ts.map +1 -1
- package/dist/focus-provisioning.js +9 -11
- package/dist/focus-provisioning.js.map +1 -1
- package/dist/focus-team-state.d.ts +23 -0
- package/dist/focus-team-state.d.ts.map +1 -1
- package/dist/focus-team-state.js +42 -0
- package/dist/focus-team-state.js.map +1 -1
- package/dist/git/merge.d.ts +24 -0
- package/dist/git/merge.d.ts.map +1 -1
- package/dist/git/merge.js +45 -5
- package/dist/git/merge.js.map +1 -1
- package/dist/git/types.d.ts +9 -0
- package/dist/git/types.d.ts.map +1 -1
- package/dist/git/types.js.map +1 -1
- package/dist/heartbeat.d.ts +53 -0
- package/dist/heartbeat.d.ts.map +1 -1
- package/dist/heartbeat.js +100 -7
- package/dist/heartbeat.js.map +1 -1
- package/dist/listener-auto-advance.d.ts.map +1 -1
- package/dist/listener-auto-advance.js +12 -2
- package/dist/listener-auto-advance.js.map +1 -1
- package/dist/nexus/escalation-bridge.d.ts +8 -0
- package/dist/nexus/escalation-bridge.d.ts.map +1 -1
- package/dist/nexus/escalation-bridge.js +8 -0
- package/dist/nexus/escalation-bridge.js.map +1 -1
- package/dist/nexus/return.d.ts +28 -2
- package/dist/nexus/return.d.ts.map +1 -1
- package/dist/nexus/return.js +53 -2
- package/dist/nexus/return.js.map +1 -1
- package/dist/output-monitor.d.ts.map +1 -1
- package/dist/output-monitor.js +14 -0
- package/dist/output-monitor.js.map +1 -1
- package/dist/prompt-sections/execution-sections.d.ts.map +1 -1
- package/dist/prompt-sections/execution-sections.js +7 -0
- package/dist/prompt-sections/execution-sections.js.map +1 -1
- package/dist/prompt-sections/persona-sections.d.ts +10 -0
- package/dist/prompt-sections/persona-sections.d.ts.map +1 -1
- package/dist/prompt-sections/persona-sections.js +4 -2
- package/dist/prompt-sections/persona-sections.js.map +1 -1
- package/dist/prompt-sections/scoping-sections.d.ts.map +1 -1
- package/dist/prompt-sections/scoping-sections.js +8 -0
- package/dist/prompt-sections/scoping-sections.js.map +1 -1
- package/dist/queries/agent-resolvable-promotion.d.ts +17 -5
- package/dist/queries/agent-resolvable-promotion.d.ts.map +1 -1
- package/dist/queries/agent-resolvable-promotion.js +24 -10
- package/dist/queries/agent-resolvable-promotion.js.map +1 -1
- package/dist/queries/daemon-connection.d.ts +21 -1
- package/dist/queries/daemon-connection.d.ts.map +1 -1
- package/dist/queries/daemon-connection.js +4 -1
- package/dist/queries/daemon-connection.js.map +1 -1
- package/dist/queries/deliveries.d.ts +44 -12
- package/dist/queries/deliveries.d.ts.map +1 -1
- package/dist/queries/deliveries.js +32 -15
- package/dist/queries/deliveries.js.map +1 -1
- package/dist/queries/delivery-gate-state.d.ts +23 -0
- package/dist/queries/delivery-gate-state.d.ts.map +1 -1
- package/dist/queries/issues.d.ts.map +1 -1
- package/dist/queries/issues.js +6 -0
- package/dist/queries/issues.js.map +1 -1
- package/dist/queries/requests.d.ts +30 -0
- package/dist/queries/requests.d.ts.map +1 -0
- package/dist/queries/requests.js +32 -0
- package/dist/queries/requests.js.map +1 -0
- package/dist/resolvers/shared/git-diff.d.ts +32 -3
- package/dist/resolvers/shared/git-diff.d.ts.map +1 -1
- package/dist/resolvers/shared/git-diff.js +120 -16
- package/dist/resolvers/shared/git-diff.js.map +1 -1
- package/dist/retention-observation.d.ts +126 -0
- package/dist/retention-observation.d.ts.map +1 -0
- package/dist/retention-observation.js +143 -0
- package/dist/retention-observation.js.map +1 -0
- package/dist/spawn/prompt-assembly.d.ts +9 -0
- package/dist/spawn/prompt-assembly.d.ts.map +1 -1
- package/dist/spawn/prompt-assembly.js +2 -1
- package/dist/spawn/prompt-assembly.js.map +1 -1
- package/dist/spawn/worktree-session.d.ts.map +1 -1
- package/dist/spawn/worktree-session.js +13 -14
- package/dist/spawn/worktree-session.js.map +1 -1
- package/dist/stall-detector-config.d.ts +2 -0
- package/dist/stall-detector-config.d.ts.map +1 -1
- package/dist/stall-detector-config.js +4 -0
- package/dist/stall-detector-config.js.map +1 -1
- package/dist/stall-detectors.d.ts +55 -0
- package/dist/stall-detectors.d.ts.map +1 -1
- package/dist/stall-detectors.js +63 -2
- package/dist/stall-detectors.js.map +1 -1
- package/dist/state-cascade.d.ts.map +1 -1
- package/dist/state-cascade.js +5 -1
- package/dist/state-cascade.js.map +1 -1
- package/dist/templates/claude-md.d.ts +1 -1
- package/dist/templates/claude-md.d.ts.map +1 -1
- package/dist/templates/claude-md.js +25 -0
- package/dist/templates/claude-md.js.map +1 -1
- package/dist/types/focus.d.ts +14 -0
- package/dist/types/focus.d.ts.map +1 -1
- package/dist/unified-shell.d.ts.map +1 -1
- package/dist/unified-shell.js +8 -0
- package/dist/unified-shell.js.map +1 -1
- package/dist/verification-deps.d.ts +14 -0
- package/dist/verification-deps.d.ts.map +1 -1
- package/dist/verification-deterministic.d.ts.map +1 -1
- package/dist/verification-deterministic.js +31 -4
- package/dist/verification-deterministic.js.map +1 -1
- package/dist/verification-engine.d.ts.map +1 -1
- package/dist/verification-engine.js +78 -21
- package/dist/verification-engine.js.map +1 -1
- package/dist/verification-evidence.d.ts.map +1 -1
- package/dist/verification-evidence.js +2 -2
- package/dist/verification-evidence.js.map +1 -1
- package/dist/worktree/index.d.ts +1 -0
- package/dist/worktree/index.d.ts.map +1 -1
- package/dist/worktree/index.js +1 -0
- package/dist/worktree/index.js.map +1 -1
- package/dist/worktree/safety.d.ts +14 -6
- package/dist/worktree/safety.d.ts.map +1 -1
- package/dist/worktree/safety.js +17 -13
- package/dist/worktree/safety.js.map +1 -1
- package/dist/worktree/scratch-guard.d.ts +57 -0
- package/dist/worktree/scratch-guard.d.ts.map +1 -1
- package/dist/worktree/scratch-guard.js +168 -21
- package/dist/worktree/scratch-guard.js.map +1 -1
- package/dist/worktree/verify-interlock.d.ts +52 -0
- package/dist/worktree/verify-interlock.d.ts.map +1 -0
- package/dist/worktree/verify-interlock.js +94 -0
- package/dist/worktree/verify-interlock.js.map +1 -0
- package/package.json +3 -3
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
import { loadEnvConfig } from '@telora/daemon-core';
|
|
22
22
|
import { runGitSync } from '../git/index.js';
|
|
23
23
|
import { assertWorktreeIdentity, defaultWorktreeIdentityViolationReporter, } from './identity.js';
|
|
24
|
+
import { isWorktreeUnderVerify } from './verify-interlock.js';
|
|
24
25
|
import { createLogger } from '../log.js';
|
|
25
26
|
const log = createLogger({ module: 'worktree' });
|
|
26
27
|
/** Conservative default sweep ceiling for a single automated commit. */
|
|
@@ -48,21 +49,83 @@ export function resolveSafetyCommitMaxFiles(env) {
|
|
|
48
49
|
function underPrefix(path, prefixes) {
|
|
49
50
|
return prefixes.some((p) => path === p || path.startsWith(p + '/'));
|
|
50
51
|
}
|
|
52
|
+
function splitLines(out) {
|
|
53
|
+
return out
|
|
54
|
+
.split('\n')
|
|
55
|
+
.map((p) => p.trim())
|
|
56
|
+
.filter((p) => p.length > 0);
|
|
57
|
+
}
|
|
51
58
|
/**
|
|
52
|
-
* Enumerate
|
|
53
|
-
* changes (incl. deletions) plus untracked, non-ignored files.
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
59
|
+
* Enumerate paths whose WORKTREE content differs from HEAD: tracked worktree
|
|
60
|
+
* changes (incl. deletions) plus untracked, non-ignored files. This is the set
|
|
61
|
+
* with real content behind it -- the agent work a safety commit must preserve.
|
|
62
|
+
* A staged-only change (a torn write) is deliberately NOT here: it has no
|
|
63
|
+
* worktree content, so it is not work to preserve.
|
|
64
|
+
*
|
|
65
|
+
* Uses name-only commands rather than parsing `git status --porcelain` (whose
|
|
66
|
+
* fixed-width status prefix is corrupted by output trimming).
|
|
57
67
|
*/
|
|
58
|
-
function
|
|
68
|
+
export function enumerateWorktreeDelta(worktreePath) {
|
|
59
69
|
const tracked = runGitSync(['diff', '--name-only', 'HEAD'], worktreePath);
|
|
60
70
|
const untracked = runGitSync(['ls-files', '--others', '--exclude-standard'], worktreePath);
|
|
61
71
|
if (!tracked.success || !untracked.success)
|
|
62
72
|
return null;
|
|
63
|
-
return [...tracked.output
|
|
64
|
-
|
|
65
|
-
|
|
73
|
+
return [...new Set([...splitLines(tracked.output), ...splitLines(untracked.output)])];
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Enumerate paths STAGED differently from HEAD (index vs HEAD). A staged path
|
|
77
|
+
* whose worktree copy matches HEAD is a TORN WRITE -- an index entry with no
|
|
78
|
+
* worktree content behind it. This is the index the detector
|
|
79
|
+
* (`git status --porcelain`, which consults the index) sees but the
|
|
80
|
+
* worktree-only enumeration above is blind to.
|
|
81
|
+
*/
|
|
82
|
+
export function enumerateIndexDelta(worktreePath) {
|
|
83
|
+
const staged = runGitSync(['diff', '--cached', '--name-only', 'HEAD'], worktreePath);
|
|
84
|
+
if (!staged.success)
|
|
85
|
+
return null;
|
|
86
|
+
return splitLines(staged.output);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* INDEX-AWARE enumeration of every path dirty vs HEAD -- in the worktree OR the
|
|
90
|
+
* index. This is what `git status --porcelain` shows the DETECTOR
|
|
91
|
+
* (worktreeHasUncommittedChanges), so the REMEDIATOR agrees with the detector
|
|
92
|
+
* on what "dirty" means. `worktreeHasNonScratchChanges` deliberately does NOT
|
|
93
|
+
* use this (it asks the narrower "is there worktree content to preserve?" --
|
|
94
|
+
* see its doc).
|
|
95
|
+
*/
|
|
96
|
+
export function enumerateDirtyVsHead(worktreePath) {
|
|
97
|
+
const worktree = enumerateWorktreeDelta(worktreePath);
|
|
98
|
+
const index = enumerateIndexDelta(worktreePath);
|
|
99
|
+
if (worktree === null || index === null)
|
|
100
|
+
return null;
|
|
101
|
+
return [...new Set([...worktree, ...index])];
|
|
102
|
+
}
|
|
103
|
+
/** Enumerate + scratch-partition the worktree and index deltas. Null on git error. */
|
|
104
|
+
function classifyDirt(worktreePath, scratchPrefixes) {
|
|
105
|
+
const worktreeDelta = enumerateWorktreeDelta(worktreePath);
|
|
106
|
+
const indexDelta = enumerateIndexDelta(worktreePath);
|
|
107
|
+
if (worktreeDelta === null || indexDelta === null)
|
|
108
|
+
return null;
|
|
109
|
+
const keptWorktree = worktreeDelta.filter((p) => !underPrefix(p, scratchPrefixes));
|
|
110
|
+
const keptIndex = indexDelta.filter((p) => !underPrefix(p, scratchPrefixes));
|
|
111
|
+
const denied = [
|
|
112
|
+
...new Set([
|
|
113
|
+
...worktreeDelta.filter((p) => underPrefix(p, scratchPrefixes)),
|
|
114
|
+
...indexDelta.filter((p) => underPrefix(p, scratchPrefixes)),
|
|
115
|
+
]),
|
|
116
|
+
];
|
|
117
|
+
return { keptWorktree, keptIndex, denied };
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Reconcile the index to the worktree by resetting all non-scratch staged paths
|
|
121
|
+
* to HEAD. Index-only: writes NO history and never touches a worktree file, so
|
|
122
|
+
* the sweep threshold (a commit-history guard) does not apply. The scratch
|
|
123
|
+
* exclusion is preserved for symmetry with the commit path.
|
|
124
|
+
*/
|
|
125
|
+
function reconcileIndexToWorktree(worktreePath, scratchPrefixes) {
|
|
126
|
+
const excludeSpecs = scratchPrefixes.map((p) => `:(exclude)${p}`);
|
|
127
|
+
const reset = runGitSync(['reset', '-q', 'HEAD', '--', '.', ...excludeSpecs], worktreePath);
|
|
128
|
+
return { success: reset.success, error: reset.success ? '' : reset.error };
|
|
66
129
|
}
|
|
67
130
|
/**
|
|
68
131
|
* Stage the worktree's changes for a daemon-authored commit, HARD-excluding
|
|
@@ -78,34 +141,87 @@ function enumerateWouldStage(worktreePath) {
|
|
|
78
141
|
export function stageExcludingDaemonScratch(worktreePath, opts = {}) {
|
|
79
142
|
const scratchPrefixes = opts.scratchPrefixes ?? DEFAULT_SCRATCH_PREFIXES;
|
|
80
143
|
const maxFiles = opts.maxFiles ?? resolveSafetyCommitMaxFiles();
|
|
81
|
-
const
|
|
82
|
-
if (
|
|
144
|
+
const dirt = classifyDirt(worktreePath, scratchPrefixes);
|
|
145
|
+
if (dirt === null) {
|
|
83
146
|
return { kind: 'error', message: 'could not enumerate worktree changes' };
|
|
84
147
|
}
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
148
|
+
const { keptWorktree, keptIndex, denied } = dirt;
|
|
149
|
+
// TORN WRITE disposition: staged content with NO worktree content behind it
|
|
150
|
+
// (the worktree matches HEAD). A safety commit exists to PRESERVE agent work;
|
|
151
|
+
// an index entry with nothing behind it is not agent work, so committing it is
|
|
152
|
+
// both impossible (git finds nothing to commit) and wrong to escalate on.
|
|
153
|
+
// Reconcile the index to the worktree instead -- nothing is preserved because
|
|
154
|
+
// nothing was ever there.
|
|
155
|
+
if (keptWorktree.length === 0 && keptIndex.length > 0) {
|
|
156
|
+
const reset = reconcileIndexToWorktree(worktreePath, scratchPrefixes);
|
|
157
|
+
if (!reset.success) {
|
|
158
|
+
return { kind: 'error', message: reset.error };
|
|
159
|
+
}
|
|
160
|
+
return { kind: 'reconciled_torn_write', reconciled: keptIndex, denied };
|
|
161
|
+
}
|
|
162
|
+
// REAL WORK disposition: the committed set is the worktree delta (what
|
|
163
|
+
// `git add -A` then `git commit` records). The threshold guards that set.
|
|
164
|
+
if (keptWorktree.length > maxFiles) {
|
|
88
165
|
// Refuse: stage nothing, leave the working tree untouched.
|
|
89
166
|
return {
|
|
90
167
|
kind: 'refused_threshold',
|
|
91
|
-
count:
|
|
92
|
-
sample:
|
|
168
|
+
count: keptWorktree.length,
|
|
169
|
+
sample: keptWorktree.slice(0, 20),
|
|
93
170
|
denied,
|
|
94
171
|
maxFiles,
|
|
95
172
|
};
|
|
96
173
|
}
|
|
97
|
-
if (
|
|
174
|
+
if (keptWorktree.length === 0) {
|
|
98
175
|
// Nothing to stage once scratch is excluded (a clean or scratch-only tree).
|
|
99
176
|
return { kind: 'staged', staged: [], denied };
|
|
100
177
|
}
|
|
101
178
|
// Stage everything EXCEPT the scratch prefixes. `.` selects the full tree;
|
|
102
179
|
// each `:(exclude)<prefix>` removes that dir and its contents from the set.
|
|
180
|
+
// `git add -A` snapshots the worktree into the index, which also reconciles
|
|
181
|
+
// any co-present torn-write index entry (its worktree == HEAD, so it drops out
|
|
182
|
+
// of the commit) -- so the MIXED case (real work + a torn write) ends clean
|
|
183
|
+
// with the real work committed and nothing lost.
|
|
103
184
|
const excludeSpecs = scratchPrefixes.map((p) => `:(exclude)${p}`);
|
|
104
185
|
const addResult = runGitSync(['add', '-A', '--', '.', ...excludeSpecs], worktreePath);
|
|
105
186
|
if (!addResult.success) {
|
|
106
187
|
return { kind: 'error', message: addResult.error };
|
|
107
188
|
}
|
|
108
|
-
return { kind: 'staged', staged:
|
|
189
|
+
return { kind: 'staged', staged: keptWorktree, denied };
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* If the worktree's ONLY non-scratch dirt is a TORN WRITE -- staged index
|
|
193
|
+
* entries with no worktree content behind them (index != HEAD, worktree ==
|
|
194
|
+
* HEAD) -- reconcile the index to the worktree (reset to HEAD) and return the
|
|
195
|
+
* reconciled paths. Returns null when there is real worktree work to preserve
|
|
196
|
+
* (that is guardedCommit's job, not this one) or when the tree is clean/
|
|
197
|
+
* scratch-only. Writes NO history and never touches a worktree file.
|
|
198
|
+
*
|
|
199
|
+
* This is the dedicated reconcile handler the advance-block path calls BEFORE it
|
|
200
|
+
* considers re-waking a team: a torn write has no content for a team to commit,
|
|
201
|
+
* so nudging the team (and eventually escalating) would be a pointless loop --
|
|
202
|
+
* the daemon self-clears it here. Identity-guarded exactly like guardedCommit,
|
|
203
|
+
* so a `.git`-less checkout is never reset against the enclosing repo.
|
|
204
|
+
*/
|
|
205
|
+
export function reconcileTornWrite(worktreePath, opts = {}) {
|
|
206
|
+
const identity = assertWorktreeIdentity(worktreePath);
|
|
207
|
+
if (!identity.ok) {
|
|
208
|
+
// Not this directory's worktree -- leave it to the commit path's identity
|
|
209
|
+
// reporter; never reset against the enclosing repo.
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
const scratchPrefixes = opts.scratchPrefixes ?? DEFAULT_SCRATCH_PREFIXES;
|
|
213
|
+
const dirt = classifyDirt(worktreePath, scratchPrefixes);
|
|
214
|
+
if (dirt === null)
|
|
215
|
+
return null;
|
|
216
|
+
const { keptWorktree, keptIndex } = dirt;
|
|
217
|
+
if (keptWorktree.length > 0 || keptIndex.length === 0)
|
|
218
|
+
return null; // not a pure torn write
|
|
219
|
+
const reset = reconcileIndexToWorktree(worktreePath, scratchPrefixes);
|
|
220
|
+
if (!reset.success)
|
|
221
|
+
return null;
|
|
222
|
+
log.info(`reconcileTornWrite: reconciled ${keptIndex.length} torn-write path(s) at ${worktreePath} ` +
|
|
223
|
+
`(index reset to HEAD; no worktree content to preserve): ${keptIndex.slice(0, 5).join(', ')}`);
|
|
224
|
+
return { reconciled: keptIndex };
|
|
109
225
|
}
|
|
110
226
|
/**
|
|
111
227
|
* True iff the worktree has uncommitted changes OUTSIDE daemon scratch. Daemon
|
|
@@ -125,10 +241,17 @@ export function worktreeHasNonScratchChanges(worktreePath, scratchPrefixes = DEF
|
|
|
125
241
|
`to attribute the enclosing repo's changes to it.`);
|
|
126
242
|
return false;
|
|
127
243
|
}
|
|
128
|
-
|
|
129
|
-
|
|
244
|
+
// Worktree-delta ONLY (not index-aware): this asks "is there worktree content
|
|
245
|
+
// to PRESERVE before we discard this worktree?", which a torn write (staged,
|
|
246
|
+
// no worktree content) is not -- so removeWorktree does not block on a torn
|
|
247
|
+
// write, it discards it with the worktree. The advance-gate detector
|
|
248
|
+
// (worktreeHasUncommittedChanges, via `git status --porcelain`) IS index-aware
|
|
249
|
+
// and drives the torn-write reconcile; these are deliberately different
|
|
250
|
+
// questions.
|
|
251
|
+
const worktreeDelta = enumerateWorktreeDelta(worktreePath);
|
|
252
|
+
if (worktreeDelta === null)
|
|
130
253
|
return false;
|
|
131
|
-
return
|
|
254
|
+
return worktreeDelta.some((p) => !underPrefix(p, scratchPrefixes));
|
|
132
255
|
}
|
|
133
256
|
/**
|
|
134
257
|
* Paths the daemon itself authors into a READ-ONLY audit worktree, on top of
|
|
@@ -193,6 +316,19 @@ export const defaultSweepRefusalReporter = (r) => {
|
|
|
193
316
|
export function guardedCommit(params) {
|
|
194
317
|
const { worktreePath, message, context } = params;
|
|
195
318
|
const reporter = params.reporter ?? defaultSweepRefusalReporter;
|
|
319
|
+
// Verify interlock: while a verify command is running in this worktree, defer
|
|
320
|
+
// -- stage and commit NOTHING. A `git add -A` inside a verify's mutation window
|
|
321
|
+
// would snapshot a transient tree the verify then restores, producing a torn
|
|
322
|
+
// write. This is a deliberate DEFERRAL, not a failure or a no-op: the next poll
|
|
323
|
+
// retries once the verify completes (the skip leaves no state behind). Checked
|
|
324
|
+
// FIRST, before the identity gate, so no daemon-authored commit path (safety /
|
|
325
|
+
// WIP / lockfile) touches a worktree mid-verify.
|
|
326
|
+
if (isWorktreeUnderVerify(worktreePath)) {
|
|
327
|
+
log.info(`guardedCommit: DEFERRING commit for "${context}" at ${worktreePath} -- a verify ` +
|
|
328
|
+
`command is running in this worktree; staging/committing a transient mid-verify tree ` +
|
|
329
|
+
`would tear a write. Deferred; the next poll retries once verify completes.`);
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
196
332
|
// Identity gate: refuse before staging if git would act on a different repo.
|
|
197
333
|
const identity = assertWorktreeIdentity(worktreePath);
|
|
198
334
|
if (!identity.ok) {
|
|
@@ -219,6 +355,17 @@ export function guardedCommit(params) {
|
|
|
219
355
|
});
|
|
220
356
|
return null;
|
|
221
357
|
}
|
|
358
|
+
if (stage.kind === 'reconciled_torn_write') {
|
|
359
|
+
// A torn write: staged index entries with no worktree content behind them.
|
|
360
|
+
// The index was reset to HEAD (the tree is now clean). No commit was made --
|
|
361
|
+
// there was nothing to preserve -- so this returns null like the clean case,
|
|
362
|
+
// but is logged DISTINCTLY so reading the log tells you a torn write was
|
|
363
|
+
// reconciled rather than a commit silently doing nothing.
|
|
364
|
+
log.info(`guardedCommit: reconciled torn write for "${context}" at ${worktreePath} -- ` +
|
|
365
|
+
`${stage.reconciled.length} staged path(s) had no worktree content behind them ` +
|
|
366
|
+
`(index reset to HEAD, nothing to preserve, no commit): ${stage.reconciled.slice(0, 5).join(', ')}`);
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
222
369
|
if (stage.staged.length === 0) {
|
|
223
370
|
// Clean or scratch-only tree -- nothing non-scratch to commit.
|
|
224
371
|
return null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scratch-guard.js","sourceRoot":"","sources":["../../src/worktree/scratch-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EACL,sBAAsB,EACtB,wCAAwC,GAEzC,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAEzC,MAAM,GAAG,GAAG,YAAY,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;AAEjD,wEAAwE;AACxE,MAAM,CAAC,MAAM,+BAA+B,GAAG,GAAG,CAAC;AAEnD;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAsB,CAAC,SAAS,CAAC,CAAC;AAEvE;;;;;;;GAOG;AACH,MAAM,UAAU,2BAA2B,CAAC,GAAuB;IACjE,OAAO,aAAa,CAAC,GAAG,CAAC,CAAC,8BAA8B,CAAC;AAC3D,CAAC;
|
|
1
|
+
{"version":3,"file":"scratch-guard.js","sourceRoot":"","sources":["../../src/worktree/scratch-guard.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-worktree verify interlock.
|
|
3
|
+
*
|
|
4
|
+
* The daemon runs a product's configured deterministic verify command INSIDE the
|
|
5
|
+
* focus worktree, and it also commits from that same worktree (safety / WIP /
|
|
6
|
+
* lockfile commits, all via `guardedCommit`). A verify command that mutates a
|
|
7
|
+
* tracked file and restores it (the general case -- e.g. this repo's
|
|
8
|
+
* verify-gate.test.mjs writes a drift marker into ACTIONS.md, then restores it in
|
|
9
|
+
* a `finally`) has a mutation WINDOW. If a 30s-poll safety-commit's `git add -A`
|
|
10
|
+
* lands inside that window it snapshots the transient drift into the index; the
|
|
11
|
+
* verify's restore then reverts the working tree; the commit never lands --
|
|
12
|
+
* leaving a staged entry with no worktree content behind it (a TORN WRITE, which
|
|
13
|
+
* wedged a focus permanently: escalation 2170fdde, 2026-08-16).
|
|
14
|
+
*
|
|
15
|
+
* The fix belongs at the daemon, not any one product's verify script: a daemon
|
|
16
|
+
* that runs arbitrary configured commands in a worktree it also commits from must
|
|
17
|
+
* serialise the two itself. This module is that serialisation -- an in-memory,
|
|
18
|
+
* per-worktree hold acquired around the verify spawn and consulted by every
|
|
19
|
+
* daemon-authored commit path. While a worktree is held, no commit path stages or
|
|
20
|
+
* commits in it; the commit is DEFERRED (retried on the next poll), never lost.
|
|
21
|
+
*
|
|
22
|
+
* In-memory by design: the hold protects a LIVE process's mutation window, so it
|
|
23
|
+
* is only meaningful while this daemon process is running. A daemon restart kills
|
|
24
|
+
* any in-flight verify AND resets the map -- so a restart can never leave a stale
|
|
25
|
+
* hold that wedges a worktree forever (which would reproduce the very stall this
|
|
26
|
+
* focus removes). The map is keyed by the CANONICAL worktree path (realpath, with
|
|
27
|
+
* a `path.resolve` fallback) so the verify's cwd and a commit path's worktreePath
|
|
28
|
+
* key identically even across symlinks or a trailing slash.
|
|
29
|
+
*
|
|
30
|
+
* @module worktree/verify-interlock
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* Acquire the verify interlock for `worktreePath` and return a release token.
|
|
34
|
+
*
|
|
35
|
+
* Call the returned token exactly once when the verify process has exited (any
|
|
36
|
+
* exit -- clean, killed, or abnormal). The token is IDEMPOTENT: calling it more
|
|
37
|
+
* than once is a no-op, so a spawn that fires both `'error'` and `'close'` cannot
|
|
38
|
+
* under-count the hold. Acquire BEFORE the spawn and release in a path that runs
|
|
39
|
+
* on every exit (a `finish`/`finally`), so the hold can never outlive the process.
|
|
40
|
+
*/
|
|
41
|
+
export declare function acquireVerifyHold(worktreePath: string): () => void;
|
|
42
|
+
/**
|
|
43
|
+
* True iff a verify command is currently running in `worktreePath` (the hold is
|
|
44
|
+
* held). Consulted by every daemon-authored commit path before it stages/commits,
|
|
45
|
+
* and by the advance-block gate before it decides to remediate -- so a commit that
|
|
46
|
+
* would snapshot a mid-verify tree is deferred, not performed. Per-worktree: a
|
|
47
|
+
* verify in one focus's worktree never blocks a commit in another's.
|
|
48
|
+
*/
|
|
49
|
+
export declare function isWorktreeUnderVerify(worktreePath: string): boolean;
|
|
50
|
+
/** Test seam: clear all holds between cases. */
|
|
51
|
+
export declare function __resetVerifyHolds(): void;
|
|
52
|
+
//# sourceMappingURL=verify-interlock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify-interlock.d.ts","sourceRoot":"","sources":["../../src/worktree/verify-interlock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAyBH;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,IAAI,CAgBlE;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAEnE;AAED,gDAAgD;AAChD,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-worktree verify interlock.
|
|
3
|
+
*
|
|
4
|
+
* The daemon runs a product's configured deterministic verify command INSIDE the
|
|
5
|
+
* focus worktree, and it also commits from that same worktree (safety / WIP /
|
|
6
|
+
* lockfile commits, all via `guardedCommit`). A verify command that mutates a
|
|
7
|
+
* tracked file and restores it (the general case -- e.g. this repo's
|
|
8
|
+
* verify-gate.test.mjs writes a drift marker into ACTIONS.md, then restores it in
|
|
9
|
+
* a `finally`) has a mutation WINDOW. If a 30s-poll safety-commit's `git add -A`
|
|
10
|
+
* lands inside that window it snapshots the transient drift into the index; the
|
|
11
|
+
* verify's restore then reverts the working tree; the commit never lands --
|
|
12
|
+
* leaving a staged entry with no worktree content behind it (a TORN WRITE, which
|
|
13
|
+
* wedged a focus permanently: escalation 2170fdde, 2026-08-16).
|
|
14
|
+
*
|
|
15
|
+
* The fix belongs at the daemon, not any one product's verify script: a daemon
|
|
16
|
+
* that runs arbitrary configured commands in a worktree it also commits from must
|
|
17
|
+
* serialise the two itself. This module is that serialisation -- an in-memory,
|
|
18
|
+
* per-worktree hold acquired around the verify spawn and consulted by every
|
|
19
|
+
* daemon-authored commit path. While a worktree is held, no commit path stages or
|
|
20
|
+
* commits in it; the commit is DEFERRED (retried on the next poll), never lost.
|
|
21
|
+
*
|
|
22
|
+
* In-memory by design: the hold protects a LIVE process's mutation window, so it
|
|
23
|
+
* is only meaningful while this daemon process is running. A daemon restart kills
|
|
24
|
+
* any in-flight verify AND resets the map -- so a restart can never leave a stale
|
|
25
|
+
* hold that wedges a worktree forever (which would reproduce the very stall this
|
|
26
|
+
* focus removes). The map is keyed by the CANONICAL worktree path (realpath, with
|
|
27
|
+
* a `path.resolve` fallback) so the verify's cwd and a commit path's worktreePath
|
|
28
|
+
* key identically even across symlinks or a trailing slash.
|
|
29
|
+
*
|
|
30
|
+
* @module worktree/verify-interlock
|
|
31
|
+
*/
|
|
32
|
+
import { realpathSync } from 'node:fs';
|
|
33
|
+
import { resolve } from 'node:path';
|
|
34
|
+
import { createLogger } from '../log.js';
|
|
35
|
+
const log = createLogger({ module: 'worktree' });
|
|
36
|
+
/**
|
|
37
|
+
* Active verify holds: canonical worktree path -> count of in-flight verifies.
|
|
38
|
+
* A COUNT (not a boolean) so overlapping verifies on the same worktree -- should
|
|
39
|
+
* they ever occur -- release correctly: the hold clears only when the LAST one
|
|
40
|
+
* releases.
|
|
41
|
+
*/
|
|
42
|
+
const holds = new Map();
|
|
43
|
+
/** Canonicalize a worktree path so verify cwd and commit worktreePath match. */
|
|
44
|
+
function keyFor(worktreePath) {
|
|
45
|
+
try {
|
|
46
|
+
return realpathSync(worktreePath);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return resolve(worktreePath);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Acquire the verify interlock for `worktreePath` and return a release token.
|
|
54
|
+
*
|
|
55
|
+
* Call the returned token exactly once when the verify process has exited (any
|
|
56
|
+
* exit -- clean, killed, or abnormal). The token is IDEMPOTENT: calling it more
|
|
57
|
+
* than once is a no-op, so a spawn that fires both `'error'` and `'close'` cannot
|
|
58
|
+
* under-count the hold. Acquire BEFORE the spawn and release in a path that runs
|
|
59
|
+
* on every exit (a `finish`/`finally`), so the hold can never outlive the process.
|
|
60
|
+
*/
|
|
61
|
+
export function acquireVerifyHold(worktreePath) {
|
|
62
|
+
const key = keyFor(worktreePath);
|
|
63
|
+
holds.set(key, (holds.get(key) ?? 0) + 1);
|
|
64
|
+
log.debug(`verify-interlock: hold acquired for ${key} (depth ${holds.get(key)})`);
|
|
65
|
+
let released = false;
|
|
66
|
+
return () => {
|
|
67
|
+
if (released)
|
|
68
|
+
return;
|
|
69
|
+
released = true;
|
|
70
|
+
const next = (holds.get(key) ?? 1) - 1;
|
|
71
|
+
if (next <= 0) {
|
|
72
|
+
holds.delete(key);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
holds.set(key, next);
|
|
76
|
+
}
|
|
77
|
+
log.debug(`verify-interlock: hold released for ${key} (depth ${next <= 0 ? 0 : next})`);
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* True iff a verify command is currently running in `worktreePath` (the hold is
|
|
82
|
+
* held). Consulted by every daemon-authored commit path before it stages/commits,
|
|
83
|
+
* and by the advance-block gate before it decides to remediate -- so a commit that
|
|
84
|
+
* would snapshot a mid-verify tree is deferred, not performed. Per-worktree: a
|
|
85
|
+
* verify in one focus's worktree never blocks a commit in another's.
|
|
86
|
+
*/
|
|
87
|
+
export function isWorktreeUnderVerify(worktreePath) {
|
|
88
|
+
return (holds.get(keyFor(worktreePath)) ?? 0) > 0;
|
|
89
|
+
}
|
|
90
|
+
/** Test seam: clear all holds between cases. */
|
|
91
|
+
export function __resetVerifyHolds() {
|
|
92
|
+
holds.clear();
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=verify-interlock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify-interlock.js","sourceRoot":"","sources":["../../src/worktree/verify-interlock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAEzC,MAAM,GAAG,GAAG,YAAY,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;AAEjD;;;;;GAKG;AACH,MAAM,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAC;AAExC,gFAAgF;AAChF,SAAS,MAAM,CAAC,YAAoB;IAClC,IAAI,CAAC;QACH,OAAO,YAAY,CAAC,YAAY,CAAC,CAAC;IACpC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,OAAO,CAAC,YAAY,CAAC,CAAC;IAC/B,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAAC,YAAoB;IACpD,MAAM,GAAG,GAAG,MAAM,CAAC,YAAY,CAAC,CAAC;IACjC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1C,GAAG,CAAC,KAAK,CAAC,uCAAuC,GAAG,WAAW,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAClF,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,OAAO,GAAG,EAAE;QACV,IAAI,QAAQ;YAAE,OAAO;QACrB,QAAQ,GAAG,IAAI,CAAC;QAChB,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;QACvC,IAAI,IAAI,IAAI,CAAC,EAAE,CAAC;YACd,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACpB,CAAC;aAAM,CAAC;YACN,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QACvB,CAAC;QACD,GAAG,CAAC,KAAK,CAAC,uCAAuC,GAAG,WAAW,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC;IAC1F,CAAC,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB,CAAC,YAAoB;IACxD,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;AACpD,CAAC;AAED,gDAAgD;AAChD,MAAM,UAAU,kBAAkB;IAChC,KAAK,CAAC,KAAK,EAAE,CAAC;AAChB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telora/daemon",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.26",
|
|
4
4
|
"description": "Agent orchestration daemon for Telora - spawns and manages Claude Code instances",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
},
|
|
38
38
|
"//testRunner": "Uses Node.js built-in test runner (node:test) via tsx for TypeScript support. The root package uses Vitest; this is a deliberate choice for the daemon package to avoid framework dependencies. See src/__tests__/ for test files.",
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@telora/daemon-core": "^0.2.
|
|
41
|
-
"@telora/mcp-products": "^0.22.
|
|
40
|
+
"@telora/daemon-core": "^0.2.76",
|
|
41
|
+
"@telora/mcp-products": "^0.22.115",
|
|
42
42
|
"commander": "^14.0.3",
|
|
43
43
|
"yaml": "^2.4.0",
|
|
44
44
|
"zod": "^4.3.6"
|