@telora/daemon 0.24.2 → 0.24.7

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 (165) hide show
  1. package/README.md +11 -3
  2. package/build-info.json +8 -3
  3. package/dist/backends/claude/one-shot-model.d.ts.map +1 -1
  4. package/dist/backends/claude/one-shot-model.js +2 -0
  5. package/dist/backends/claude/one-shot-model.js.map +1 -1
  6. package/dist/ci/ci-status.d.ts.map +1 -1
  7. package/dist/ci/ci-status.js +1 -0
  8. package/dist/ci/ci-status.js.map +1 -1
  9. package/dist/ci/product-ci-repo.d.ts.map +1 -1
  10. package/dist/ci/product-ci-repo.js +3 -1
  11. package/dist/ci/product-ci-repo.js.map +1 -1
  12. package/dist/cli/env-command.d.ts +21 -0
  13. package/dist/cli/env-command.d.ts.map +1 -0
  14. package/dist/cli/env-command.js +42 -0
  15. package/dist/cli/env-command.js.map +1 -0
  16. package/dist/completion/state-cascade/review-approved-head-gate.d.ts.map +1 -1
  17. package/dist/completion/state-cascade/review-approved-head-gate.js +2 -0
  18. package/dist/completion/state-cascade/review-approved-head-gate.js.map +1 -1
  19. package/dist/completion/state-cascade/review-capture-head-gate.d.ts +28 -0
  20. package/dist/completion/state-cascade/review-capture-head-gate.d.ts.map +1 -0
  21. package/dist/completion/state-cascade/review-capture-head-gate.js +48 -0
  22. package/dist/completion/state-cascade/review-capture-head-gate.js.map +1 -0
  23. package/dist/completion/state-cascade/state-cascade.d.ts +12 -0
  24. package/dist/completion/state-cascade/state-cascade.d.ts.map +1 -1
  25. package/dist/completion/state-cascade/state-cascade.js +19 -10
  26. package/dist/completion/state-cascade/state-cascade.js.map +1 -1
  27. package/dist/composition/cli-env-file-context.d.ts +4 -0
  28. package/dist/composition/cli-env-file-context.d.ts.map +1 -0
  29. package/dist/composition/cli-env-file-context.js +27 -0
  30. package/dist/composition/cli-env-file-context.js.map +1 -0
  31. package/dist/composition/daemon-contexts.d.ts.map +1 -1
  32. package/dist/composition/daemon-contexts.js +3 -1
  33. package/dist/composition/daemon-contexts.js.map +1 -1
  34. package/dist/composition/execution-git-context.d.ts.map +1 -1
  35. package/dist/composition/execution-git-context.js +1 -0
  36. package/dist/composition/execution-git-context.js.map +1 -1
  37. package/dist/composition/execution-worktree-context.js +1 -1
  38. package/dist/composition/release-note-pass-context.d.ts +7 -0
  39. package/dist/composition/release-note-pass-context.d.ts.map +1 -0
  40. package/dist/composition/release-note-pass-context.js +64 -0
  41. package/dist/composition/release-note-pass-context.js.map +1 -0
  42. package/dist/composition/runtime-contexts.d.ts.map +1 -1
  43. package/dist/composition/runtime-contexts.js +2 -0
  44. package/dist/composition/runtime-contexts.js.map +1 -1
  45. package/dist/composition/spawn-pass-deps.d.ts +2 -2
  46. package/dist/composition/spawn-pass-deps.d.ts.map +1 -1
  47. package/dist/composition/spawn-pass-deps.js +3 -1
  48. package/dist/composition/spawn-pass-deps.js.map +1 -1
  49. package/dist/composition/state-cascade-context.d.ts.map +1 -1
  50. package/dist/composition/state-cascade-context.js +2 -0
  51. package/dist/composition/state-cascade-context.js.map +1 -1
  52. package/dist/config/env-file.d.ts +37 -0
  53. package/dist/config/env-file.d.ts.map +1 -0
  54. package/dist/config/env-file.js +85 -0
  55. package/dist/config/env-file.js.map +1 -0
  56. package/dist/config/live-daemon-config.js +1 -1
  57. package/dist/daemon-state/daemon-process.d.ts +10 -0
  58. package/dist/daemon-state/daemon-process.d.ts.map +1 -1
  59. package/dist/daemon-state/daemon-process.js +24 -15
  60. package/dist/daemon-state/daemon-process.js.map +1 -1
  61. package/dist/daemon-state/provider-window.d.ts.map +1 -1
  62. package/dist/daemon-state/provider-window.js +1 -0
  63. package/dist/daemon-state/provider-window.js.map +1 -1
  64. package/dist/directive/directive-queue.js +1 -1
  65. package/dist/focus/daemon-contexts.d.ts +3 -0
  66. package/dist/focus/daemon-contexts.d.ts.map +1 -1
  67. package/dist/focus/daemon-contexts.js.map +1 -1
  68. package/dist/focus/focus-loop-registry.d.ts.map +1 -1
  69. package/dist/focus/focus-loop-registry.js +21 -0
  70. package/dist/focus/focus-loop-registry.js.map +1 -1
  71. package/dist/focus/pending-review-approved-head.d.ts +5 -1
  72. package/dist/focus/pending-review-approved-head.d.ts.map +1 -1
  73. package/dist/focus/pending-review-approved-head.js +35 -1
  74. package/dist/focus/pending-review-approved-head.js.map +1 -1
  75. package/dist/focus/review-spawn-coverage.d.ts +11 -0
  76. package/dist/focus/review-spawn-coverage.d.ts.map +1 -1
  77. package/dist/focus/review-spawn-coverage.js +14 -0
  78. package/dist/focus/review-spawn-coverage.js.map +1 -1
  79. package/dist/git/delivery-trailer.d.ts.map +1 -1
  80. package/dist/git/delivery-trailer.js +2 -0
  81. package/dist/git/delivery-trailer.js.map +1 -1
  82. package/dist/git/utils.d.ts.map +1 -1
  83. package/dist/git/utils.js +2 -1
  84. package/dist/git/utils.js.map +1 -1
  85. package/dist/index.js +14 -7
  86. package/dist/index.js.map +1 -1
  87. package/dist/merge/candidate-rework.d.ts.map +1 -1
  88. package/dist/merge/candidate-rework.js +24 -2
  89. package/dist/merge/candidate-rework.js.map +1 -1
  90. package/dist/merge/completion-merge-activity.d.ts.map +1 -1
  91. package/dist/merge/completion-merge-activity.js +7 -1
  92. package/dist/merge/completion-merge-activity.js.map +1 -1
  93. package/dist/merge/completion-merge-driver.d.ts +15 -5
  94. package/dist/merge/completion-merge-driver.d.ts.map +1 -1
  95. package/dist/merge/completion-merge-driver.js +7 -2
  96. package/dist/merge/completion-merge-driver.js.map +1 -1
  97. package/dist/merge/completion-merge-gate.d.ts +5 -1
  98. package/dist/merge/completion-merge-gate.d.ts.map +1 -1
  99. package/dist/merge/completion-merge-gate.js +23 -7
  100. package/dist/merge/completion-merge-gate.js.map +1 -1
  101. package/dist/merge/merge-back-loop.d.ts.map +1 -1
  102. package/dist/merge/merge-back-loop.js +2 -1
  103. package/dist/merge/merge-back-loop.js.map +1 -1
  104. package/dist/merge/merge-gate-coverage.d.ts +0 -2
  105. package/dist/merge/merge-gate-coverage.d.ts.map +1 -1
  106. package/dist/merge/merge-gate-coverage.js +11 -9
  107. package/dist/merge/merge-gate-coverage.js.map +1 -1
  108. package/dist/prompt-sections/release-note-faithfulness-section.d.ts +28 -0
  109. package/dist/prompt-sections/release-note-faithfulness-section.d.ts.map +1 -0
  110. package/dist/prompt-sections/release-note-faithfulness-section.js +99 -0
  111. package/dist/prompt-sections/release-note-faithfulness-section.js.map +1 -0
  112. package/dist/prompt-sections/release-note-section.d.ts +58 -0
  113. package/dist/prompt-sections/release-note-section.d.ts.map +1 -0
  114. package/dist/prompt-sections/release-note-section.js +285 -0
  115. package/dist/prompt-sections/release-note-section.js.map +1 -0
  116. package/dist/queries/api-client/shared.d.ts +1 -0
  117. package/dist/queries/api-client/shared.d.ts.map +1 -1
  118. package/dist/queries/api-client/shared.js +16 -1
  119. package/dist/queries/api-client/shared.js.map +1 -1
  120. package/dist/queries/release-notes/release-notes.d.ts +242 -0
  121. package/dist/queries/release-notes/release-notes.d.ts.map +1 -0
  122. package/dist/queries/release-notes/release-notes.js +148 -0
  123. package/dist/queries/release-notes/release-notes.js.map +1 -0
  124. package/dist/release-notes/release-note-author.d.ts +56 -0
  125. package/dist/release-notes/release-note-author.d.ts.map +1 -0
  126. package/dist/release-notes/release-note-author.js +107 -0
  127. package/dist/release-notes/release-note-author.js.map +1 -0
  128. package/dist/release-notes/release-note-faithfulness.d.ts +40 -0
  129. package/dist/release-notes/release-note-faithfulness.d.ts.map +1 -0
  130. package/dist/release-notes/release-note-faithfulness.js +107 -0
  131. package/dist/release-notes/release-note-faithfulness.js.map +1 -0
  132. package/dist/release-notes/release-note-pass.d.ts +77 -0
  133. package/dist/release-notes/release-note-pass.d.ts.map +1 -0
  134. package/dist/release-notes/release-note-pass.js +201 -0
  135. package/dist/release-notes/release-note-pass.js.map +1 -0
  136. package/dist/shell/unified-shell.d.ts.map +1 -1
  137. package/dist/shell/unified-shell.js +11 -5
  138. package/dist/shell/unified-shell.js.map +1 -1
  139. package/dist/testing/fixtures/api-client.js +1 -1
  140. package/dist/testing/fixtures/approved-head-inert-deps.d.ts +4 -3
  141. package/dist/testing/fixtures/approved-head-inert-deps.d.ts.map +1 -1
  142. package/dist/testing/fixtures/approved-head-inert-deps.js +3 -0
  143. package/dist/testing/fixtures/approved-head-inert-deps.js.map +1 -1
  144. package/dist/testing/fixtures/completion-judge-stubs.d.ts.map +1 -1
  145. package/dist/testing/fixtures/completion-judge-stubs.js +2 -0
  146. package/dist/testing/fixtures/completion-judge-stubs.js.map +1 -1
  147. package/dist/testing/fixtures/index.js +1 -1
  148. package/dist/testing/fixtures/pending-spawn-world.d.ts.map +1 -1
  149. package/dist/testing/fixtures/pending-spawn-world.js +1 -0
  150. package/dist/testing/fixtures/pending-spawn-world.js.map +1 -1
  151. package/dist/tower/tower-mode.d.ts +17 -5
  152. package/dist/tower/tower-mode.d.ts.map +1 -1
  153. package/dist/tower/tower-mode.js +27 -3
  154. package/dist/tower/tower-mode.js.map +1 -1
  155. package/dist/types/index.js +1 -1
  156. package/dist/worktree/capture-head-adoption.d.ts +44 -0
  157. package/dist/worktree/capture-head-adoption.d.ts.map +1 -0
  158. package/dist/worktree/capture-head-adoption.js +96 -0
  159. package/dist/worktree/capture-head-adoption.js.map +1 -0
  160. package/dist/worktree/safety.d.ts +12 -3
  161. package/dist/worktree/safety.d.ts.map +1 -1
  162. package/dist/worktree/safety.js +15 -3
  163. package/dist/worktree/safety.js.map +1 -1
  164. package/docs/runbook-tower.md +237 -0
  165. package/package.json +8 -5
