@telora/daemon 0.20.95 → 0.20.103

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 (53) hide show
  1. package/build-info.json +2 -2
  2. package/dist/audit-prompt.d.ts.map +1 -1
  3. package/dist/audit-prompt.js +6 -1
  4. package/dist/audit-prompt.js.map +1 -1
  5. package/dist/cli/connect.d.ts.map +1 -1
  6. package/dist/cli/connect.js +12 -12
  7. package/dist/cli/connect.js.map +1 -1
  8. package/dist/crash-recovery.d.ts +12 -2
  9. package/dist/crash-recovery.d.ts.map +1 -1
  10. package/dist/crash-recovery.js +43 -4
  11. package/dist/crash-recovery.js.map +1 -1
  12. package/dist/delivery-advance-block.d.ts.map +1 -1
  13. package/dist/delivery-advance-block.js +21 -4
  14. package/dist/delivery-advance-block.js.map +1 -1
  15. package/dist/gitignore-convergence.d.ts +47 -0
  16. package/dist/gitignore-convergence.d.ts.map +1 -0
  17. package/dist/gitignore-convergence.js +75 -0
  18. package/dist/gitignore-convergence.js.map +1 -0
  19. package/dist/unified-init.d.ts.map +1 -1
  20. package/dist/unified-init.js +11 -19
  21. package/dist/unified-init.js.map +1 -1
  22. package/dist/unified-shell-provisioning.d.ts +22 -0
  23. package/dist/unified-shell-provisioning.d.ts.map +1 -1
  24. package/dist/unified-shell-provisioning.js +55 -0
  25. package/dist/unified-shell-provisioning.js.map +1 -1
  26. package/dist/unified-shell.d.ts.map +1 -1
  27. package/dist/unified-shell.js +9 -1
  28. package/dist/unified-shell.js.map +1 -1
  29. package/dist/worktree/identity.d.ts +63 -0
  30. package/dist/worktree/identity.d.ts.map +1 -0
  31. package/dist/worktree/identity.js +79 -0
  32. package/dist/worktree/identity.js.map +1 -0
  33. package/dist/worktree/index.d.ts +3 -0
  34. package/dist/worktree/index.d.ts.map +1 -1
  35. package/dist/worktree/index.js +3 -0
  36. package/dist/worktree/index.js.map +1 -1
  37. package/dist/worktree/reap.d.ts +60 -0
  38. package/dist/worktree/reap.d.ts.map +1 -0
  39. package/dist/worktree/reap.js +175 -0
  40. package/dist/worktree/reap.js.map +1 -0
  41. package/dist/worktree/safety.d.ts +15 -4
  42. package/dist/worktree/safety.d.ts.map +1 -1
  43. package/dist/worktree/safety.js +72 -39
  44. package/dist/worktree/safety.js.map +1 -1
  45. package/dist/worktree/scratch-guard.d.ts +126 -0
  46. package/dist/worktree/scratch-guard.d.ts.map +1 -0
  47. package/dist/worktree/scratch-guard.js +196 -0
  48. package/dist/worktree/scratch-guard.js.map +1 -0
  49. package/dist/worktree.d.ts +7 -39
  50. package/dist/worktree.d.ts.map +1 -1
  51. package/dist/worktree.js +8 -103
  52. package/dist/worktree.js.map +1 -1
  53. package/package.json +3 -3
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Atomic worktree reaping.
3
+ *
4
+ * "The daemon owns the boundary of every path it creates." A worktree checkout
5
+ * on disk and its `.git/worktrees/<id>` registration must live and die together
6
+ * -- a directory left behind WITHOUT its registration is invisible to
7
+ * `git worktree list`/`prune` and accumulates as unbounded residue (the 4.9 GB
8
+ * BELL leak). This module wraps daemon-core's base removal with an explicit
9
+ * post-condition check so a reap can never silently leave that residue.
10
+ */
11
+ /**
12
+ * Cap on the number of files we blob-verify for an UNREGISTERED, .git-less
13
+ * residue checkout. Above this we cannot cheaply prove reachability, so we
14
+ * preserve (safe) rather than stall crash recovery hashing a huge tree.
15
+ */
16
+ export declare const MAX_RESIDUE_FILES_TO_VERIFY = 2000;
17
+ /**
18
+ * True iff `worktreePath` is still listed in `repoPath`'s
19
+ * `git worktree list --porcelain` (i.e. git still has a registration for it).
20
+ */
21
+ export declare function worktreeIsRegistered(repoPath: string, worktreePath: string): boolean;
22
+ /**
23
+ * Remove a worktree ATOMICALLY: after this returns `true`, the directory is
24
+ * gone AND no `git worktree` registration for it remains.
25
+ *
26
+ * Delegates the removal to daemon-core's `removeWorktreeBase` (which runs
27
+ * `git worktree remove --force`, or an `rmSync` fallback + `git worktree
28
+ * prune`), then VERIFIES the post-condition. If a stale registration lingers
29
+ * (the fallback path can leave one until prune), it prunes and re-checks. Any
30
+ * remaining inconsistency -- a surviving directory or registration -- is logged
31
+ * loudly and returns `false`, so an unregistered leftover can never pass
32
+ * silently as a clean reap.
33
+ *
34
+ * Signature matches daemon-core `removeWorktreeBase` so it is a drop-in.
35
+ */
36
+ export declare function reapWorktreeAtomically(repoPath: string, worktreePath: string): boolean;
37
+ /** Verdict on whether an unregistered residue checkout is safe to reap. */
38
+ export interface ResidueClassification {
39
+ /** True iff the residue's content is already reachable from integration. */
40
+ reapSafe: boolean;
41
+ /** Human-readable justification (for the crash-recovery log). */
42
+ reason: string;
43
+ }
44
+ /**
45
+ * Classify whether an UNREGISTERED residue checkout (a directory under the
46
+ * worktree dir with no `git worktree list` entry) is safe to reap. Because
47
+ * there is no branch ref to diff, safety is judged by content reachability from
48
+ * integration:
49
+ *
50
+ * - Functional checkout (its HEAD still resolves): reap-safe iff it has no
51
+ * uncommitted changes AND its HEAD commit is an ancestor of integration.
52
+ * - Broken/.git-less checkout: reap-safe iff every file matches the same
53
+ * path's blob in integration ({@link classifyResidueByContent}).
54
+ *
55
+ * Every uncertain case resolves to `reapSafe: false` (preserve) so unmerged
56
+ * work is never destroyed -- the atomic-reap half of this focus ensures no NEW
57
+ * residue accumulates while preserved residue awaits manual review.
58
+ */
59
+ export declare function classifyUnregisteredResidue(residuePath: string, repoPath: string, integrationBranch: string): ResidueClassification;
60
+ //# sourceMappingURL=reap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reap.d.ts","sourceRoot":"","sources":["../../src/worktree/reap.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAUH;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,OAAO,CAAC;AAEhD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAUpF;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAuBtF;AAED,2EAA2E;AAC3E,MAAM,WAAW,qBAAqB;IACpC,4EAA4E;IAC5E,QAAQ,EAAE,OAAO,CAAC;IAClB,iEAAiE;IACjE,MAAM,EAAE,MAAM,CAAC;CAChB;AAwED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,2BAA2B,CACzC,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,iBAAiB,EAAE,MAAM,GACxB,qBAAqB,CAsBvB"}
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Atomic worktree reaping.
3
+ *
4
+ * "The daemon owns the boundary of every path it creates." A worktree checkout
5
+ * on disk and its `.git/worktrees/<id>` registration must live and die together
6
+ * -- a directory left behind WITHOUT its registration is invisible to
7
+ * `git worktree list`/`prune` and accumulates as unbounded residue (the 4.9 GB
8
+ * BELL leak). This module wraps daemon-core's base removal with an explicit
9
+ * post-condition check so a reap can never silently leave that residue.
10
+ */
11
+ import { existsSync, readdirSync } from 'node:fs';
12
+ import { resolve, join } from 'node:path';
13
+ import { runGitSync } from '../git/index.js';
14
+ import { removeWorktreeBase } from '@telora/daemon-core';
15
+ import { createLogger } from '../log.js';
16
+ const log = createLogger({ module: 'worktree' });
17
+ /**
18
+ * Cap on the number of files we blob-verify for an UNREGISTERED, .git-less
19
+ * residue checkout. Above this we cannot cheaply prove reachability, so we
20
+ * preserve (safe) rather than stall crash recovery hashing a huge tree.
21
+ */
22
+ export const MAX_RESIDUE_FILES_TO_VERIFY = 2000;
23
+ /**
24
+ * True iff `worktreePath` is still listed in `repoPath`'s
25
+ * `git worktree list --porcelain` (i.e. git still has a registration for it).
26
+ */
27
+ export function worktreeIsRegistered(repoPath, worktreePath) {
28
+ const result = runGitSync(['worktree', 'list', '--porcelain'], repoPath);
29
+ if (!result.success)
30
+ return false;
31
+ const target = resolve(worktreePath);
32
+ for (const line of result.output.split('\n')) {
33
+ if (line.startsWith('worktree ')) {
34
+ if (resolve(line.slice('worktree '.length).trim()) === target)
35
+ return true;
36
+ }
37
+ }
38
+ return false;
39
+ }
40
+ /**
41
+ * Remove a worktree ATOMICALLY: after this returns `true`, the directory is
42
+ * gone AND no `git worktree` registration for it remains.
43
+ *
44
+ * Delegates the removal to daemon-core's `removeWorktreeBase` (which runs
45
+ * `git worktree remove --force`, or an `rmSync` fallback + `git worktree
46
+ * prune`), then VERIFIES the post-condition. If a stale registration lingers
47
+ * (the fallback path can leave one until prune), it prunes and re-checks. Any
48
+ * remaining inconsistency -- a surviving directory or registration -- is logged
49
+ * loudly and returns `false`, so an unregistered leftover can never pass
50
+ * silently as a clean reap.
51
+ *
52
+ * Signature matches daemon-core `removeWorktreeBase` so it is a drop-in.
53
+ */
54
+ export function reapWorktreeAtomically(repoPath, worktreePath) {
55
+ const removed = removeWorktreeBase(repoPath, worktreePath);
56
+ if (!removed)
57
+ return false;
58
+ let dirGone = !existsSync(worktreePath);
59
+ let registered = worktreeIsRegistered(repoPath, worktreePath);
60
+ if (registered) {
61
+ // The rmSync-fallback path drops the directory but leaves the registration
62
+ // until a prune; converge it here so the two never diverge.
63
+ runGitSync(['worktree', 'prune'], repoPath);
64
+ registered = worktreeIsRegistered(repoPath, worktreePath);
65
+ dirGone = !existsSync(worktreePath);
66
+ }
67
+ if (!dirGone || registered) {
68
+ log.error(`Worktree reap left an inconsistent state at ${worktreePath}: ` +
69
+ `dirGone=${dirGone}, stillRegistered=${registered}. Manual cleanup may be required.`);
70
+ return false;
71
+ }
72
+ return true;
73
+ }
74
+ /**
75
+ * Recursively list repo-relative file paths under `dir`, EXCLUDING any `.git`
76
+ * entry (the residue's dangling gitdir pointer). Returns null on a read error,
77
+ * or once the count exceeds {@link MAX_RESIDUE_FILES_TO_VERIFY} (caller then
78
+ * preserves rather than verify an unbounded tree).
79
+ */
80
+ function listResidueFiles(dir) {
81
+ const out = [];
82
+ const walk = (abs, rel) => {
83
+ let entries;
84
+ try {
85
+ entries = readdirSync(abs, { withFileTypes: true });
86
+ }
87
+ catch {
88
+ return false;
89
+ }
90
+ for (const e of entries) {
91
+ if (e.name === '.git')
92
+ continue;
93
+ const childRel = rel ? `${rel}/${e.name}` : e.name;
94
+ if (e.isDirectory()) {
95
+ if (!walk(join(abs, e.name), childRel))
96
+ return false;
97
+ }
98
+ else {
99
+ out.push(childRel);
100
+ if (out.length > MAX_RESIDUE_FILES_TO_VERIFY)
101
+ return false;
102
+ }
103
+ }
104
+ return true;
105
+ };
106
+ return walk(dir, '') ? out : null;
107
+ }
108
+ /**
109
+ * Blob-level reachability check for a broken (.git-less) residue checkout: it is
110
+ * reap-safe ONLY when EVERY file it holds matches the same path's blob in the
111
+ * integration branch (content already reachable). Any new or modified file --
112
+ * i.e. potential unmerged work -- makes it unsafe (preserve). An unreadable or
113
+ * over-large tree is likewise preserved. Errs strictly toward preserve so a
114
+ * reap never loses work.
115
+ */
116
+ function classifyResidueByContent(residuePath, repoPath, integrationBranch) {
117
+ const files = listResidueFiles(residuePath);
118
+ if (files === null) {
119
+ return { reapSafe: false, reason: 'residue too large or unreadable to verify; preserved for manual review' };
120
+ }
121
+ if (files.length === 0) {
122
+ return { reapSafe: true, reason: 'residue holds no files' };
123
+ }
124
+ for (const rel of files) {
125
+ const abs = join(residuePath, rel);
126
+ const blob = runGitSync(['hash-object', abs], repoPath);
127
+ if (!blob.success || blob.output.trim().length === 0) {
128
+ return { reapSafe: false, reason: `could not hash residue file ${rel}; preserved` };
129
+ }
130
+ const intBlob = runGitSync(['rev-parse', '--verify', '--quiet', `${integrationBranch}:${rel}`], repoPath);
131
+ if (!intBlob.success || intBlob.output.trim().length === 0) {
132
+ return { reapSafe: false, reason: `${rel} is absent from ${integrationBranch} (potential unmerged work)` };
133
+ }
134
+ if (blob.output.trim() !== intBlob.output.trim()) {
135
+ return { reapSafe: false, reason: `${rel} differs from ${integrationBranch} (potential unmerged work)` };
136
+ }
137
+ }
138
+ return { reapSafe: true, reason: `all ${files.length} residue file(s) match ${integrationBranch}` };
139
+ }
140
+ /**
141
+ * Classify whether an UNREGISTERED residue checkout (a directory under the
142
+ * worktree dir with no `git worktree list` entry) is safe to reap. Because
143
+ * there is no branch ref to diff, safety is judged by content reachability from
144
+ * integration:
145
+ *
146
+ * - Functional checkout (its HEAD still resolves): reap-safe iff it has no
147
+ * uncommitted changes AND its HEAD commit is an ancestor of integration.
148
+ * - Broken/.git-less checkout: reap-safe iff every file matches the same
149
+ * path's blob in integration ({@link classifyResidueByContent}).
150
+ *
151
+ * Every uncertain case resolves to `reapSafe: false` (preserve) so unmerged
152
+ * work is never destroyed -- the atomic-reap half of this focus ensures no NEW
153
+ * residue accumulates while preserved residue awaits manual review.
154
+ */
155
+ export function classifyUnregisteredResidue(residuePath, repoPath, integrationBranch) {
156
+ const head = runGitSync(['rev-parse', '--verify', 'HEAD'], residuePath);
157
+ if (head.success && head.output.trim().length > 0) {
158
+ const sha = head.output.trim();
159
+ const status = runGitSync(['status', '--porcelain'], residuePath);
160
+ if (status.success && status.output.trim().length > 0) {
161
+ return { reapSafe: false, reason: 'functional checkout has uncommitted changes (potential unmerged work)' };
162
+ }
163
+ const ancestor = runGitSync(['merge-base', '--is-ancestor', sha, integrationBranch], repoPath);
164
+ if (ancestor.success) {
165
+ return { reapSafe: true, reason: `HEAD ${sha.slice(0, 8)} is reachable from ${integrationBranch}` };
166
+ }
167
+ return {
168
+ reapSafe: false,
169
+ reason: `HEAD ${sha.slice(0, 8)} holds commit(s) not reachable from ${integrationBranch}`,
170
+ };
171
+ }
172
+ // No resolvable HEAD -- the classic .git-less residue. Fall back to blobs.
173
+ return classifyResidueByContent(residuePath, repoPath, integrationBranch);
174
+ }
175
+ //# sourceMappingURL=reap.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reap.js","sourceRoot":"","sources":["../../src/worktree/reap.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAClD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAEzC,MAAM,GAAG,GAAG,YAAY,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;AAEjD;;;;GAIG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,IAAI,CAAC;AAEhD;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAAC,QAAgB,EAAE,YAAoB;IACzE,MAAM,MAAM,GAAG,UAAU,CAAC,CAAC,UAAU,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,QAAQ,CAAC,CAAC;IACzE,IAAI,CAAC,MAAM,CAAC,OAAO;QAAE,OAAO,KAAK,CAAC;IAClC,MAAM,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IACrC,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QAC7C,IAAI,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;YACjC,IAAI,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM;gBAAE,OAAO,IAAI,CAAC;QAC7E,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,sBAAsB,CAAC,QAAgB,EAAE,YAAoB;IAC3E,MAAM,OAAO,GAAG,kBAAkB,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;IAC3D,IAAI,CAAC,OAAO;QAAE,OAAO,KAAK,CAAC;IAE3B,IAAI,OAAO,GAAG,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC;IACxC,IAAI,UAAU,GAAG,oBAAoB,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;IAE9D,IAAI,UAAU,EAAE,CAAC;QACf,2EAA2E;QAC3E,4DAA4D;QAC5D,UAAU,CAAC,CAAC,UAAU,EAAE,OAAO,CAAC,EAAE,QAAQ,CAAC,CAAC;QAC5C,UAAU,GAAG,oBAAoB,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;QAC1D,OAAO,GAAG,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC;IACtC,CAAC;IAED,IAAI,CAAC,OAAO,IAAI,UAAU,EAAE,CAAC;QAC3B,GAAG,CAAC,KAAK,CACP,+CAA+C,YAAY,IAAI;YAC/D,WAAW,OAAO,qBAAqB,UAAU,mCAAmC,CACrF,CAAC;QACF,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAUD;;;;;GAKG;AACH,SAAS,gBAAgB,CAAC,GAAW;IACnC,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,MAAM,IAAI,GAAG,CAAC,GAAW,EAAE,GAAW,EAAW,EAAE;QACjD,IAAI,OAAO,CAAC;QACZ,IAAI,CAAC;YACH,OAAO,GAAG,WAAW,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QACtD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;QACD,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;YACxB,IAAI,CAAC,CAAC,IAAI,KAAK,MAAM;gBAAE,SAAS;YAChC,MAAM,QAAQ,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;YACnD,IAAI,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC;gBACpB,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC;oBAAE,OAAO,KAAK,CAAC;YACvD,CAAC;iBAAM,CAAC;gBACN,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;gBACnB,IAAI,GAAG,CAAC,MAAM,GAAG,2BAA2B;oBAAE,OAAO,KAAK,CAAC;YAC7D,CAAC;QACH,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC,CAAC;IACF,OAAO,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;AACpC,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,wBAAwB,CAC/B,WAAmB,EACnB,QAAgB,EAChB,iBAAyB;IAEzB,MAAM,KAAK,GAAG,gBAAgB,CAAC,WAAW,CAAC,CAAC;IAC5C,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACnB,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,wEAAwE,EAAE,CAAC;IAC/G,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,wBAAwB,EAAE,CAAC;IAC9D,CAAC;IACD,KAAK,MAAM,GAAG,IAAI,KAAK,EAAE,CAAC;QACxB,MAAM,GAAG,GAAG,IAAI,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC;QACnC,MAAM,IAAI,GAAG,UAAU,CAAC,CAAC,aAAa,EAAE,GAAG,CAAC,EAAE,QAAQ,CAAC,CAAC;QACxD,IAAI,CAAC,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACrD,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,+BAA+B,GAAG,aAAa,EAAE,CAAC;QACtF,CAAC;QACD,MAAM,OAAO,GAAG,UAAU,CACxB,CAAC,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,GAAG,iBAAiB,IAAI,GAAG,EAAE,CAAC,EACnE,QAAQ,CACT,CAAC;QACF,IAAI,CAAC,OAAO,CAAC,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC3D,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,GAAG,mBAAmB,iBAAiB,4BAA4B,EAAE,CAAC;QAC7G,CAAC;QACD,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC;YACjD,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,GAAG,iBAAiB,iBAAiB,4BAA4B,EAAE,CAAC;QAC3G,CAAC;IACH,CAAC;IACD,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,KAAK,CAAC,MAAM,0BAA0B,iBAAiB,EAAE,EAAE,CAAC;AACtG,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,2BAA2B,CACzC,WAAmB,EACnB,QAAgB,EAChB,iBAAyB;IAEzB,MAAM,IAAI,GAAG,UAAU,CAAC,CAAC,WAAW,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE,WAAW,CAAC,CAAC;IACxE,IAAI,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAClD,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QAC/B,MAAM,MAAM,GAAG,UAAU,CAAC,CAAC,QAAQ,EAAE,aAAa,CAAC,EAAE,WAAW,CAAC,CAAC;QAClE,IAAI,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACtD,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,uEAAuE,EAAE,CAAC;QAC9G,CAAC;QACD,MAAM,QAAQ,GAAG,UAAU,CACzB,CAAC,YAAY,EAAE,eAAe,EAAE,GAAG,EAAE,iBAAiB,CAAC,EACvD,QAAQ,CACT,CAAC;QACF,IAAI,QAAQ,CAAC,OAAO,EAAE,CAAC;YACrB,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,sBAAsB,iBAAiB,EAAE,EAAE,CAAC;QACtG,CAAC;QACD,OAAO;YACL,QAAQ,EAAE,KAAK;YACf,MAAM,EAAE,QAAQ,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,uCAAuC,iBAAiB,EAAE;SAC1F,CAAC;IACJ,CAAC;IACD,2EAA2E;IAC3E,OAAO,wBAAwB,CAAC,WAAW,EAAE,QAAQ,EAAE,iBAAiB,CAAC,CAAC;AAC5E,CAAC"}
@@ -4,8 +4,16 @@
4
4
  * Uncommitted change detection, unmerged commit checks, safety commits,
