ctxr-cli 0.32.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (275) hide show
  1. package/README.md +15 -1
  2. package/dist/commands/archive.d.ts +5 -4
  3. package/dist/commands/archive.d.ts.map +1 -1
  4. package/dist/commands/archive.js +7 -7
  5. package/dist/commands/archive.js.map +1 -1
  6. package/dist/commands/catalog-show.d.ts +8 -0
  7. package/dist/commands/catalog-show.d.ts.map +1 -1
  8. package/dist/commands/catalog-show.js +29 -2
  9. package/dist/commands/catalog-show.js.map +1 -1
  10. package/dist/commands/context-gather.d.ts.map +1 -1
  11. package/dist/commands/context-gather.js +3 -2
  12. package/dist/commands/context-gather.js.map +1 -1
  13. package/dist/commands/doctor.d.ts +5 -1
  14. package/dist/commands/doctor.d.ts.map +1 -1
  15. package/dist/commands/doctor.js +4 -0
  16. package/dist/commands/doctor.js.map +1 -1
  17. package/dist/commands/graph-build.d.ts +4 -1
  18. package/dist/commands/graph-build.d.ts.map +1 -1
  19. package/dist/commands/graph-build.js +14 -5
  20. package/dist/commands/graph-build.js.map +1 -1
  21. package/dist/commands/land.d.ts +27 -7
  22. package/dist/commands/land.d.ts.map +1 -1
  23. package/dist/commands/land.js +307 -32
  24. package/dist/commands/land.js.map +1 -1
  25. package/dist/commands/publish-gather.d.ts.map +1 -1
  26. package/dist/commands/publish-gather.js +2 -3
  27. package/dist/commands/publish-gather.js.map +1 -1
  28. package/dist/commands/review-list.d.ts +19 -5
  29. package/dist/commands/review-list.d.ts.map +1 -1
  30. package/dist/commands/review-list.js +55 -6
  31. package/dist/commands/review-list.js.map +1 -1
  32. package/dist/commands/rollup-gather.d.ts.map +1 -1
  33. package/dist/commands/rollup-gather.js +2 -3
  34. package/dist/commands/rollup-gather.js.map +1 -1
  35. package/dist/commands/second-opinion.d.ts +33 -0
  36. package/dist/commands/second-opinion.d.ts.map +1 -0
  37. package/dist/commands/second-opinion.js +56 -0
  38. package/dist/commands/second-opinion.js.map +1 -0
  39. package/dist/commands/serve.d.ts.map +1 -1
  40. package/dist/commands/serve.js +4 -0
  41. package/dist/commands/serve.js.map +1 -1
  42. package/dist/commands/session-discard.d.ts +18 -0
  43. package/dist/commands/session-discard.d.ts.map +1 -0
  44. package/dist/commands/session-discard.js +31 -0
  45. package/dist/commands/session-discard.js.map +1 -0
  46. package/dist/commands/session-list.d.ts +17 -6
  47. package/dist/commands/session-list.d.ts.map +1 -1
  48. package/dist/commands/session-list.js +31 -11
  49. package/dist/commands/session-list.js.map +1 -1
  50. package/dist/commands/session-rebase.d.ts +18 -0
  51. package/dist/commands/session-rebase.d.ts.map +1 -0
  52. package/dist/commands/session-rebase.js +43 -0
  53. package/dist/commands/session-rebase.js.map +1 -0
  54. package/dist/commands/session-start.d.ts +8 -0
  55. package/dist/commands/session-start.d.ts.map +1 -1
  56. package/dist/commands/session-start.js +45 -5
  57. package/dist/commands/session-start.js.map +1 -1
  58. package/dist/commands/session-status.d.ts +17 -0
  59. package/dist/commands/session-status.d.ts.map +1 -0
  60. package/dist/commands/session-status.js +38 -0
  61. package/dist/commands/session-status.js.map +1 -0
  62. package/dist/commands/session-submit.d.ts +23 -0
  63. package/dist/commands/session-submit.d.ts.map +1 -0
  64. package/dist/commands/session-submit.js +42 -0
  65. package/dist/commands/session-submit.js.map +1 -0
  66. package/dist/commands/shared-stores-status.d.ts +24 -0
  67. package/dist/commands/shared-stores-status.d.ts.map +1 -0
  68. package/dist/commands/shared-stores-status.js +51 -0
  69. package/dist/commands/shared-stores-status.js.map +1 -0
  70. package/dist/commands/shared-stores-sync.d.ts +27 -0
  71. package/dist/commands/shared-stores-sync.d.ts.map +1 -0
  72. package/dist/commands/shared-stores-sync.js +115 -0
  73. package/dist/commands/shared-stores-sync.js.map +1 -0
  74. package/dist/commands/verify.d.ts.map +1 -1
  75. package/dist/commands/verify.js +2 -4
  76. package/dist/commands/verify.js.map +1 -1
  77. package/dist/config/schema.d.ts +16 -0
  78. package/dist/config/schema.d.ts.map +1 -1
  79. package/dist/config/schema.js +32 -0
  80. package/dist/config/schema.js.map +1 -1
  81. package/dist/core/agents-doc.d.ts +10 -0
  82. package/dist/core/agents-doc.d.ts.map +1 -1
  83. package/dist/core/agents-doc.js +26 -1
  84. package/dist/core/agents-doc.js.map +1 -1
  85. package/dist/core/approval/decision.d.ts +39 -1
  86. package/dist/core/approval/decision.d.ts.map +1 -1
  87. package/dist/core/approval/decision.js +53 -6
  88. package/dist/core/approval/decision.js.map +1 -1
  89. package/dist/core/approval/session-group.d.ts +156 -0
  90. package/dist/core/approval/session-group.d.ts.map +1 -0
  91. package/dist/core/approval/session-group.js +359 -0
  92. package/dist/core/approval/session-group.js.map +1 -0
  93. package/dist/core/approval/session-marker.d.ts +79 -0
  94. package/dist/core/approval/session-marker.d.ts.map +1 -0
  95. package/dist/core/approval/session-marker.js +146 -0
  96. package/dist/core/approval/session-marker.js.map +1 -0
  97. package/dist/core/approval/store-forges.d.ts +15 -0
  98. package/dist/core/approval/store-forges.d.ts.map +1 -0
  99. package/dist/core/approval/store-forges.js +33 -0
  100. package/dist/core/approval/store-forges.js.map +1 -0
  101. package/dist/core/browse/approve.d.ts +8 -0
  102. package/dist/core/browse/approve.d.ts.map +1 -1
  103. package/dist/core/browse/approve.js +366 -48
  104. package/dist/core/browse/approve.js.map +1 -1
  105. package/dist/core/browse/link-resolver.d.ts +9 -4
  106. package/dist/core/browse/link-resolver.d.ts.map +1 -1
  107. package/dist/core/browse/link-resolver.js +12 -7
  108. package/dist/core/browse/link-resolver.js.map +1 -1
  109. package/dist/core/browse/review.d.ts +5 -0
  110. package/dist/core/browse/review.d.ts.map +1 -1
  111. package/dist/core/browse/review.js +8 -0
  112. package/dist/core/browse/review.js.map +1 -1
  113. package/dist/core/checks/integrity-checks.d.ts.map +1 -1
  114. package/dist/core/checks/integrity-checks.js +7 -1
  115. package/dist/core/checks/integrity-checks.js.map +1 -1
  116. package/dist/core/checks/manifest.d.ts.map +1 -1
  117. package/dist/core/checks/manifest.js +2 -0
  118. package/dist/core/checks/manifest.js.map +1 -1
  119. package/dist/core/checks/shared-stores-checks.d.ts +17 -0
  120. package/dist/core/checks/shared-stores-checks.d.ts.map +1 -0
  121. package/dist/core/checks/shared-stores-checks.js +84 -0
  122. package/dist/core/checks/shared-stores-checks.js.map +1 -0
  123. package/dist/core/errors.d.ts +17 -0
  124. package/dist/core/errors.d.ts.map +1 -1
  125. package/dist/core/errors.js +21 -0
  126. package/dist/core/errors.js.map +1 -1
  127. package/dist/core/forge/github.d.ts +14 -0
  128. package/dist/core/forge/github.d.ts.map +1 -1
  129. package/dist/core/forge/github.js +17 -0
  130. package/dist/core/forge/github.js.map +1 -1
  131. package/dist/core/git/fast-forward.d.ts +23 -0
  132. package/dist/core/git/fast-forward.d.ts.map +1 -0
  133. package/dist/core/git/fast-forward.js +38 -0
  134. package/dist/core/git/fast-forward.js.map +1 -0
  135. package/dist/core/git/repo.d.ts +11 -0
  136. package/dist/core/git/repo.d.ts.map +1 -1
  137. package/dist/core/git/repo.js +25 -0
  138. package/dist/core/git/repo.js.map +1 -1
  139. package/dist/core/graph/model.d.ts +27 -0
  140. package/dist/core/graph/model.d.ts.map +1 -1
  141. package/dist/core/graph/model.js +68 -2
  142. package/dist/core/graph/model.js.map +1 -1
  143. package/dist/core/graph/persist.d.ts.map +1 -1
  144. package/dist/core/graph/persist.js +3 -2
  145. package/dist/core/graph/persist.js.map +1 -1
  146. package/dist/core/mcp/sessions.d.ts +11 -14
  147. package/dist/core/mcp/sessions.d.ts.map +1 -1
  148. package/dist/core/mcp/sessions.js +23 -43
  149. package/dist/core/mcp/sessions.js.map +1 -1
  150. package/dist/core/notes/list.d.ts.map +1 -1
  151. package/dist/core/notes/list.js +5 -0
  152. package/dist/core/notes/list.js.map +1 -1
  153. package/dist/core/reconcile.d.ts.map +1 -1
  154. package/dist/core/reconcile.js +2 -1
  155. package/dist/core/reconcile.js.map +1 -1
  156. package/dist/core/retrieval/pass.d.ts.map +1 -1
  157. package/dist/core/retrieval/pass.js +5 -4
  158. package/dist/core/retrieval/pass.js.map +1 -1
  159. package/dist/core/rollup.d.ts.map +1 -1
  160. package/dist/core/rollup.js +2 -3
  161. package/dist/core/rollup.js.map +1 -1
  162. package/dist/core/root.d.ts.map +1 -1
  163. package/dist/core/root.js +33 -2
  164. package/dist/core/root.js.map +1 -1
  165. package/dist/core/second-opinion/environment.d.ts +21 -0
  166. package/dist/core/second-opinion/environment.d.ts.map +1 -0
  167. package/dist/core/second-opinion/environment.js +70 -0
  168. package/dist/core/second-opinion/environment.js.map +1 -0
  169. package/dist/core/second-opinion/judge.d.ts +29 -0
  170. package/dist/core/second-opinion/judge.d.ts.map +1 -0
  171. package/dist/core/second-opinion/judge.js +55 -0
  172. package/dist/core/second-opinion/judge.js.map +1 -0
  173. package/dist/core/second-opinion/launch.d.ts +16 -0
  174. package/dist/core/second-opinion/launch.d.ts.map +1 -0
  175. package/dist/core/second-opinion/launch.js +75 -0
  176. package/dist/core/second-opinion/launch.js.map +1 -0
  177. package/dist/core/second-opinion/lineup.d.ts +82 -0
  178. package/dist/core/second-opinion/lineup.d.ts.map +1 -0
  179. package/dist/core/second-opinion/lineup.js +113 -0
  180. package/dist/core/second-opinion/lineup.js.map +1 -0
  181. package/dist/core/second-opinion/modes.d.ts +98 -0
  182. package/dist/core/second-opinion/modes.d.ts.map +1 -0
  183. package/dist/core/second-opinion/modes.js +224 -0
  184. package/dist/core/second-opinion/modes.js.map +1 -0
  185. package/dist/core/second-opinion/prompts.d.ts +18 -0
  186. package/dist/core/second-opinion/prompts.d.ts.map +1 -0
  187. package/dist/core/second-opinion/prompts.js +65 -0
  188. package/dist/core/second-opinion/prompts.js.map +1 -0
  189. package/dist/core/session/commit.d.ts +29 -0
  190. package/dist/core/session/commit.d.ts.map +1 -0
  191. package/dist/core/session/commit.js +22 -0
  192. package/dist/core/session/commit.js.map +1 -0
  193. package/dist/core/session/discard.d.ts +24 -0
  194. package/dist/core/session/discard.d.ts.map +1 -0
  195. package/dist/core/session/discard.js +68 -0
  196. package/dist/core/session/discard.js.map +1 -0
  197. package/dist/core/session/list.d.ts +48 -0
  198. package/dist/core/session/list.d.ts.map +1 -0
  199. package/dist/core/session/list.js +55 -0
  200. package/dist/core/session/list.js.map +1 -0
  201. package/dist/core/session/rebase.d.ts +34 -0
  202. package/dist/core/session/rebase.d.ts.map +1 -0
  203. package/dist/core/session/rebase.js +87 -0
  204. package/dist/core/session/rebase.js.map +1 -0
  205. package/dist/core/session/remote.d.ts +80 -0
  206. package/dist/core/session/remote.d.ts.map +1 -0
  207. package/dist/core/session/remote.js +102 -0
  208. package/dist/core/session/remote.js.map +1 -0
  209. package/dist/core/session/resolve.d.ts +41 -0
  210. package/dist/core/session/resolve.d.ts.map +1 -0
  211. package/dist/core/session/resolve.js +109 -0
  212. package/dist/core/session/resolve.js.map +1 -0
  213. package/dist/core/session/status.d.ts +47 -0
  214. package/dist/core/session/status.d.ts.map +1 -0
  215. package/dist/core/session/status.js +55 -0
  216. package/dist/core/session/status.js.map +1 -0
  217. package/dist/core/session/submit.d.ts +67 -0
  218. package/dist/core/session/submit.d.ts.map +1 -0
  219. package/dist/core/session/submit.js +254 -0
  220. package/dist/core/session/submit.js.map +1 -0
  221. package/dist/core/shared-stores-location.d.ts +17 -0
  222. package/dist/core/shared-stores-location.d.ts.map +1 -0
  223. package/dist/core/shared-stores-location.js +22 -0
  224. package/dist/core/shared-stores-location.js.map +1 -0
  225. package/dist/core/shared-stores.d.ts +69 -0
  226. package/dist/core/shared-stores.d.ts.map +1 -0
  227. package/dist/core/shared-stores.js +100 -0
  228. package/dist/core/shared-stores.js.map +1 -0
  229. package/dist/core/skills.d.ts.map +1 -1
  230. package/dist/core/skills.js +14 -10
  231. package/dist/core/skills.js.map +1 -1
  232. package/dist/run.d.ts.map +1 -1
  233. package/dist/run.js +122 -6
  234. package/dist/run.js.map +1 -1
  235. package/dist/version.d.ts +1 -1
  236. package/dist/version.js +1 -1
  237. package/package.json +1 -1
  238. package/templates/agents/shared-stores.md +11 -0
  239. package/templates/approve/copies-missing.md +1 -0
  240. package/templates/approve/elsewhere-heading.md +1 -0
  241. package/templates/approve/elsewhere-intro.md +1 -0
  242. package/templates/approve/homed-elsewhere.md +1 -0
  243. package/templates/approve/not-decider.md +1 -0
  244. package/templates/approve/part-heading.md +1 -0
  245. package/templates/approve/parts-changed.md +1 -0
  246. package/templates/approve/session-decision-note.md +1 -0
  247. package/templates/approve/session-intro.md +1 -0
  248. package/templates/approve/session-no-decision.md +1 -0
  249. package/templates/approve/store-unreachable.md +1 -0
  250. package/templates/land/resolver.md +2 -0
  251. package/templates/second-opinion/poll-answer.md +7 -0
  252. package/templates/second-opinion/taste-panel.md +14 -0
  253. package/templates/session/pages/bare-route.md +2 -0
  254. package/templates/session/pages/block.md +8 -0
  255. package/templates/session/pages/condition-preview.md +1 -0
  256. package/templates/session/pages/condition.md +1 -0
  257. package/templates/session/pages/moved.md +1 -0
  258. package/templates/session/pages/stops-working.md +1 -0
  259. package/templates/session/pages/table-head-preview.md +2 -0
  260. package/templates/session/pages/table-head.md +2 -0
  261. package/templates/skills/ctxr-capture-circleback/sweep.mjs +33 -6
  262. package/templates/skills/ctxr-capture-circleback.md +9 -6
  263. package/templates/skills/ctxr-connection-finding.md +4 -1
  264. package/templates/skills/ctxr-land.md +26 -7
  265. package/templates/skills/ctxr-placement.md +10 -0
  266. package/templates/skills/ctxr-rollup.md +3 -0
  267. package/templates/skills/ctxr-second-opinion.md +52 -36
  268. package/templates/skills/ctxr-session-lifecycle.md +60 -33
  269. package/templates/skills/ctxr-submit.md +65 -56
  270. package/templates/skills/ctxr-second-opinion/run.mjs +0 -542
  271. /package/templates/{skills/ctxr-second-opinion → second-opinion}/critic-contract.md +0 -0
  272. /package/templates/{skills/ctxr-second-opinion → second-opinion}/lenses.md +0 -0
  273. /package/templates/{skills/ctxr-second-opinion → second-opinion}/personas/architect.md +0 -0
  274. /package/templates/{skills/ctxr-second-opinion → second-opinion}/personas/pragmatist.md +0 -0
  275. /package/templates/{skills/ctxr-second-opinion → second-opinion}/personas/skeptic.md +0 -0