@@ -0,0 +1,44 @@
1
+ /** The focus branch whose head a review would judge, and the repo its ref lives in (worktrees share it). */
2
+ export interface CaptureHeadTarget {
3
+ repoPath: string;
4
+ branchName: string | null;
5
+ }
6
+ /** What the adoption found at the focus head, and did. */
7
+ export type CaptureHeadAdoption =
8
+ /** The head is not a daemon capture: nothing to adopt, review may judge it. */
9
+ {
10
+ kind: 'not_capture';
11
+ headSha: string;
12
+ }
13
+ /** The head was a capture; `adoptedSha` (the new head) commits it as the team's own. */
14
+ | {
15
+ kind: 'adopted';
16
+ captureSha: string;
17
+ adoptedSha: string;
18
+ trailerValue: string;
19
+ }
20
+ /** The head is a capture and could not be adopted (or the branch moved meanwhile); retried next read. */
21
+ | {
22
+ kind: 'failed';
23
+ captureSha: string;
24
+ reason: string;
25
+ }
26
+ /** The head could not be read. */
27
+ | {
28
+ kind: 'unreadable';
29
+ reason: string;
30
+ };
31
+ /** Subject prefix of an adoption commit -- deliberately not a capture subject. */
32
+ export declare const CAPTURE_ADOPTION_SUBJECT_PREFIX = "chore: adopt captured agent work as the team's own commit (";
33
+ /** The adoption commit's message, as commit-tree `-m` paragraphs (the last one is the trailer block). */
34
+ export declare function captureAdoptionMessage(capture: {
35
+ sha: string;
36
+ subject: string;
37
+ }, trailerValue: string): string[];
38
+ /**
39
+ * Adopt the focus head when it is a daemon capture (module doc). Never throws:
40
+ * an unreadable head, a failed commit or a branch that moved meanwhile is
41
+ * reported, and the next read retries against whatever the head is then.
42
+ */
43
+ export declare function adoptCaptureHead(target: CaptureHeadTarget): CaptureHeadAdoption;
44
+ //# sourceMappingURL=capture-head-adoption.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capture-head-adoption.d.ts","sourceRoot":"","sources":["../../src/worktree/capture-head-adoption.ts"],"names":[],"mappings":"AAwCA,4GAA4G;AAC5G,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,0DAA0D;AAC1D,MAAM,MAAM,mBAAmB;AAC7B,+EAA+E;AAC7E;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE;AAC1C,wFAAwF;GACtF;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE;AACnF,yGAAyG;GACvG;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE;AACxD,kCAAkC;GAChC;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3C,kFAAkF;AAClF,eAAO,MAAM,+BAA+B,gEAAiE,CAAC;AAmB9G,yGAAyG;AACzG,wBAAgB,sBAAsB,CAAC,OAAO,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,EAAE,CAShH;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,iBAAiB,GAAG,mBAAmB,CAsB/E"}
@@ -0,0 +1,96 @@
1
+ // PINNED-BY-BEGIN (generated by gen:pinned-by -- do not hand-edit; checked by lint:pinned-by)
2
+ // Pinned by: packages/daemon/src/completion/state-cascade/review-capture-head-gate.test.ts,
3
+ // packages/daemon/src/focus/pending-review-capture-head.test.ts,
4
+ // packages/daemon/src/integration/capture-head-review-regression.test.ts,
5
+ // packages/daemon/src/integration/commits-after-approval-review-regression.test.ts,
6
+ // packages/daemon/src/integration/fully-merged-focus-sibling-red-scenario.test.ts,
7
+ // packages/daemon/src/worktree/capture-head-adoption.test.ts
8
+ // PINNED-BY-END
9
+ /**
10
+ * A focus review never judges a daemon WIP capture: before a review window opens
11
+ * or a review spawns, a focus head that is a daemon capture of uncommitted agent
12
+ * work is ADOPTED as the team's own commit, and the review judges that commit.
13
+ *
14
+ * Why: the completion merge gate never lets a capture anchor its own approval
15
+ * (merge/merge-gate-coverage.ts anchorGates), and review runs once per focus head
16
+ * (focus/pending-review-approved-head.ts), so a review that approved a capture
17
+ * head left a finished focus held off integration with no step that advances it.
18
+ * The fix is at the source: the head a review judges is never a capture.
19
+ * History: docs/daemon/history-platform.md (worktree/capture-head-adoption.ts).
20
+ *
21
+ * The adoption is the daemon committing on the team's behalf, append-only: a new
22
+ * commit on top of the capture with the capture's tree (it changes no file), a
23
+ * subject that is not a capture subject, and the capture's Telora-Delivery
24
+ * trailer -- its delivery's id, or `none` when the capture names no delivery (the
25
+ * adoption itself carries no work, and an `unattributed` stamp would be named as
26
+ * misattributed work). History is never rewritten: verdicts, attribution and
27
+ * holds are keyed on commit shas. The branch moves by a compare-and-swap ref
28
+ * update, so a commit that landed meanwhile is never overwritten. Plumbing
29
+ * (commit-tree + update-ref) runs no hooks and touches no worktree: a worktree on
30
+ * the branch follows it to a commit with the same tree, so its index and files
31
+ * stay exactly as they were.
32
+ *
33
+ * @module worktree/capture-head-adoption
34
+ */
35
+ import { runGitSync } from '../git/types.js';
36
+ import { classifyDeliveryTrailer, DELIVERY_TRAILER_KEY, NON_DELIVERY_SENTINEL } from '../git/delivery-trailer.js';
37
+ import { isDaemonWipCapture } from './safety.js';
38
+ /** Subject prefix of an adoption commit -- deliberately not a capture subject. */
39
+ export const CAPTURE_ADOPTION_SUBJECT_PREFIX = 'chore: adopt captured agent work as the team\'s own commit (';
40
+ const SHA_RE = /^[0-9a-f]{40,64}$/i;
41
+ const FIELD_SEP = '\x1f';
42
+ /** The head commit of `ref`, or why it could not be read. */
43
+ function readHead(repoPath, ref) {
44
+ const result = runGitSync(['log', '-1', `--format=%H${FIELD_SEP}%s`, ref, '--'], repoPath);
45
+ if (!result.success)
46
+ return { reason: `git log ${ref} failed: ${result.error || result.output}` };
47
+ const [sha = '', ...subject] = result.output.trim().split(FIELD_SEP);
48
+ return SHA_RE.test(sha) ? { sha, subject: subject.join(FIELD_SEP) } : { reason: `${ref} has no readable head` };
49
+ }
50
+ /** The trailer an adoption carries: the capture's delivery, else the non-delivery sentinel (module doc). */
51
+ function adoptionTrailerValue(repoPath, captureSha) {
52
+ const attribution = classifyDeliveryTrailer(repoPath, captureSha);
53
+ return attribution.kind === 'delivery' ? attribution.deliveryId : NON_DELIVERY_SENTINEL;
54
+ }
55
+ /** The adoption commit's message, as commit-tree `-m` paragraphs (the last one is the trailer block). */
56
+ export function captureAdoptionMessage(capture, trailerValue) {
57
+ return [
58
+ `${CAPTURE_ADOPTION_SUBJECT_PREFIX}${capture.sha.slice(0, 12)})`,
59
+ `The daemon captured this focus's uncommitted agent work as ${capture.sha.slice(0, 12)} ` +
60
+ `("${capture.subject}"). A focus review never judges a daemon capture: the completion merge gate ` +
61
+ `never lets a capture anchor its own approval. This commit adopts that work as the team's own, so ` +
62
+ `the review judges a commit the merge gate accepts. It changes no files.`,
63
+ `${DELIVERY_TRAILER_KEY}: ${trailerValue}`,
64
+ ];
65
+ }
66
+ /**
67
+ * Adopt the focus head when it is a daemon capture (module doc). Never throws:
68
+ * an unreadable head, a failed commit or a branch that moved meanwhile is
69
+ * reported, and the next read retries against whatever the head is then.
70
+ */
71
+ export function adoptCaptureHead(target) {
72
+ if (!target.branchName)
73
+ return { kind: 'unreadable', reason: 'the focus has no branch' };
74
+ const ref = `refs/heads/${target.branchName}`;
75
+ const head = readHead(target.repoPath, ref);
76
+ if ('reason' in head)
77
+ return { kind: 'unreadable', reason: head.reason };
78
+ if (!isDaemonWipCapture(head))
79
+ return { kind: 'not_capture', headSha: head.sha };
80
+ const trailerValue = adoptionTrailerValue(target.repoPath, head.sha);
81
+ const paragraphs = captureAdoptionMessage(head, trailerValue).flatMap((p) => ['-m', p]);
82
+ const commit = runGitSync(['commit-tree', `${head.sha}^{tree}`, '-p', head.sha, ...paragraphs], target.repoPath);
83
+ const adoptedSha = commit.output.trim();
84
+ if (!commit.success || !SHA_RE.test(adoptedSha)) {
85
+ return { kind: 'failed', captureSha: head.sha, reason: `git commit-tree failed: ${commit.error || commit.output}` };
86
+ }
87
+ // Compare-and-swap: moves the branch only if its head is still the capture. The
88
+ // reflog step reads as a commit, so git/focus-own-delta.ts counts it as the focus's own.
89
+ const reflog = `commit: adopt daemon capture ${head.sha.slice(0, 12)} as the team's own (telora-daemon)`;
90
+ const moved = runGitSync(['update-ref', '-m', reflog, ref, adoptedSha, head.sha], target.repoPath);
91
+ if (!moved.success) {
92
+ return { kind: 'failed', captureSha: head.sha, reason: `${ref} moved or could not be updated: ${moved.error || moved.output}` };
93
+ }
94
+ return { kind: 'adopted', captureSha: head.sha, adoptedSha, trailerValue };
95
+ }
96
+ //# sourceMappingURL=capture-head-adoption.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capture-head-adoption.js","sourceRoot":"","sources":["../../src/worktree/capture-head-adoption.ts"],"names":[],"mappings":"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"}
@@ -29,12 +29,21 @@ export declare function branchHasUnmergedCommits(branchName: string, targetBranc
29
29
  /**
30
30
  * Subject prefixes of the two commits the daemon writes to capture an agent's
31
31
  * uncommitted work: commitWipChanges (team exit) and safetyCommitWorktree
32
- * (worktree removal, terminated-team reconcile). The merge gate recognizes a
33
- * daemon capture by these (merge/merge-gate-coverage.ts), so the writers build
34
- * their subjects from the same constants.
32
+ * (worktree removal, terminated-team reconcile). A daemon capture is recognized
33
+ * by these (isDaemonWipCapture), so the writers build their subjects from the
34
+ * same constants.
35
35
  */
36
36
  export declare const WIP_CAPTURE_SUBJECT_PREFIX = "WIP: Uncommitted work captured by daemon on agent exit (";
37
37
  export declare const SAFETY_COMMIT_SUBJECT_PREFIX = "wip: uncommitted agent work for \"";
38
+ /**
39
+ * True for a commit the daemon wrote to capture an agent's uncommitted work (a
40
+ * commitWipChanges or safetyCommitWorktree subject). The ONE recognizer: the merge
41
+ * gate's capture rule (merge/merge-gate-coverage.ts) and the review's capture-head
42
+ * adoption (worktree/capture-head-adoption.ts) both read it. Pure.
43
+ */
44
+ export declare function isDaemonWipCapture(commit: {
45
+ readonly subject: string;
46
+ }): boolean;
38
47
  /** The subject commitWipChanges writes for `contextName`. */
39
48
  export declare function wipCaptureSubject(contextName: string): string;
40
49
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"safety.d.ts","sourceRoot":"","sources":["../../src/worktree/safety.ts"],"names":[],"mappings":"AA0BA,OAAO,EAIL,KAAK,sBAAsB,EAC5B,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EAEL,KAAK,uBAAuB,EAC5B,KAAK,0BAA0B,EAChC,MAAM,+BAA+B,CAAC;AACvC,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,kCAAkC,CAAC;AAEtF;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,gBAAgB,EAAE,sBAAsB,CAAC;IAClD,QAAQ,CAAC,kBAAkB,EAAE,6BAA6B,CAAC;IAC3D,QAAQ,CAAC,eAAe,EAAE,IAAI,CAAC,0BAA0B,EAAE,gBAAgB,CAAC,CAAC;CAC9E;AAMD;;;;;;;;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;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,6DAA6D,CAAC;AACrG,eAAO,MAAM,4BAA4B,uCAAsC,CAAC;AAEhF,6DAA6D;AAC7D,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,sBAAsB,EACjC,WAAW,CAAC,EAAE,uBAAuB,GACpC,MAAM,GAAG,IAAI,CAaf;AAED;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B,EAAE,SAAS,MAAM,EAMzD,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,qCAAqC,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAOnF;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CACtC,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,sBAAsB,GAChC,MAAM,GAAG,IAAI,CAUf;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE;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,iEAAiE;IACjE,SAAS,EAAE,sBAAsB,CAAC;IAClC,4EAA4E;IAC5E,WAAW,CAAC,EAAE,uBAAuB,CAAC;CACvC,GACA,OAAO,CAoFT;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,gBAAgB,CAC9B,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,sBAAsB,EACjC,WAAW,CAAC,EAAE,uBAAuB,GACpC,MAAM,GAAG,IAAI,CASf"}
1
+ {"version":3,"file":"safety.d.ts","sourceRoot":"","sources":["../../src/worktree/safety.ts"],"names":[],"mappings":"AA6BA,OAAO,EAIL,KAAK,sBAAsB,EAC5B,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EAEL,KAAK,uBAAuB,EAC5B,KAAK,0BAA0B,EAChC,MAAM,+BAA+B,CAAC;AACvC,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,kCAAkC,CAAC;AAEtF;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,gBAAgB,EAAE,sBAAsB,CAAC;IAClD,QAAQ,CAAC,kBAAkB,EAAE,6BAA6B,CAAC;IAC3D,QAAQ,CAAC,eAAe,EAAE,IAAI,CAAC,0BAA0B,EAAE,gBAAgB,CAAC,CAAC;CAC9E;AAMD;;;;;;;;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;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,6DAA6D,CAAC;AACrG,eAAO,MAAM,4BAA4B,uCAAsC,CAAC;AAEhF;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE;IAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAEhF;AAED,6DAA6D;AAC7D,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,sBAAsB,EACjC,WAAW,CAAC,EAAE,uBAAuB,GACpC,MAAM,GAAG,IAAI,CAaf;AAED;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B,EAAE,SAAS,MAAM,EAMzD,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,qCAAqC,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAOnF;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CACtC,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,sBAAsB,GAChC,MAAM,GAAG,IAAI,CAUf;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE;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,iEAAiE;IACjE,SAAS,EAAE,sBAAsB,CAAC;IAClC,4EAA4E;IAC5E,WAAW,CAAC,EAAE,uBAAuB,CAAC;CACvC,GACA,OAAO,CAoFT;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,gBAAgB,CAC9B,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,sBAAsB,EACjC,WAAW,CAAC,EAAE,uBAAuB,GACpC,MAAM,GAAG,IAAI,CASf"}
@@ -1,13 +1,16 @@
1
1
  // PINNED-BY-BEGIN (generated by gen:pinned-by -- do not hand-edit; checked by lint:pinned-by)