5
5
  * guarded worktree removal, and WIP commit after agent exit.
6
6
  */
7
+ import { type SweepRefusalReporter } from './scratch-guard.js';
8
+ import { type WorktreeIdentityViolationReporter } from './identity.js';
7
9
  /**
8
10
  * Check if a worktree has uncommitted changes (dirty working tree).
11
+ *
12
+ * Identity-guarded: if the directory is not the worktree it names (a `.git`-less
13
+ * checkout whose `git status` would report the ENCLOSING repo's tree), the
14
+ * enclosing repo's state is never attributed to this worktree -- the mismatch is
15
+ * logged loudly and the function returns false. The write paths (guardedCommit,
16
+ * removeWorktree) are the hard gate; this keeps the read from lying.
9
17
  */
10
18
  export declare function worktreeHasUncommittedChanges(worktreePath: string): boolean;
11
19
  /**
@@ -16,7 +24,7 @@ export declare function branchHasUnmergedCommits(branchName: string, targetBranc
16
24
  * Commit all uncommitted changes in a worktree as a safety net.
17
25
  * Returns the commit hash if successful, null otherwise.
18
26
  */
19
- export declare function safetyCommitWorktree(worktreePath: string, deliveryName: string): string | null;
27
+ export declare function safetyCommitWorktree(worktreePath: string, deliveryName: string, reporter?: SweepRefusalReporter): string | null;
20
28
  /**
21
29
  * Generated lockfile basenames the daemon may safely auto-commit. These are
22
30
  * machine-regenerated (never hand-authored), so a worktree whose ONLY dirt is
@@ -52,7 +60,7 @@ export declare function dirtyChangesAreGeneratedLockfilesOnly(worktreePath: stri
52
60
  * agent work is never silently committed here. Returns the commit hash, or
53
61
  * null if staging/committing failed (e.g. nothing staged).
54
62
  */
