ctxr-cli 0.32.0 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/version.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Kept in sync with package.json's "version" field. */
2
- export declare const CLI_VERSION = "0.32.0";
2
+ export declare const CLI_VERSION = "0.33.0";
3
3
  /**
4
4
  * The npm package this executable is published as. Bound here, beside the
5
5
  * version, because the release check resolves the latest published release by
package/dist/version.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Kept in sync with package.json's "version" field. */
2
- export const CLI_VERSION = '0.32.0';
2
+ export const CLI_VERSION = '0.33.0';
3
3
  /**
4
4
  * The npm package this executable is published as. Bound here, beside the
5
5
  * version, because the release check resolves the latest published release by
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ctxr-cli",
3
- "version": "0.32.0",
3
+ "version": "0.33.0",
4
4
  "description": "Context store for AI coding agents — markdown and git, catalog and graph retrieval, no vector database.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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".