2
2
  // Pinned by: packages/daemon/src/completion/team-exit/teardown-merge-gate.test.ts,
3
3
  // packages/daemon/src/completion/team-teardown/mid-flight-merge-gate.test.ts,
4
+ // packages/daemon/src/focus/pending-review-capture-head.test.ts,
4
5
  // packages/daemon/src/integration/approved-head-hold-release-completion-regression.test.ts,
6
+ // packages/daemon/src/integration/capture-head-review-regression.test.ts,
5
7
  // packages/daemon/src/merge/attributed-capture-merge-gate.test.ts,
6
8
  // packages/daemon/src/merge/completion-merge-gate.test.ts,
7
9
  // packages/daemon/src/merge/merge-gate-coverage.test.ts,
8
10
  // packages/daemon/src/merge/safe-remove-merge-gate.test.ts,
9
11
  // packages/daemon/src/verify/attribution/exit-capture-attribution-regression.test.ts,
10
12
  // packages/daemon/src/worktree/capture-attribution-commit.test.ts,
13
+ // packages/daemon/src/worktree/capture-head-adoption.test.ts,
11
14
  // packages/daemon/src/worktree/safety-commit-guard.test.ts,
12
15
  // packages/daemon/src/worktree/safety-commit-under-guard.test.ts,