55
- export declare function commitGeneratedLockfiles(worktreePath: string): string | null;
63
+ export declare function commitGeneratedLockfiles(worktreePath: string, reporter?: SweepRefusalReporter): string | null;
56
64
  /**
57
65
  * Remove a worktree when an agent session ends.
58
66
  * The branch is preserved for PR creation.
@@ -68,6 +76,7 @@ export declare function removeWorktree(repoPath: string, worktreePath: string, o
68
76
  integrationBranch?: string;
69
77
  deliveryName?: string;
70
78
  mergeSucceeded?: boolean;
79
+ identityReporter?: WorktreeIdentityViolationReporter;
71
80
  }): boolean;
72
81
  /**
73
82
  * Commit any uncommitted changes in a worktree as a WIP commit.
@@ -76,9 +85,11 @@ export declare function removeWorktree(repoPath: string, worktreePath: string, o
76
85
  * removal. Captures in-flight work that the agent didn't commit before
77
86
  * being terminated.
78
87
  *
79
- * Delegates to daemon-core's commitWip for the actual git operations.
88
+ * Stages via the scoped-staging guard (scratch-excluded, sweep-refused) and
89
+ * commits in the daemon package -- it no longer delegates to daemon-core's
90
+ * blind-`git add -A` commitWip, so the WIP path is guarded like every other.
80
91
  *
81
92
  * @returns The commit hash if a WIP commit was created, null if no changes or failure.
82
93
  */