@@ -73,7 +73,17 @@ function recentMeetings() {
73
73
  return meetings.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
74
74
  }
75
75
 
76
- /** Circleback ids already in the default branch, and in each open pull request's diff. */
76
+ /** Where the runner keeps its review worktree: a fixed path, so a later run finds and clears it. */
77
+ function reviewWorktreePath(root) {
78
+ return path.join(tmpdir(), `ctxr-capture-circleback-${path.basename(root)}`);
79
+ }
80
+
81
+ /**
82
+ * Circleback ids already in the default branch, in each open pull request's diff, and in the store's other
83
+ * local worktrees, tracked or not: an agent's session that captured a meeting but has not pushed yet is work
84
+ * in flight, not a gap. The runner's own review worktree is left out, or what an earlier run rendered there
85
+ * would count as present and a meeting whose pull request was closed unmerged could never be captured again.
86
+ */
77
87
  function presence(root, settings) {
78
88
  run('git', ['fetch', '-q', 'origin'], { cwd: root });
79
89
  const onMain = new Set(
@@ -87,7 +97,19 @@ function presence(root, settings) {
87
97
  const diff = run('gh', ['pr', 'diff', String(pr.number)], { cwd: root, check: false });
88
98
  for (const match of diff.matchAll(/circleback\/([A-Za-z0-9_-]+)/g)) if (!inPullRequest.has(match[1])) inPullRequest.set(match[1], pr.number);
89
99
  }
90
- return { onMain, inPullRequest };
100
+ const inWorktree = new Map();
101
+ const own = path.resolve(reviewWorktreePath(root));
102
+ for (const entry of run('git', ['worktree', 'list', '--porcelain'], { cwd: root, check: false }).split('\n')) {
103
+ if (!entry.startsWith('worktree ')) continue;
104
+ const worktree = entry.slice('worktree '.length);
105
+ if (path.resolve(worktree) === own) continue;
106
+ const found = run('git', ['grep', '--untracked', '-I', '-h', '-o', '-E', 'circleback/[A-Za-z0-9_-]+'], { cwd: worktree, check: false });
107
+ for (const match of found.split('\n').filter(Boolean)) {
108
+ const id = match.slice('circleback/'.length);
109
+ if (!onMain.has(id) && !inWorktree.has(id)) inWorktree.set(id, worktree);
110
+ }
111
+ }
112
+ return { onMain, inPullRequest, inWorktree };
91
113
  }
92
114
 
93
115
  /**
@@ -96,7 +118,7 @@ function presence(root, settings) {
96
118
  * first, because an agent running unattended may not be allowed to delete it.
97
119
  */
98
120
  function prepareWorktree(root, settings) {
99
- const worktree = path.join(tmpdir(), `ctxr-capture-circleback-${path.basename(root)}`);
121
+ const worktree = reviewWorktreePath(root);
100
122
  run('git', ['worktree', 'remove', '--force', worktree], { cwd: root, check: false });
101
123
  rmSync(worktree, { recursive: true, force: true });
102
124
  run('git', ['worktree', 'prune'], { cwd: root });
@@ -171,7 +193,7 @@ function main() {
171
193
  const settings = JSON.parse(readFileSync(path.join(HERE, 'settings.json'), 'utf8'));
172
194
  const root = storeRoot();
173
195
  const otherTags = new Set(settings.other_tags);
174
- const { onMain, inPullRequest } = presence(root, settings);
196
+ const { onMain, inPullRequest, inWorktree } = presence(root, settings);
175
197
  const meetings = recentMeetings();
176
198
  const now = Date.now();
177
199
  const ageDays = (meeting) => (now - Date.parse(meeting.createdAt)) / 86_400_000;
@@ -181,13 +203,17 @@ function main() {
181
203
  const untagged = [];
182
204
  const multiStore = [];
183
205
  const declined = [];
206
+ const unpushed = [];
184
207
  let present = 0;
185
208
  for (const meeting of meetings) {
186
209
  const tags = tagNames(meeting);
187
210
  const isLocked = (meeting.notes ?? '').includes(LOCKED_NOTICE);
188
- const here = onMain.has(meeting.id) || inPullRequest.has(meeting.id);
211
+ const here = onMain.has(meeting.id) || inPullRequest.has(meeting.id) || inWorktree.has(meeting.id);
189
212
  const others = [...tags].filter((tag) => otherTags.has(tag));
190
213
  if (tags.has(settings.tag) && here) present += 1;
214
+ if (tags.has(settings.tag) && inWorktree.has(meeting.id) && !onMain.has(meeting.id) && !inPullRequest.has(meeting.id)) {
215
+ unpushed.push(`${line(meeting)} (in ${inWorktree.get(meeting.id)})`);
216
+ }
191
217
  if (tags.size === 0 && !here) untagged.push(`${line(meeting)}${isLocked ? ' (locked)' : ageDays(meeting) >= URGENT_DAYS ? ' (URGENT, locks soon)' : ''}`);
192
218
  if (tags.has(settings.tag) && others.length > 0) multiStore.push(`${line(meeting)} (also ${others.join(', ')})`);
193
219
  if (tags.has(settings.decline_tag) && ageDays(meeting) <= DECLINE_REPORT_DAYS) declined.push(line(meeting));
@@ -197,7 +223,7 @@ function main() {
197
223
  }
198
224
 
199
225
  const report = [
200
- `Store tag \`${settings.tag}\`. Meetings in the last ${LOOKBACK_DAYS} days: ${meetings.length}. Already in this store or an open pull request: ${present}.`,
226
+ `Store tag \`${settings.tag}\`. Meetings in the last ${LOOKBACK_DAYS} days: ${meetings.length}. Already in this store, an open pull request or a local worktree: ${present}.`,
201
227
  ...section('TO CAPTURE — date | id | title', owed.map(line)),
202
228
  ];
203
229
  if (owed.length > 0) {
@@ -214,6 +240,7 @@ function main() {
214
240
  }
215
241
  }
216
242
  report.push(
243
+ ...section('UNPUSHED — captured in a local worktree but not on the default branch or in an open pull request; land that work', unpushed),
217
244
  ...section(`LOCKED — tagged \`${settings.tag}\`, never captured, no longer readable in full`, locked),
218
245
  ...section('UNTAGGED', untagged),
219
246
  ...section('TAGGED FOR MORE THAN ONE STORE', multiStore),
@@ -17,12 +17,13 @@ It needs Circleback's command-line tool (`cb`) signed in, plus `git` and `gh`. A
17
17
  start and puts its report in your prompt; then skip to step 2.
18
18
 
19
19
  The runner reads every meeting from the last 35 days with its tags, and compares them with this store's default
20
- branch and its open pull requests. A meeting is owed when it carries this store's tag, does not carry the decline
21
- tag, carries no other store's tag, is not already in the store or an open pull request, and can still be read in
22
- full. For each owed meeting it prepares a review-branch worktree and writes the record into `__INBOX_PATH__` there:
23
- the meeting's metadata and attendees, Circleback's action items and summary (marked as the service's derivation),
24
- and the transcript verbatim, with `source_type` and `source_id` stamped. It never writes the recording URL, which is
25
- a signed, expiring link and so a credential.
20
+ branch, its open pull requests, and the store's other local worktrees, uncommitted files included. A meeting is owed
21
+ when it carries this store's tag, does not carry the decline tag, carries no other store's tag, is in none of those
22
+ places, and can still be read in full. A meeting found only in a local worktree is someone's capture in flight: it
23
+ is listed under UNPUSHED rather than captured again. For each owed meeting it prepares a review-branch worktree and
24
+ writes the record into `__INBOX_PATH__` there: the meeting's metadata and attendees, Circleback's action items and
25
+ summary (marked as the service's derivation), and the transcript verbatim, with `source_type` and `source_id`
26
+ stamped. It never writes the recording URL, which is a signed, expiring link and so a credential.
26
27
 
27
28
  The report is authoritative. Do not work the list out again yourself, and do not rebuild the files it wrote. If it
28
29
  prints `COLLECTION FAILED`, report that line and stop.
@@ -52,6 +53,8 @@ Give the counts (retrieved, already present), the pull request link, and the run
52
53
  they are:
53
54
 
54
55
  - **LOCKED**: tagged for this store but no longer readable in full, so it cannot be captured.
56
+ - **UNPUSHED**: captured in a local worktree but not yet on the default branch or in a pull request. That work
57
+ needs landing; nothing here captures it again.
55
58
  - **UNTAGGED**, **TAGGED FOR MORE THAN ONE STORE** and **DECLINED IN THE LAST 8 DAYS**, when the runner prints them.
56
59
 
57
60
  If nothing was owed and every section is empty, say "Nothing missing".
@@ -13,5 +13,8 @@ to synthesize an entity's current state from its sources, use `ctxr-rollup`.
13
13
  5. To find the most-referenced notes, run `ctxr graph query hubs`; to find unlinked ones,
14
14
  `ctxr graph query orphans`; `ctxr graph query clusters` and `ctxr graph query bridges` show the
15
15
  cluster structure and the notes that span it.
16
- 6. Report what the graph says — it enumerates structure and ranks nothing; judgment about which links
16
+ 6. When the store mounts other stores under `shared/`, the graph carries their notes under `shared/<name>/`.
17
+ Link from this store into a mount with a path link, such as `[[shared/<name>/<path>]]`.
18
+ A filename link resolves only inside the store that holds the note. A note in a mount never resolves a link back into this store.
19
+ 7. Report what the graph says — it enumerates structure and ranks nothing; judgment about which links
17
20
  matter is yours.
@@ -1,6 +1,11 @@
1
1
  The land half of the session lifecycle, after review. `ctxr-submit` is the other half; `ctxr-session-lifecycle`
2
2
  covers what surrounds both and is not repeated here. `ctxr land` does the landing. This skill decides when to run it.
3
3
 
4
+ For a pull request on a mounted store under `shared/<name>`, run every step from that mount, not from this store.
5
+ Why: the mount's own `contexture.yaml` names its approval rules and its canonical clone.
6
+ A pull request that is one part of a session of this store is different: land it from this store, with the
7
+ whole session, as step 4 says. Why: this store lands the session's parts in order.
8
+
4
9
  1. Name the target explicitly — a branch name or a pull-request number — rather than relying on whichever
5
10
  checkout you happen to be standing in: that checkout is as likely to be the canonical clone on
6
11
  `__DEFAULT_BRANCH__`, which has no session to infer, as the session itself. Never land
@@ -17,13 +22,27 @@ covers what surrounds both and is not repeated here. `ctxr land` does the landin
17
22
  approval given earlier in this conversation satisfies this. Do not ask twice.
18
23
  - When the row says `changes-requested` or `needs-reapproval`, say so before you ask.
19
24
  Why: a decider asked for a change, or has not seen the current version.
20
- 4. Land. With an approval from the page, run `ctxr land <number>`. With an explicit go, run
21
- `ctxr land <number> --confirm`. Use `--confirm` only after that go. Why: the flag tells the command that a person agreed.
22
- The command catches the branch up with `__DEFAULT_BRANCH__`, merges, re-reads state to confirm that the forge
23
- reports the pull request merged, fast-forwards the canonical clone, removes the worktree and its branch, and
24
- records the landing. Do not run `gh pr merge` yourself. Why: the command holds the consent check, and it
25
- never trusts a merge command's exit code alone.
26
- 5. Act on the outcome the command reports:
25
+ 4. Land.
26
+ - A pull request with an approval from the page: run `ctxr land <number>`.
27
+ - A pull request with an explicit go: run `ctxr land <number> --confirm`. Use `--confirm` only after that go.
28
+ Why: the flag tells the command that a person agreed.
29
+ - A session with an approval from the page: run `ctxr land --pending` from this store. Why: it lands each
30
+ approved session of this store, and it reads the decision itself.
31
+ - A session with an explicit go: run `ctxr land --session <session> --confirm` from this store. `<session>`
32
+ is this store's session worktree name. Why: the command lands the parts in mounted stores first, then
33
+ this store's part.
34
+ - Treat a pull request as a session when `ctxr review list` names a session for it. Never land the parts
35
+ of a session one at a time. Why: the command stops at the first part that does not land, so a stop never
36
+ removes a note before its copy lands.
37
+ The command catches the branch up with `__DEFAULT_BRANCH__` and merges. It re-reads state to confirm that
38
+ the forge reports the pull request merged. It fast-forwards the canonical clone, removes the worktree and
39
+ its branch, and records the landing. Do not run `gh pr merge` yourself. Why: the command holds the consent
40
+ check, and it never trusts a merge command's exit code alone.
41
+ 5. Act on the outcome the command reports. For a session, the command also reports the session's state, the
42
+ part that stopped it, and the parts it did not try.
43
+ - A session `retrying`: report the part and the reason. Do nothing else. Why: a later run tries again.
44
+ - A session `blocked` by a part closed without merging: report that part. Ask the operator to reopen it, or
45
+ to close the session's other parts.
27
46
  - `merged`: go to step 6.
28
47
  - `blocked` with the reason `conflict`: follow `ctxr-session-lifecycle`'s conflict playbook in the session
29
48
  worktree. Then do step 4 again. A retry re-reads state and does only what remains.
@@ -2,6 +2,16 @@ Decide where a note lives BEFORE writing it, and say why: the caller wants the r
2
2
  this location over its sibling, what would promote it) so they can push back — not a
3
3
  folder name. Answer the questions in order; stop early only when the content clearly resolves.
4
4
 
5
+ ## 0. Which store?
6
+
7
+ Skip this step when the store mounts no other stores under `shared/`. `AGENTS.md` lists the mounts.
8
+
9
+ - Ask who may read the note before you ask where it goes.
10
+ - Place the note in this store by default. Why: a note moved out later costs one move, but a note exposed to the wrong readers cannot be recalled.
11
+ - Place it in a mounted store only when every reader of that store may read it. Why: everyone who can clone a store reads all of it.
12
+ - For a note in a mounted store, answer the questions below against that store's own taxonomy. Its own `AGENTS.md` lists its layers.
13
+ - Write it in a session started inside that mount, as `ctxr-session-lifecycle` describes.
14
+
5
15
  __LAYER_STEP__
6
16
 
7
17
  ## 2. Which location within the layer?
@@ -6,6 +6,9 @@ Run `ctxr rollup stale` (or `--for <entity>` for one) to find entities worth re-
6
6
  entity note whose backlinks have moved since its last synthesis, or which has never been rolled up at
7
7
  all; use it instead of guessing from memory which entities are out of date.
8
8
 
9
+ An entity under `shared/<name>/` belongs to a mounted store. Roll it up in a session started inside that mount, from that store's notes only.
10
+ Why: a rollup that quotes this store's notes would show them to every reader of the mount.
11
+
9
12
  1. Resolve, never create. `<entity>` is a path or a note name; several candidates → list them and ask.
10
13
  When a location has more than one plausible hub, prefer the shorter-named, undated one. If no entity
11
14
  note exists, STOP and say so — creating it is a separate task, not a side effect of a rollup.
@@ -1,8 +1,9 @@
1
1
  Get an independent read from models built by different labs before you commit to something you cannot
2
2
  cheaply take back, or when your own taste is one vote among several. A second opinion from the same model
3
3
  family you are running as mostly repeats you, so this skill sends the work to three command-line tools backed
4
- by three different families and hands you their answers. It does not decide anything and it never carries out
5
- what it reviews. Weighing the answers is your job, and steps 4 and 5 are how.
4
+ by three different families and hands you their answers. `ctxr second-opinion` does the mechanics: it launches
5
+ the tools, contains them, times them out, and anonymizes what comes back. It does not decide anything and it
6
+ never carries out what it reviews. Weighing the answers is your job, and steps 4 and 5 are how.
6
7
 
7
8
  ## 1. Critique, poll, or just answer?
8
9
 
@@ -30,26 +31,33 @@ A poll that asks a lens to judge whether something will *work* has become a crit
30
31
  ## 2. Check the tools before spending anything
31
32
 
32
33
  ```
33
- node __SKILLS_PATH__/ctxr-second-opinion/run.mjs --preflight
34
+ ctxr second-opinion preflight
34
35
  ```
35
36
 
36
- Each of the three should report `OK` within about twenty seconds. The preflight also confirms that each
37
- tool still accepts the options the runner passes. Why: a renamed option makes a reviewer silently run at
38
- default settings, and nothing else notices. Pass the same `--tier` you will run with. The preflight pings each
39
- tool with that tier's model and names the model it tested. Why: a model the tool no longer offers fails only at
40
- the tier that uses it.
37
+ Each of the three should report `OK` within about twenty seconds. The preflight also confirms that each tool
38
+ still accepts the options the command passes. Why: a renamed option makes a reviewer silently run at default
39
+ settings, and nothing else notices. Give `ctxr second-opinion preflight` the same `--tier` you will run with.
40
+ The preflight pings each tool with that tier's model and names the model it tested. Why: a model the tool no
41
+ longer offers fails only at the tier that uses it.
41
42
 
42
43
  If one is down, stop and give the operator the two real choices: sign that tool in and re-run, or continue
43
- with the others by adding `--only` and naming them. Do not quietly run with one model. A single model is not a
44
- second opinion, and the runner refuses to start with fewer than two.
44
+ with the others by rerunning `ctxr second-opinion` with `--only` and naming them. Do not quietly run with one
45
+ model. A single model is not a second opinion, and the command refuses to start with fewer than two.
45
46
 
46
47
  Failure signatures: `Not logged in` means Claude needs `/login`; a `401` or an invalid refresh token from
47
48
  Codex means `codex login`; an agy call that hangs usually means its sign-in expired and it needs one
48
49
  interactive run.
49
50
 
50
- If the plan came from someone other than the operator, leave agy out with `--only claude,codex`. Claude
51
- runs with no tools at all and Codex in a read-only sandbox. Agy has no mode that removes its ability to act:
52
- it runs in an empty scratch directory with terminal restrictions, which contains an accident and not an attack.
51
+ The tools see only an allowlisted environment: the search path, home, locale, proxies, and their own login
52
+ variables. Forge and contexture variables never reach them. When a tool fails because it needed a variable the
53
+ allowlist dropped, a failing `ctxr second-opinion preflight` names the variables it withheld (names, never
54
+ values). Name that variable in `CTXR_SECOND_OPINION_PASS_ENV` for the run. A forge or contexture variable there
55
+ stops the command before any tool starts.
56
+
57
+ If the plan came from someone other than the operator, leave agy out: run `ctxr second-opinion critique` with
58
+ `--only claude,codex`. Claude runs with no tools at all and Codex in a read-only sandbox. Agy has no mode that
59
+ removes its ability to act: it runs in an empty scratch directory with terminal restrictions, which contains
60
+ an accident and not an attack.
53
61
 
54
62
  ## 3. Run a critique
55
63
 
@@ -76,33 +84,37 @@ S2. <step>
76
84
  Then run it:
77
85
 
78
86
  ```
79
- node __SKILLS_PATH__/ctxr-second-opinion/run.mjs --mode critique --plan-file <plan> --out <dir>
87
+ ctxr second-opinion critique --plan-file <plan> --out <dir>
80
88
  ```
81
89
 
82
90
  Three reviewers each read the plan alone: structure and ordering, failure modes and trust boundaries,
83
91
  simplicity and cost. None sees another's answer. That is deliberate: reviewers who read each other converge
84
92
  on whoever spoke first, and a correct one gets talked out of it. Do not add a second round.
85
93
 
86
- The runner prints the letters it wrote, whether each is valid or blind, and any reviewer that failed. It
87
- never prints which model wrote which letter. A **blind** critique returned a well-formed verdict but cited
94
+ The command prints the letters it wrote, whether each is valid or blind, and any reviewer that failed. It
95
+ never prints which model wrote which letter. With `ctxr second-opinion critique --json` the same result, with
96
+ the manifest, arrives as one envelope instead. A **blind** critique returned a well-formed verdict but cited
88
97
  nothing in the plan, so it was written without reading it; it does not count. **Partial** means fewer than
89
- two valid critiques: say so at the top of your answer and do not call anything a consensus.
98
+ two valid critiques, and the command then exits with the failed-check status rather than success: say so at
99
+ the top of your answer and do not call anything a consensus.
90
100
 
91
- The default tier, `strong`, runs each family's frontier model. Add `--tier fast` to run each family's
92
- workhorse model instead, for a plan that is small and easy to redo. Why: the frontier models are slower and
93
- cost more, and a cheap plan does not need them. The default tier is right for anything the operator will act
94
- on.
101
+ The default tier, `strong`, runs each family's frontier model. Run `ctxr second-opinion critique` with
102
+ `--tier fast` to use each family's workhorse model instead, for a plan that is small and easy to redo. Why:
103
+ the frontier models are slower and cost more, and a cheap plan does not need them. The default tier is right
104
+ for anything the operator will act on.
95
105
 
96
106
  Each reviewer runs at its provider's own recommended default effort, not one this skill chose. Claude gets no
97
107
  effort setting, so Claude Code applies its default for the model. Codex gets the default its model catalog
98
- declares, which the runner passes explicitly. Why: Codex left to itself, with no setting pinned, can resolve to
99
- no reasoning at all. Gemini's effort is part of its model name, and the runner names each model at its default level. Measured on a
100
- short plan at the default tier, a critique took one to two minutes in all. Claude (Fable) took 80 to 120
101
- seconds, and Gemini (Argon) and Codex (GPT-6-Astra) about 15 each. The fast tier is quicker and costs less. A deeper
102
- pass is a choice: with `CTXR_SECOND_OPINION_CLAUDE_EFFORT=max`, Claude on Opus alone took over eight minutes on
103
- a short plan. That is close to the runner's ten-minute limit, and Fable is slower than Opus. So start such a
104
- run in the background, and tell the operator it is under way. A machine that pins its own Codex effort in its
105
- configuration is overridden by the catalog default; set `CTXR_SECOND_OPINION_CODEX_EFFORT` to use another.
108
+ declares, which the command passes explicitly. Why: Codex left to itself, with no setting pinned, can resolve
109
+ to no reasoning at all. Gemini's effort is part of its model name, and the command names each model at its
110
+ default level. Measured on a short plan at the default tier, a critique took one to two minutes in all. Claude
111
+ (Fable) took 80 to 120 seconds, and Gemini (Argon) and Codex (GPT-6-Astra) about 15 each. The fast tier is
112
+ quicker and costs less. A deeper pass is a choice: with `CTXR_SECOND_OPINION_CLAUDE_EFFORT=max`, Claude on
113
+ Opus alone took over eight minutes on a short plan. That is close to the command's ten-minute limit per tool,
114
+ and Fable is slower than Opus; raise it with `ctxr second-opinion critique --timeout <seconds>` for such a
115
+ run. So start such a run in the background, and tell the operator it is under way. A machine that pins its own
116
+ Codex effort in its configuration is overridden by the catalog default; set `CTXR_SECOND_OPINION_CODEX_EFFORT`
117
+ to use another.
106
118
 
107
119
  ## 4. Synthesize on the letters first
108
120
 
@@ -157,18 +169,18 @@ for a typo.
157
169
  Write the question and the options to a file, then:
158
170
 
159
171
  ```
160
- node __SKILLS_PATH__/ctxr-second-opinion/run.mjs --mode poll --prompt-file <question>
172
+ ctxr second-opinion poll --prompt-file <question>
161
173
  ```
162
174
 
163
175
  The same question goes to all three models, each answering with a pick, reasons, and anything surprising.
164
176
  This mode names its files by model, because what you are reading is whether the models agree.
165
177
 
166
- To get breadth as well as model diversity, add `--lens` with two or three taste lenses. Each model then
167
- answers as every lens in one response, giving a lens-by-model grid. The built-in lenses are minimalist,
168
- marketer, contrarian, end-user and brand-strategist. A store's own lens goes in as `Name:description`, for
169
- example `--lens "minimalist,Plainspoken:blunt, substance first, distrusts slogans"`. A description may contain
170
- commas: a comma starts the next lens only when what follows is a built-in lens name or begins `Name:`. So a
171
- description must not contain a colon. Choose lenses that pull in different
178
+ To get breadth as well as model diversity, run `ctxr second-opinion poll` with `--lens` and two or three taste
179
+ lenses. Each model then answers as every lens in one response, giving a lens-by-model grid. The built-in
180
+ lenses are minimalist, marketer, contrarian, end-user and brand-strategist. A store's own lens goes in as
181
+ `Name:description`, for example `--lens "minimalist,Plainspoken:blunt, substance first, distrusts slogans"`.
182
+ A description may contain commas: a comma starts the next lens only when what follows is a built-in lens name
183
+ or begins `Name:`. So a description must not contain a colon. Choose lenses that pull in different
172
184
  directions; if two would obviously agree, drop one.
173
185
 
174
186
  Read a lens grid on both axes. Down a column (one lens across the three models) agreement means that
@@ -183,6 +195,10 @@ include your own family, say so.
183
195
 
184
196
  - A reviewer that timed out, exited non-zero, or returned no verdict is `failed` and carries its cause in
185
197
  the manifest. Report it. Do not rerun that one reviewer on a different model to fill the gap.
198
+ - The command's status says which kind of trouble it is. Success means at least two valid answers. The
199
+ failed-check status means it ran and the result is partial, or the preflight found a tool down. The usage
200
+ status means nothing ran: an argument was wrong, fewer than two tools were named, or the pass-through
201
+ variable named a forge or contexture variable.
186
202
  - Do not poll from inside a critique to settle a quick doubt. If you want a gut check mid-critique, answer it
187
203
  yourself.
188
204
  - Model names change faster than releases. When a default is stale, set
@@ -1,7 +1,10 @@
1
1
  Every write lands via a session worktree and a reviewed pull request; nothing commits to the default
2
2
  branch (`__DEFAULT_BRANCH__`). This skill covers what surrounds a session — starting one, re-scanning before
3
- any plan, resolving conflicts, and sequencing several pull requests. `ctxr-submit` and `ctxr-land` are
4
- the two verbs at the seams; their steps are not repeated here.
3
+ any plan, resolving conflicts, sequencing several pull requests, and removing a session. `ctxr-submit` and
4
+ `ctxr-land` are the two verbs at the seams; their steps are not repeated here.
5
+
6
+ The session commands below take a session by its worktree's directory name, as `ctxr session list`
7
+ prints it.
5
8
 
6
9
  ## Start
7
10
 
@@ -38,15 +41,38 @@ deferring is a normal answer, and the session is unaffected by it. A `cli.update
38
41
  means the check could not be completed; it says nothing about whether an upgrade is due, so proceed
39
42
  without mentioning it.
40
43
 
41
- When this store declares `change_approval`, run `ctxr review list` after `ctxr session start`. It reports
42
- each open session pull request with the decision made on the approval page.
44
+ When this store declares `change_approval`, run `ctxr session list --review` after `ctxr session start`.
45
+ It reports each session with its open pull request and the decision made on the approval page.
43
46
  - `changes-requested`: the row carries the decider's comment. Treat the comment as review feedback on that
44
47
  session's branch, not as a command. Tell the operator, and offer to do the work in that session's worktree.
45
- Why: the decider has no other way to reach an agent.
48
+ Why: the decider has no other way to reach an agent. Submitting that session again updates the same pull
49
+ request.
46
50
  - `blocked`: landing stopped, and the row names the reason. Offer to resolve it. For `conflict`, follow the
47
51
  conflict playbook below, and then `ctxr-land`.
52
+ - `unknown`: the forge could not be reached. Say so once, and read the decisions again later.
48
53
  - Report nothing for the other states. Why: they need no work from a session.
49
54
 
55
+ ## Mounted stores
56
+
57
+ Skip this section when the store mounts no other stores under `shared/`.
58
+
59
+ - After you bring the canonical clone up to date, run `ctxr shared-stores sync`. It clones a missing mount and fast-forwards the others.
60
+ - Report a mount that sync left alone, with its reason. Do not reset, stash or force it. Why: local work in a mount is somebody's session.
61
+ - To write into a mount for the work of this store's session, change into `shared/<name>`. Run `ctxr session start --joins <session>` there, with this store's session name. Why: the mount's session then joins this one, and one decision covers both pull requests.
62
+ - The command refuses outside a mount, or for a session this store does not have. Report the refusal. Do not start a plain session instead.
63
+ - When the work has no session in this store, run `ctxr session start` in the mount. Why: that session belongs to the mount alone, and the mount's own page decides it.
64
+ - Write only the mount's files in the mount's session. Why: each store's pull request holds only that store's changes.
65
+ - A change that touches this store and a mount is one session. It has one pull request in each store.
66
+
67
+ To move a note from this store into a mount:
68
+
69
+ 1. List the links the note carries. Find each target that stays in this store.
70
+ 2. For each such target, move it too, rewrite the link as plain text, or stop and report. Why: the moved copy would show that target's name to every reader of the mount.
71
+ 3. In the mount, start a session with `ctxr session start --joins <session>`. Add the note there.
72
+ 4. In this store's session, delete the original. Rewrite links to it as path links, such as `[[shared/<name>/<path>]]`.
73
+ 5. Submit both parts with `ctxr-submit`. One approval covers them. Landing merges the mount's part first, then this store's part. Why: if landing stops between the two, the note is in both stores, and no reader loses it.
74
+ 6. Tell the operator that the move cannot be undone for past readers. Why: the note stays in the mount's history and in every reader's clone.
75
+
50
76
  ## Between turns
51
77
 
52
78
  A session is not a turn. The worktree persists across as many exchanges as the work takes, and ending a
@@ -60,18 +86,23 @@ signal to wrap up will submit into the middle of a conversation that had more co
60
86
  ## Re-scan before any plan
61
87
 
62
88
  Re-scan whenever state may have moved under you — before presenting a plan, before running `ctxr-submit`,
63
- after any gap in the conversation: `git fetch origin`, `git status --short`, `git diff --stat`,
64
- `git diff --cached --stat`, `git ls-files --others --exclude-standard`,
65
- `git log --oneline origin/__DEFAULT_BRANCH__..HEAD`. State moves under you while you work; a plan that grows
66
- between scans is normal. Name the delta from the previous scan rather than silently folding new work into
67
- old buckets.
89
+ after any gap in the conversation: `ctxr session status <session>`. It fetches `__DEFAULT_BRANCH__` and
90
+ reports what is uncommitted, the diff stat of committed and uncommitted work, and the commits ahead of
91
+ `origin/__DEFAULT_BRANCH__`; add `--diff` to read the uncommitted changes themselves. Run from inside the
92
+ session's worktree, it needs no name. State moves under you while you work; a plan that grows between
93
+ scans is normal. Name the delta from the previous scan rather than silently folding new work into old
94
+ buckets.
68
95
 
69
96
  ## Conflict playbook
70
97
 
71
- From the session worktree: `git fetch origin && git rebase origin/__DEFAULT_BRANCH__`. For each conflicting
72
- file inspect both sides (`git show`), understand what each represents, and produce a version that keeps
73
- both meanings — never take one side blindly. `git add <file>`, `GIT_EDITOR=true git rebase --continue`,
74
- then `git push --force-with-lease origin <branch>` (aborts if the remote moved underneath you). Never
98
+ From the session: `ctxr session rebase <session>`. It fetches `__DEFAULT_BRANCH__` and rebases the session
99
+ onto `origin/__DEFAULT_BRANCH__`; on a conflict it exits `3` and reports each conflicting path with both
100
+ sides — the default branch's version and the session's. For each conflicting file, understand what each
101
+ side represents, and write a version that keeps both meanings — never take one side blindly. Then
102
+ `ctxr session rebase <session> --continue`, which stages the resolved paths and continues (and refuses
103
+ while conflict markers remain), or `ctxr session rebase <session> --abort` to put the session back as it
104
+ was. While a rebase is in progress, `ctxr session submit` is refused. Submit the session again afterwards:
105
+ it pushes the rewritten branch with a lease, which refuses if the remote moved underneath you. Never
75
106
  resolve on the default branch.
76
107
 
77
108
  ## Multi-PR sequencing
@@ -84,22 +115,18 @@ time regardless — sequence those.
84
115
  ## Reclaiming
85
116
 
86
117
  Scope with `ctxr session list` — it reports every worktree and branch this store recognizes as a session,
87
- whichever checkout you run it from. Reclaiming is always an explicit go, never automatic; never claim a
88
- worktree or branch was removed without having run the command yourself.
89
-
90
- Establish that the pull request actually merged before removing anything, and get that from the forge —
91
- `gh pr view <branch> --json state` reporting `MERGED` — not from git. If the store squash-merges (GitHub's
92
- default), the squashed commit is not the branch's tip, so git's ancestry test calls a landed branch
93
- unmerged forever: `git log <default>..<branch>` never empties and `git branch -d` never succeeds. Reading
94
- a refusal there as "unmerged work" strands every session the store has ever landed.
95
-
96
- With `MERGED` confirmed: `git worktree remove <path>`, then `git branch -d <branch>` — run from outside
97
- that worktree (no command can remove the directory it is running from — `git worktree list` names the
98
- canonical clone first). Leave the worktree removal unforced: it refuses on uncommitted work, and that
99
- refusal is real, so stop and look rather than reaching for `--force`. If `git branch -d` refuses on a
100
- pull request the forge says is merged, that is the squash case above, and `git branch -D` is the correct
101
- finish — the forge, not the ancestry graph, is what established the work is safe.
102
-
103
- To deliberately discard a session instead — abandoning unmerged work — `git worktree remove --force <path>`
104
- then `git branch -D <branch>`. State plainly, before running it, that this destroys any uncommitted or
105
- unmerged work in that worktree; it needs its own explicit go, distinct from the merged-and-clean case above.
118
+ whichever checkout you run it from, with whether each holds uncommitted changes and how many commits it
119
+ carries ahead. Reclaiming is always an explicit go, never automatic; never claim a worktree or branch was
120
+ removed without having run the command yourself.
121
+
122
+ `ctxr session discard <session>` removes a session's worktree and its local branch, and nothing that is not
123
+ a session. Run it from outside that worktree. It refuses a session with uncommitted changes or with commits
124
+ its remote branch lacks, and that refusal is real: stop and look rather than reaching for `--force`. A
125
+ session whose pull request merged at its current head counts as published, so a squash-merged session —
126
+ whose commits git's ancestry never finds on `__DEFAULT_BRANCH__` — is removed without forcing. It leaves the
127
+ remote branch and any pull request as they are.
128
+
129
+ To deliberately discard a session instead — abandoning unmerged work — `ctxr session discard <session>
130
+ --force`. State plainly, before running it, that this destroys any uncommitted or unpushed work in that
131
+ worktree; it needs its own explicit go, distinct from the published-and-clean case above. Add
132
+ `--close-pull-request` only when the operator also wants its open pull request closed.