13
16
  // packages/daemon/src/worktree/unmerged-capture-refusal.test.ts,
@@ -60,12 +63,21 @@ export function branchHasUnmergedCommits(branchName, targetBranch, repoPath) {
60
63
  /**
61
64
  * Subject prefixes of the two commits the daemon writes to capture an agent's
62
65
  * uncommitted work: commitWipChanges (team exit) and safetyCommitWorktree
63
- * (worktree removal, terminated-team reconcile). The merge gate recognizes a
64
- * daemon capture by these (merge/merge-gate-coverage.ts), so the writers build
65
- * their subjects from the same constants.
66
+ * (worktree removal, terminated-team reconcile). A daemon capture is recognized
67
+ * by these (isDaemonWipCapture), so the writers build their subjects from the
68
+ * same constants.
66
69
  */
67
70
  export const WIP_CAPTURE_SUBJECT_PREFIX = 'WIP: Uncommitted work captured by daemon on agent exit (';
68
71
  export const SAFETY_COMMIT_SUBJECT_PREFIX = 'wip: uncommitted agent work for "';
72
+ /**
73
+ * True for a commit the daemon wrote to capture an agent's uncommitted work (a
74
+ * commitWipChanges or safetyCommitWorktree subject). The ONE recognizer: the merge
75
+ * gate's capture rule (merge/merge-gate-coverage.ts) and the review's capture-head
76
+ * adoption (worktree/capture-head-adoption.ts) both read it. Pure.
77
+ */
78
+ export function isDaemonWipCapture(commit) {
79
+ return commit.subject.startsWith(WIP_CAPTURE_SUBJECT_PREFIX) || commit.subject.startsWith(SAFETY_COMMIT_SUBJECT_PREFIX);
80
+ }
69
81
  /** The subject commitWipChanges writes for `contextName`. */
70
82
  export function wipCaptureSubject(contextName) {
71
83
  return `${WIP_CAPTURE_SUBJECT_PREFIX}${contextName})`;
@@ -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,237 @@
1
+ # Runbook: the product tower
2
+
3
+ The tower is traffic control for a product's armed foci. It changes what
4
+ ARMED means -- from "run now" to "run when cleared" -- and it never arms or
5
+ un-arms anything: arming stays the human go-signal. Decision record:
6
+ `docs/DECISIONS.md` ("The tower: traffic control for armed foci lives at the
7
+ product").
8
+
9
+ With the tower in `enforce`, an operator can arm several foci of a product,
10
+ set the product's limit to 1, and walk away: they run one at a time, in focus
11
+ rank order.
12
+
13
+ ## The pieces
14
+
15
+ | Piece | Where | What it is |
16
+ |---|---|---|
17
+ | The limit | `products.tower_concurrency_limit` | How many of the product's foci may hold a live coding or review team at once. `NULL` = no product limit: the host session cap alone bounds it. Set with `telora_product` `update` `towerConcurrencyLimit` (`null` clears it). |
18
+ | The verdict | `product_focuses.tower_verdict` | The tower's verdict on one armed, active focus: `cleared`, or `holding` with exactly one reason and since-when. |
19
+ | The pass | `packages/daemon/src/tower/tower-pass.ts` | One pass per configured product; the SOLE writer of verdicts. |
20
+ | Admission | `packages/daemon/src/tower/tower-admission.ts` | What the spawn passes ask before they start a coding or review team. |
21
+ | The graph | `focus_tower_edges` | Declared `depends_on` / `excludes` edges between two foci of the product, with provenance and evidence. Written over MCP (`telora_product_tower`); shown on the product page's Focus tab. |
22
+ | Landed | `packages/daemon/src/tower/focus-landed.ts` | The ONE definition of "this focus's work has landed", shared with the PRD frontier. |
23
+ | The vocabulary | `@telora/daemon-core` `tower.ts` | Modes, verdict states, reasons (in precedence order), edge kinds and provenance, and the fail-open freshness rule -- one definition for the daemon and the dashboard. |
24
+
25
+ ## The verdict
26
+
27
+ ```
28
+ { state: 'cleared' | 'holding',
29
+ reason: null | 'product_limit_reached' | 'waits_on' | 'excluded_by',
30
+ since: ISO, -- when this state + reason began (kept across re-confirmations)
31
+ computedAt: ISO, -- when the pass last confirmed it
32
+ armedAt: ISO|null, -- the arm (agent_assigned_at) it was computed under
33
+ mode: 'observe' | 'enforce',
34
+ detail: { limit?, occupied?, focusId?, focusName?, evidence? } }
35
+ ```
36
+
37
+ The reasons, and what the dashboard says for each:
38
+
39
+ | Reason | Dashboard | Means |
40
+ |---|---|---|
41
+ | `product_limit_reached` | Held by tower: product limit reached (N of L running) | The product's free slots went to foci ahead of this one. |
42
+ | `waits_on` | Held by tower: waits on <focus> | A `depends_on` predecessor has not landed (tower graph). |
43
+ | `excluded_by` | Held by tower: excluded by <focus> on <evidence> | An `excludes` edge joins it to a focus that is running or cleared ahead of it (tower graph). |
44
+
45
+ ## The graph
46
+
47
+ Edges join two foci of ONE product (an edge across products, or from a focus to
48
+ itself, is refused):
49
+
50
+ - **`depends_on`** (directed): `toFocusId` waits until `fromFocusId` has
51
+ LANDED. A `depends_on` edge that would close a cycle is refused at the write,
52
+ and the refusal names the cycle (`UI -> Schema -> API -> UI`). The refusal is
53
+ atomic: a database trigger serializes each product's `depends_on` writes, so
54
+ two concurrent writes can never close a cycle between them.
55
+ - **`excludes`** (mutual): the two foci are never cleared together -- e.g. both
56
+ rewrite the same files, fixtures or migration journal. Stored once per pair.
57
+
58
+ Every edge carries **provenance** (`declared` -- written by a human or an agent
59
+ over MCP; `inferred` and `observed` are reserved for the follow-on focus that
60
+ draws edges itself), **evidence** (why the edge holds -- required), and who
61
+ wrote it and when. Deleting an edge leaves a tombstone (`deleted_at`).
62
+
63
+ ```
64
+ telora_product_tower { action: "edge_create", productId, fromFocusId, toFocusId,
65
+ kind: "depends_on" | "excludes", evidence }
66
+ telora_product_tower { action: "edge_list", productId, focusId? }
67
+ telora_product_tower { action: "edge_delete", towerEdgeId }
68
+ ```
69
+
70
+ ## The landed rule
71
+
72
+ A focus has LANDED -- judged by the daemon, no human close -- when:
73
+
74
+ 1. it has at least one delivery, and
75
+ 2. every non-cancelled delivery is `done` (at least one: a focus whose every
76
+ delivery was cancelled landed nothing), and
77
+ 3. its branch tip is contained in the integration branch
78
+ (`git rev-list --count <integration>..<branch>` reads 0; no checkout) -- or
79
+ the branch is gone AND the focus's merge record (`focus_merges`, written by
80
+ the daemon when it merges) names a merge commit integration contains. The
81
+ daemon removes a focus branch only after merging it; the record is the proof.
82
+ A branch that is gone with no such record (a hand-deleted branch, a legacy
83
+ focus that never recorded one) is unproven: unknown, never landed.
84
+
85
+ The PRD frontier's completion signal is this same rule (`prd/controller-shared.ts`
86
+ `computeCompletedFocusIds`): a frontier overcome clears only when its focus has
87
+ landed. A landed check git cannot answer (integration unreadable, a ref or
88
+ containment read that errored, an unreadable or missing merge record) is UNKNOWN: the tower leaves the dependent focus's verdict unavailable (fail
89
+ open) rather than holding it, and the PRD frontier waits a tick.
90
+
91
+ ## Precedence
92
+
93
+ A verdict records exactly one reason. When several apply, the first in this
94
+ order wins (daemon-core `TOWER_HOLD_REASONS`):
95
+
96
+ 1. `waits_on` -- a fact about this focus's prerequisites;
97
+ 2. `excluded_by` -- a fact about this focus's collisions;
98
+ 3. `product_limit_reached` -- a fact about the queue.
99
+
100
+ A focus held for a graph reason takes no slot. `excluded_by` holds the focus
101
+ reached second, in rank order, while the other runs or is cleared, so two
102
+ excluded foci are never cleared together.
103
+
104
+ ## The slot rule
105
+
106
+ A focus occupies one of the product's slots **only while it has a live coding
107
+ or review team**. An armed focus with no live team -- waiting on CI, a verify
108
+ hold, an escalation -- occupies nothing, so one stalled focus never holds the
109
+ product's queue.
110
+
111
+ Each pass, per product:
112
+
113
+ 1. Every focus with a live team is cleared (the tower never stops a running team).
114
+ 2. Free slots = limit - live foci (unbounded with no limit).
115
+ 3. The other armed foci are walked in **focus rank order** (lowest
116
+ `priority_rank` first) -- foci the spawn passes last found wanting a team
117
+ ahead of foci they found idle. A focus that waits on an unlanded
118
+ predecessor, or is excluded by a focus already running or cleared, holds
119
+ for that reason; the rest each take a free slot (cleared) until none is
120
+ left, and the remainder hold `product_limit_reached`.
121
+
122
+ "Wanting" is CURRENT demand: a focus wants a slot when it could run now if the
123
+ tower and the host cap let it. Before the tower allocates, every ready-focus
124
+ pass refreshes stale agent roles (as the spawn pass does) and probes each
125
+ surfaced focus -- ready, or holding a queued directive -- through the gates its
126
+ spawn pass applies, in the pass's order, bar the tower and the host cap
127
+ (`packages/daemon/src/focus/spawn-demand-probe.ts`): its role, a live team, the
128
+ review budget, an open parking escalation (the guard `spawnFocusTeam` applies),
129
+ a demoted just-torn-down respawn, a queued review whose window cannot be read
130
+ (the ready pass withholds it), the operator-hold park, a review at an approved
131
+ head, a verify hold, a cooldown, no team work, and -- for a queued directive --
132
+ an active row the pass could not read (it cannot spawn without one). So a focus held by any of
133
+ those reserves no slot, even the first time it surfaces; a focus whose gate
134
+ clears is ranked with the waiters on that same pass; a focus that spawned has
135
+ had its demand met; and a focus no longer surfaced wants nothing -- when a team
136
+ exits, its focus reserves nothing and the slot goes to the next wanting focus on
137
+ the same pass. The gates that dispose of, park or release a queued directive (a
138
+ stale stage, a closed review window, a superseded coding directive, the
139
+ operator-hold park and its release, a review at an approved head) run for real
140
+ before allocation: they are idempotent and the pending pass would make the same
141
+ disposal on that poll. A probe that cannot answer keeps a fresh record and
142
+ otherwise fails open (the focus wants a slot), so a read error never demotes a
143
+ higher-ranked focus behind the waiters.
144
+
145
+ The pass runs at the head of every ready-focus pass -- on daemon start, on
146
+ every 30s poll, on the engine's 60s timer and on a completion trigger; there is
147
+ no production path to the ready pass that skips it, and the passes never overlap
148
+ (`runReadyFocusPassThroughTower`, pinned by `tower/tower-entrypoints.test.ts`) -- so a change to the armed set, the limit, focus rank or a
149
+ focus's team presence is reflected within one poll. It writes only a verdict
150
+ that changed, or one older than half the freshness bound (5 minutes), so a
151
+ running tower's verdicts never go stale. It also clears the verdict of a focus
152
+ that is no longer armed. A pass that throws for one product is logged and never
153
+ stops another product's pass; admission fails open on that product's foci at
154
+ once (see the fail-open rule).
155
+
156
+ ## The three modes: `TELORA_TOWER_MODE`
157
+
158
+ Declared in `@telora/daemon-core` `env-config.ts`; default **`observe`**.
159
+
160
+ | Mode | Tower pass | Spawn passes |
161
+ |---|---|---|
162
+ | `off` | Does not run. | Exactly as before the tower: API order, host cap only. |
163
+ | `observe` | Writes verdicts; the dashboard shows them, marked "(observe only)". | Ignore verdicts. Every spawn decision `enforce` would have made differently is logged: `[tower] observe: enforce would hold "<focus>" (<reason> since <t>); decision taken: spawned`. |
164
+ | `enforce` | Writes verdicts. | Start no coding or review team for a focus whose verdict is holding AND fresh; offer ready foci, and foci with a queued (e.g. review) directive, to the host in focus rank order. |
165
+
166
+ ## The fail-open rule
167
+
168
+ In every mode, only a fresh, explicit holding verdict ever stops a spawn, and
169
+ only while this daemon's tower pass for the focus's product is STANDING: its
170
+ most recent pass for the product succeeded, within the freshness bound. When the
171
+ product's pass fails -- or has not yet succeeded since the daemon started --
172
+ every verdict of that product is unobeyed AT ONCE, however fresh, and the daemon
173
+ logs `[tower] "<focus>": its product's tower pass failed -- failing open`.
174
+ Another product's holds are untouched. A verdict that is
175
+
176
+ - missing,
177
+ - unreadable,
178
+ - older than the bounded age (`TOWER_VERDICT_MAX_AGE_MS`, 5 minutes), or
179
+ - computed under an earlier arm (its `armedAt` is not the focus's current `agent_assigned_at`)
180
+
181
+ is NO verdict: the focus spawns exactly as it would with the tower off, the
182
+ daemon logs `[tower] "<focus>": tower verdict unavailable (<why>) -- failing
183
+ open`, and the dashboard names it ("Tower verdict unavailable (stale) --
184
+ spawns as with the tower off"). A tower that stops writing therefore releases
185
+ every hold within five minutes rather than freezing the product.
186
+
187
+ The dashboard judges freshness on a clock while the page is open, so a hold
188
+ turns into "unavailable" on screen when the spawn pass stops obeying it. Whether
189
+ the product's tower is running at all comes from the daemon heartbeat
190
+ (`metadata.tower` = `{ [productId]: { mode, pass } }`): with the tower running,
191
+ an armed focus with no verdict reads "unavailable" even when no focus has one,
192
+ and a failed pass marks every armed focus of the product "unavailable (the
193
+ product's tower pass failed)"; with `TELORA_TOWER_MODE=off` no tower line shows.
194
+
195
+ Why admission fails OPEN while the merge gates fail CLOSED: the failures are not
196
+ symmetric. A wrongly blocked merge is visible -- a held delivery, a red gate, a
197
+ named reason. A wrongly held spawn is silent -- a focus that simply never runs,
198
+ which is the daemon's recurring bug class. The tower is in front of every
199
+ spawn, so it must never be able to cause that silently.
200
+
201
+ ## Moving a host from observe to enforce
202
+
203
+ 1. Leave the host in `observe` (the default) and set the product's limit:
204
+ `telora_product update { productId, towerConcurrencyLimit: 1 }`.
205
+ 2. Arm the foci. On the dashboard each armed focus now carries the tower's
206
+ line; on the host, read the divergences:
207
+ `telora-daemon logs | grep '\[tower\]'`.
208
+ 3. Judge them on real foci: every `observe: enforce would hold` line should be
209
+ a spawn you would have wanted held, and no focus should show "tower verdict
210
+ unavailable" while the daemon is healthy.
211
+ 4. Set `TELORA_TOWER_MODE=enforce` in the daemon's environment and restart it
212
+ (drain first if teams are running). Holds now take effect on the next poll.
213
+ 5. To back out: `TELORA_TOWER_MODE=observe` (verdicts kept, not obeyed) or
214
+ `off` (no pass at all) and restart. Clearing the product limit
215
+ (`towerConcurrencyLimit: null`) releases every `product_limit_reached`
216
+ hold on the next poll without a restart.
217
+
218
+ ## Reading a stuck focus
219
+
220
+ - **"Held by tower: product limit reached"** -- look at which foci are running
221
+ (they hold the slots) and their rank; raise the limit or re-rank if the
222
+ order is wrong.
223
+ - **"Held by tower: waits on <focus>"** -- the predecessor has not landed: it
224
+ still has open deliveries, or its branch is not yet merged into integration.
225
+ If the dependency is wrong, delete the edge (`edge_delete`).
226
+ - **"Held by tower: excluded by <focus> on <evidence>"** -- the other focus is
227
+ running (or was cleared ahead of this one); this one clears when it stops.
228
+ - **"Tower verdict unavailable"** -- the focus will spawn anyway. Check the
229
+ daemon log for `[tower] pass failed for product ...`.
230
+ - **"Waiting for a session slot"** -- the tower has cleared it; the HOST cap is
231
+ full. That is the session cap, not the tower.
232
+
233
+ ## Stated limit
234
+
235
+ A product served by two daemon hosts would get two towers writing the same
236
+ verdicts, each counting only its own live teams. No product is configured that
237
+ way.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telora/daemon",
3
- "version": "0.24.2",
3
+ "version": "0.24.7",
4
4
  "description": "Agent orchestration daemon for Telora - spawns and manages Claude Code instances",
5
5
  "type": "module",
6
6
  "bin": {
@@ -20,7 +20,8 @@
20
20
  "scripts/telora-daemon-wrapper.sh",
21
21
  "build-info.json",
22
22
  "README.md",
23
- ".env.example"
23
+ ".env.example",
24
+ "docs/runbook-tower.md"
24
25
  ],
25
26
  "scripts": {
26
27
  "dev": "tsx watch src/index.ts",
@@ -33,12 +34,14 @@
33
34
  "test": "node scripts/run-tests.mjs",
34
35
  "stamp-build-info": "node scripts/stamp-build-info.mjs",
35
36
  "stamp-build-info:local": "node scripts/stamp-build-info.mjs --empty",
36
- "prepublishOnly": "npm run build && npm run stamp-build-info"
37
+ "copy-shipped-docs": "node scripts/copy-shipped-docs.mjs",
38
+ "prepublishOnly": "npm run build && npm run stamp-build-info",
39
+ "prepack": "npm run copy-shipped-docs"
37
40
  },
38
41
  "//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. Test files live beside the modules they exercise (owner-directory layout); the runner discovers them recursively.",
39
42
  "dependencies": {
40
- "@telora/daemon-core": "^0.3.0",
41
- "@telora/mcp-products": "^0.23.2",
43
+ "@telora/daemon-core": "^0.3.1",
44
+ "@telora/mcp-products": "^0.23.4",
42
45
  "commander": "^14.0.3",
43
46
  "yaml": "^2.4.0",
44
47
  "zod": "^4.3.6"