83
- export declare function commitWipChanges(worktreePath: string, contextName: string): string | null;
94
+ export declare function commitWipChanges(worktreePath: string, contextName: string, reporter?: SweepRefusalReporter): string | null;
84
95
  //# sourceMappingURL=safety.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"safety.d.ts","sourceRoot":"","sources":["../../src/worktree/safety.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAaH;;GAEG;AACH,wBAAgB,6BAA6B,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAI3E;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CACtC,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,GACf,OAAO,CAMT;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAqB9F;AAED;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B,EAAE,SAAS,MAAM,EAMzD,CAAC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,qCAAqC,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAWnF;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAgB5E;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE;IACP,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,OAAO,CAAC;CACrB,GACL,OAAO,CAgDT;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGzF"}
1
+ {"version":3,"file":"safety.d.ts","sourceRoot":"","sources":["../../src/worktree/safety.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,EAGL,KAAK,oBAAoB,EAC1B,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAGL,KAAK,iCAAiC,EACvC,MAAM,eAAe,CAAC;AAMvB;;;;;;;;GAQG;AACH,wBAAgB,6BAA6B,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAa3E;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CACtC,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,GACf,OAAO,CAMT;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,EACpB,QAAQ,CAAC,EAAE,oBAAoB,GAC9B,MAAM,GAAG,IAAI,CAUf;AAED;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B,EAAE,SAAS,MAAM,EAMzD,CAAC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,qCAAqC,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAWnF;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CACtC,YAAY,EAAE,MAAM,EACpB,QAAQ,CAAC,EAAE,oBAAoB,GAC9B,MAAM,GAAG,IAAI,CAUf;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE;IACP,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,gBAAgB,CAAC,EAAE,iCAAiC,CAAC;CACjD,GACL,OAAO,CAsET;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAC9B,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,EACnB,QAAQ,CAAC,EAAE,oBAAoB,GAC9B,MAAM,GAAG,IAAI,CAQf"}
@@ -6,15 +6,30 @@
6
6
  */
7
7
  import { existsSync } from 'node:fs';
8
8
  import { runGitSync } from '../git/index.js';
9
- import { commitWip, removeWorktreeBase, } from '@telora/daemon-core';
9
+ import { guardedCommit, worktreeHasNonScratchChanges, } from './scratch-guard.js';
10
+ import { reapWorktreeAtomically } from './reap.js';
11
+ import { assertWorktreeIdentity, defaultWorktreeIdentityViolationReporter, } from './identity.js';
10
12
  import { createLogger } from '../log.js';
11
13
  const log = createLogger({ module: 'worktree' });
12
14
  /**
13
15
  * Check if a worktree has uncommitted changes (dirty working tree).
16
+ *
17
+ * Identity-guarded: if the directory is not the worktree it names (a `.git`-less
18
+ * checkout whose `git status` would report the ENCLOSING repo's tree), the
19
+ * enclosing repo's state is never attributed to this worktree -- the mismatch is
20
+ * logged loudly and the function returns false. The write paths (guardedCommit,
21
+ * removeWorktree) are the hard gate; this keeps the read from lying.
14
22
  */
15
23
  export function worktreeHasUncommittedChanges(worktreePath) {
16
24
  if (!existsSync(worktreePath))
17
25
  return false;
26
+ const identity = assertWorktreeIdentity(worktreePath);
27
+ if (!identity.ok) {
28
+ log.error(`worktreeHasUncommittedChanges: ${worktreePath} is not its own worktree ` +
29
+ `(git resolves it to ${identity.resolvedRepo ?? 'no repository'}); refusing ` +
30
+ `to attribute the enclosing repo's status to it.`);
31
+ return false;
32
+ }
18
33
  const result = runGitSync(['status', '--porcelain'], worktreePath);
19
34
  return result.success && result.output.trim().length > 0;
20
35
  }
@@ -29,22 +44,16 @@ export function branchHasUnmergedCommits(branchName, targetBranch, repoPath) {
29
44
  * Commit all uncommitted changes in a worktree as a safety net.
30
45
  * Returns the commit hash if successful, null otherwise.
31
46
  */
32
- export function safetyCommitWorktree(worktreePath, deliveryName) {
33
- // Stage everything
34
- const addResult = runGitSync(['add', '-A'], worktreePath);
35
- if (!addResult.success) {
36
- log.error(`Safety commit: failed to stage changes: ${addResult.error}`);
37
- return null;
38
- }
39
- // Commit
40
- const commitResult = runGitSync(['commit', '-m', `wip: uncommitted agent work for "${deliveryName}" (safety commit)`], worktreePath);
41
- if (!commitResult.success) {
42
- log.warn(`Safety commit: commit failed (may be empty): ${commitResult.error}`);
43
- return null;
44
- }
45
- // Return the new commit hash
46
- const hashResult = runGitSync(['rev-parse', 'HEAD'], worktreePath);
47
- return hashResult.success ? hashResult.output : null;
47
+ export function safetyCommitWorktree(worktreePath, deliveryName, reporter) {
48
+ // Routes through the scoped-staging guard: daemon scratch (`.telora/`) is
49
+ // never staged, and a mass sweep is refused (working tree left untouched,
50
+ // reporter invoked) rather than committed.
51
+ return guardedCommit({
52
+ worktreePath,
53
+ message: `wip: uncommitted agent work for "${deliveryName}" (safety commit)`,
54
+ context: deliveryName,
55
+ reporter,
56
+ });
48
57
  }
49
58
  /**
50
59
  * Generated lockfile basenames the daemon may safely auto-commit. These are
@@ -101,19 +110,16 @@ export function dirtyChangesAreGeneratedLockfilesOnly(worktreePath) {
101
110
  * agent work is never silently committed here. Returns the commit hash, or
102
111
  * null if staging/committing failed (e.g. nothing staged).
103
112
  */
104
- export function commitGeneratedLockfiles(worktreePath) {
105
- const addResult = runGitSync(['add', '-A'], worktreePath);
106
- if (!addResult.success) {
107
- log.error(`lockfile auto-commit: failed to stage changes: ${addResult.error}`);
108
- return null;
109
- }
110
- const commitResult = runGitSync(['commit', '-m', 'chore: auto-commit generated lockfile churn (daemon remediation)'], worktreePath);
111
- if (!commitResult.success) {
112
- log.warn(`lockfile auto-commit: commit failed (may be empty): ${commitResult.error}`);
113
- return null;
114
- }
115
- const hashResult = runGitSync(['rev-parse', 'HEAD'], worktreePath);
116
- return hashResult.success ? hashResult.output : null;
113
+ export function commitGeneratedLockfiles(worktreePath, reporter) {
114
+ // Callers gate this behind dirtyChangesAreGeneratedLockfilesOnly, so the
115
+ // staged set is a handful of lockfiles; the guard is pure defense-in-depth
116
+ // (scratch exclusion + sweep refusal) shared with every other commit path.
117
+ return guardedCommit({
118
+ worktreePath,
119
+ message: 'chore: auto-commit generated lockfile churn (daemon remediation)',
120
+ context: 'generated-lockfile churn',
121
+ reporter,
122
+ });
117
123
  }
118
124
  /**
119
125
  * Remove a worktree when an agent session ends.
@@ -133,8 +139,27 @@ export function removeWorktree(repoPath, worktreePath, options = {}) {
133
139
  runGitSync(['worktree', 'prune'], repoPath);
134
140
  return true;
135
141
  }
136
- // Guard 1: Check for uncommitted changes and safety-commit them
137
- if (worktreeHasUncommittedChanges(worktreePath)) {
142
+ // Guard 0: Identity. If the directory is not the worktree it names (a
143
+ // `.git`-less checkout whose git commands redirect at the enclosing repo),
144
+ // refuse before the dirty-check/safety-commit guards run -- otherwise the
145
+ // dirty-check would read the parent's tree and the safety commit would stage
146
+ // it. Unregistered residue is reaped via the crash-recovery residue path,
147
+ // not here.
148
+ const identity = assertWorktreeIdentity(worktreePath);
149
+ if (!identity.ok) {
150
+ const identityReporter = options.identityReporter ?? defaultWorktreeIdentityViolationReporter;
151
+ identityReporter({
152
+ worktreePath,
153
+ resolvedRepo: identity.resolvedRepo,
154
+ operation: 'remove',
155
+ });
156
+ return false;
157
+ }
158
+ // Guard 1: Safety-commit real (non-scratch) uncommitted work. Daemon
159
+ // scratch (`.telora/`) is disposable, so a worktree dirty only with scratch
160
+ // is treated as clean here -- it is discarded with the worktree, never
161
+ // committed and never a reason to refuse removal.
162
+ if (worktreeHasNonScratchChanges(worktreePath)) {
138
163
  log.warn(`Worktree has uncommitted changes: ${worktreePath}. ` +
139
164
  `Creating safety commit to preserve work...`);
140
165
  const hash = safetyCommitWorktree(worktreePath, deliveryName ?? 'unknown');
@@ -142,9 +167,10 @@ export function removeWorktree(repoPath, worktreePath, options = {}) {
142
167
  log.info(`Safety commit created: ${hash.slice(0, 8)}`);
143
168
  }
144
169
  else {
145
- // Could not commit -- refuse to delete
170
+ // Could not commit (guard refused a mass sweep, or the commit failed) --
171
+ // refuse to delete so the work stays recoverable.
146
172
  log.error(`REFUSING to remove worktree ${worktreePath}: ` +
147
- `has uncommitted changes and safety commit failed. ` +
173
+ `has uncommitted changes and safety commit failed or was refused. ` +
148
174
  `Manual cleanup required.`);
149
175
  return false;
150
176
  }
@@ -158,8 +184,8 @@ export function removeWorktree(repoPath, worktreePath, options = {}) {
158
184
  return false;
159
185
  }
160
186
  }
161
- // Safe to remove -- delegate to daemon-core's base removal
162
- return removeWorktreeBase(repoPath, worktreePath);
187
+ // Safe to remove -- atomic reap (dir + registration gone together, verified)
188
+ return reapWorktreeAtomically(repoPath, worktreePath);
163
189
  }
164
190
  catch (err) {
165
191
  log.error('Error removing worktree:', err instanceof Error ? err.message : String(err));
@@ -173,12 +199,19 @@ export function removeWorktree(repoPath, worktreePath, options = {}) {
173
199
  * removal. Captures in-flight work that the agent didn't commit before
174
200
  * being terminated.
175
201
  *
176
- * Delegates to daemon-core's commitWip for the actual git operations.
202
+ * Stages via the scoped-staging guard (scratch-excluded, sweep-refused) and
203
+ * commits in the daemon package -- it no longer delegates to daemon-core's
204
+ * blind-`git add -A` commitWip, so the WIP path is guarded like every other.
177
205
  *
178
206
  * @returns The commit hash if a WIP commit was created, null if no changes or failure.
179
207
  */
180
- export function commitWipChanges(worktreePath, contextName) {
208
+ export function commitWipChanges(worktreePath, contextName, reporter) {
181
209
  log.info(`Committing WIP changes in ${worktreePath} for "${contextName}"`);
182
- return commitWip(worktreePath, `Uncommitted work captured by daemon on agent exit (${contextName})`);
210
+ return guardedCommit({
211
+ worktreePath,
212
+ message: `WIP: Uncommitted work captured by daemon on agent exit (${contextName})`,
213
+ context: contextName,
214
+ reporter,
215
+ });
183
216
  }
184
217
  //# sourceMappingURL=safety.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"safety.js","sourceRoot":"","sources":["../../src/worktree/safety.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"safety.js","sourceRoot":"","sources":["../../src/worktree/safety.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Scoped-staging guard for daemon-authored safety / WIP / lockfile commits.
3
+ *
4
+ * "The daemon owns the boundary of every path it creates." A blind `git add -A`
5
+ * in a repo whose `.gitignore` is missing or broken can sweep the entire
6
+ * worktree tree (nested agent checkouts under `.telora/`) into history -- the
7
+ * 15,684-file BELL-CSNv5 incident. Every daemon-authored commit MUST route its
8
+ * staging through this guard so that:
9
+ *
10
+ * 1. Daemon scratch (`.telora/` and, optionally, the configured worktree dir)
11
+ * is NEVER staged, regardless of the repo's ignore state -- enforced by a
12
+ * git `:(exclude)` pathspec, not by trusting `.gitignore`.
13
+ * 2. A commit whose (post-exclusion) staged file count exceeds a sane
14
+ * threshold is REFUSED -- the guard stages nothing and leaves the working
15
+ * tree untouched so the caller can escalate and the real work stays
16
+ * recoverable.
17
+ *
18
+ * This is defense-in-depth BEHIND the boot-time `.gitignore` convergence: even
19
+ * with a correct ignore, the sweep must be structurally impossible here.
20
+ */
21
+ import { type WorktreeIdentityViolationReporter } from './identity.js';
22
+ /** Conservative default sweep ceiling for a single automated commit. */
23
+ export declare const DEFAULT_SAFETY_COMMIT_MAX_FILES = 200;
24
+ /**
25
+ * Daemon scratch path prefixes that must never be staged by an automated
26
+ * commit. `.telora/` is the root of everything the daemon writes into a repo
27
+ * (config, worktrees, session logs); the daemon's worktree dir is nested under
28
+ * it, so this single prefix covers the sweep vector. Extra prefixes (e.g. a
29
+ * worktree dir configured OUTSIDE `.telora`) can be supplied per call.
30
+ */
31
+ export declare const DEFAULT_SCRATCH_PREFIXES: readonly string[];
32
+ /**
33
+ * Resolve the max-file sweep threshold via the canonical env-config index
34
+ * (`TELORA_SAFETY_COMMIT_MAX_FILES` in `@telora/daemon-core` `env-config.ts`).
35
+ * An invalid value fails loudly at parse time, per the env-config contract --
36
+ * there is no silent fallback. The daemon-core default must match
37
+ * {@link DEFAULT_SAFETY_COMMIT_MAX_FILES}; the scratch-guard test pins the two
38
+ * together.
39
+ */
40
+ export declare function resolveSafetyCommitMaxFiles(env?: NodeJS.ProcessEnv): number;
41
+ export interface ScopedStageOptions {
42
+ /** Scratch prefixes to hard-exclude (defaults to {@link DEFAULT_SCRATCH_PREFIXES}). */
43
+ scratchPrefixes?: readonly string[];
44
+ /** Max post-exclusion staged files before refusal (defaults to env / {@link DEFAULT_SAFETY_COMMIT_MAX_FILES}). */
45
+ maxFiles?: number;
46
+ }
47
+ /** Outcome of a scoped staging attempt. */
48
+ export type ScopedStageResult = {
49
+ kind: 'staged';
50
+ staged: string[];
51
+ denied: string[];
52
+ } | {
53
+ kind: 'refused_threshold';
54
+ /** Post-exclusion count that tripped the guard. */
55
+ count: number;
56
+ /** Bounded sample of the tripping paths for the escalation. */
57
+ sample: string[];
58
+ denied: string[];
59
+ maxFiles: number;
60
+ } | {
61
+ kind: 'error';
62
+ message: string;
63
+ };
64
+ /**
65
+ * Stage the worktree's changes for a daemon-authored commit, HARD-excluding
66
+ * daemon scratch and REFUSING a mass sweep.
67
+ *
68
+ * On success stages everything except the scratch prefixes (via a git
69
+ * `:(exclude)` pathspec, so exclusion holds even when scratch is not
70
+ * gitignored). On threshold refusal stages NOTHING and returns the tripping
71
+ * count + a bounded sample so the caller can preserve the tree and escalate.
72
+ *
73
+ * Does not commit -- callers commit after a `staged` result.
74
+ */
75
+ export declare function stageExcludingDaemonScratch(worktreePath: string, opts?: ScopedStageOptions): ScopedStageResult;
76
+ /**
77
+ * True iff the worktree has uncommitted changes OUTSIDE daemon scratch. Daemon
78
+ * scratch (`.telora/`) is disposable, so a worktree dirty only with scratch has
79
+ * no agent work to preserve -- callers use this to decide whether a safety
80
+ * commit is even needed before removal.
81
+ */
82
+ export declare function worktreeHasNonScratchChanges(worktreePath: string, scratchPrefixes?: readonly string[]): boolean;
83
+ /** Details of a refused mass-sweep, handed to a {@link SweepRefusalReporter}. */
84
+ export interface SweepRefusalReport {
85
+ worktreePath: string;
86
+ /** Human label for the commit context (delivery/focus name). */
87
+ context: string;
88
+ /** Post-exclusion staged count that tripped the guard. */
89
+ count: number;
90
+ /** Bounded sample of the tripping paths. */
91
+ sample: string[];
92
+ maxFiles: number;
93
+ }
94
+ /**
95
+ * Callback invoked when the guard refuses a mass sweep. Injected by a
96
+ * context-holding caller to file an escalation; defaults to a loud log so every
97
+ * refusal is at least named in the daemon log.
98
+ */
99
+ export type SweepRefusalReporter = (report: SweepRefusalReport) => void;
100
+ /** Default reporter: loudly name the worktree, count, and a path sample. */
101
+ export declare const defaultSweepRefusalReporter: SweepRefusalReporter;
102
+ /**
103
+ * Stage (scratch-excluded, sweep-guarded) and commit a daemon-authored commit.
104
+ * The ONE implementation every daemon safety/WIP/lockfile commit routes
105
+ * through, so none can stage `.telora/` scratch or sweep the tree.
106
+ *
107
+ * Returns the commit sha on success, or null when there was nothing to commit
108
+ * (clean or scratch-only tree), the worktree failed the identity check, the
109
+ * sweep was refused, or a git op failed. On a threshold refusal the working
110
+ * tree is left untouched and the reporter is invoked exactly once.
111
+ *
112
+ * Identity is asserted BEFORE any staging: if the directory is not the worktree
113
+ * it names (e.g. a `.git`-less checkout whose git commands resolve at the
114
+ * enclosing repo), nothing is staged or committed and the identity reporter is
115
+ * invoked once -- so a daemon safety/WIP/lockfile commit can never land in the
116
+ * parent repository's tree.
117
+ */
118
+ export declare function guardedCommit(params: {
119
+ worktreePath: string;
120
+ message: string;
121
+ context: string;
122
+ reporter?: SweepRefusalReporter;
123
+ identityReporter?: WorktreeIdentityViolationReporter;
124
+ options?: ScopedStageOptions;
125
+ }): string | null;
126
+ //# sourceMappingURL=scratch-guard.d.ts.map