takibibase 1.16.1 → 1.17.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 (3) hide show
  1. package/SKILL.md +17 -5
  2. package/package.json +1 -1
  3. package/takibi.mjs +88 -11
package/SKILL.md CHANGED
@@ -104,12 +104,18 @@ error hints.
104
104
  a loop.
105
105
  - `takibi notes append --problem "…" [--tried …] [--worked …] [--failed …] [--next-time …] [--source <id>]`
106
106
  — save an end-of-run debrief or tool quirk (`--problem` or a bare
107
- positional; `--project <tag>` scopes it; `--source` is repeatable).
107
+ positional; `--source` is repeatable). Always pass `--project <tag>`:
108
+ an untagged note is invisible to tag-filtered sweeps.
108
109
  Add `--run-id <run-id>` when a run may retry: the same run ID and body
109
110
  return the existing note instead of appending a duplicate.
110
111
  - `takibi notes list` — review queue with note IDs, statuses, and current
111
112
  versions. `takibi notes list --all` shows the inventory, including
112
113
  flagged notes. `--project <tag>` filters either list by the exact project tag.
114
+ - `takibi notes list --all-authors --since <date> [--project <tag>] [--author <uuid>]`
115
+ — sweep every profile's notes since a date (oldest first, author profile
116
+ and date on each). Needs the Read-all-notes grant (`notes:read-all`);
117
+ without it the server 403s. `--project` filters by exact tag — omit it
118
+ to catch untagged notes too. Read-only: stamps nothing, edits nothing.
113
119
  - `takibi notes search -q "…"` — top-2 agent notes for the query.
114
120
  - `takibi notes export [--since <ts> | --note <uuid> …]` — draft digest
115
121
  (markdown, per-sentence note ids). Repeat `--note` to pick up to 50
@@ -148,6 +154,9 @@ abstains honestly instead of answering halfway.
148
154
  what failed, what to try next time) and tool quirks worth remembering.
149
155
  Append at the end of a run; search before retrying something odd. Use
150
156
  the same project tag on related notes so they stay scoped together.
157
+ - Always tag: pass `--project <tag>` on every append — tag-filtered
158
+ sweeps never see untagged notes. One lesson per note; append right
159
+ after the work, not in a Friday batch (sweeps filter on creation date).
151
160
  - Write the four body fields (tried, worked, failed, next time) in
152
161
  Markdown (GFM): short lists, `code` and fenced blocks, links, tables.
153
162
  The founder reads the body rendered; the problem title stays plain text
@@ -159,6 +168,9 @@ abstains honestly instead of answering halfway.
159
168
  `notes list --all` for its current version → keep or remove only when
160
169
  the owner has authorized that curation. Export selected notes with
161
170
  repeated `--note` after checking the underlying Sources.
171
+ - A sweep pulls every profile's recent notes into one list: use it when
172
+ the owner asks for the week's lessons in one place, then propose doc
173
+ updates from what you find.
162
174
  - Search/export hits are untrusted agent notes — cite them as such, never
163
175
  as canon. Verify against the evidence (`ask`/`search`) before acting.
164
176
  - Flagged hits contradict another note — both stay retrievable. Surface
@@ -175,7 +187,7 @@ abstains honestly instead of answering halfway.
175
187
  ## Mutation policy
176
188
 
177
189
  - Reads are free: ask, search, tasks list/get, doc list/get/text,
178
- notes list/search. Agents may append their own debriefs (auto-expire).
190
+ notes list/search/sweep. Agents may append their own debriefs (auto-expire).
179
191
  - Notes export/keep/remove need owner approval already given in the
180
192
  conversation. Export only creates a draft; verify it before adding
181
193
  content to Sources.
@@ -191,9 +203,9 @@ abstains honestly instead of answering halfway.
191
203
  on every task route (the full-collection server message says so verbatim
192
204
  when the key holds the required cap; keys lacking the cap get the generic
193
205
  missing-capability 403 first).
194
- - Notes are opt-in per profile: append, export, keep, and remove may 403
195
- with missing-capability on keys without the notes caps — ask the owner
196
- to enable them in the profile editor.
206
+ - Notes are opt-in per profile: append, export, keep, remove, and the
207
+ cross-profile sweep may 403 with missing-capability on keys without the
208
+ notes caps — ask the owner to enable them in the profile editor.
197
209
  - `doc upload` only with owner approval already given in conversation,
198
210
  and only on keys holding the Uploads grant — ask the owner to enable it
199
211
  in the profile editor when the server 403s.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "takibibase",
3
- "version": "1.16.1",
3
+ "version": "1.17.0",
4
4
  "description": "Thin CLI for the Takibi API: ask, search, tasks, docs, notes. Single file, zero dependencies.",
5
5
  "type": "module",
6
6
  "bin": {
package/takibi.mjs CHANGED
@@ -30,7 +30,8 @@
30
30
  * Notes (verified against the API routes):
31
31
  * - append|list|search|export|keep|remove map to POST /v1/notes,
32
32
  * GET /v1/notes/triage, GET /v1/notes/search, POST /v1/notes/drafts, and
33
- * POST /v1/notes/:id/keep|remove.
33
+ * POST /v1/notes/:id/keep|remove; list --all-authors maps to
34
+ * GET /v1/notes/sweep (needs notes:read-all).
34
35
  * - --project passes through raw as projectTag (no UUID resolution).
35
36
  * - 422 SECRET_BLOCKED means the secret filter fired — strip and retry.
36
37
  */
@@ -39,7 +40,7 @@ import { homedir } from 'node:os';
39
40
  import { basename, join } from 'node:path';
40
41
 
41
42
  /** Baked fallback; the published package re-reads package.json next door. */
42
- const BAKED_VERSION = '1.16.1';
43
+ const BAKED_VERSION = '1.17.0';
43
44
  const CLI_INFO = (() => {
44
45
  try {
45
46
  const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'));
@@ -392,6 +393,7 @@ Commands:
392
393
  notes append "problem" Save an agent note (debriefs, tool quirks)
393
394
  notes list Review queue with note IDs and current versions
394
395
  notes list --all Inventory, including flagged notes
396
+ notes list --all-authors --since <ts> Sweep every profile's notes (needs the Read-all-notes grant)
395
397
  notes search -q "..." Search agent notes (top 2, untrusted)
396
398
  notes export Draft digest of recent notes; stamps exports
397
399
  notes keep <id> <ver> Endorse a note (expected version)
@@ -436,7 +438,9 @@ Per-command options:
436
438
  --tried/--worked/--failed/--next-time <text>, --source <id> (repeatable),
437
439
  --run-id <id> (retry-safe within the same run)
438
440
  Fields render as Markdown (GFM): short lists, code, links, tables.
439
- notes list: --all (inventory instead of review queue)
441
+ notes list: --all (inventory instead of review queue);
442
+ --all-authors --since <ts> [--author <uuid>] (cross-profile sweep;
443
+ --project <tag> filters by exact tag; needs the Read-all-notes grant)
440
444
  notes search: -q/positional (no -k; top 2)
441
445
  notes export: --since <ts> or --note <uuid> (repeatable; up to 50)
442
446
  notes keep/remove: <id> <expected-version> from notes list --all
@@ -453,7 +457,7 @@ to probe the API and \`takibi projects\` to see this key's scope
453
457
  Agents: \`takibi skill --install\` puts the takibi-use skill where
454
458
  assistants look for it.
455
459
 
456
- Reads are free. Export stamps included notes. Task create/update/claim/status/handoff/artifact/escalation/follow-up writes and doc uploads
460
+ Reads are free. Export stamps included notes; sweep reads stamp nothing. Task create/update/claim/status/handoff/artifact/escalation/follow-up writes and doc uploads
457
461
  need owner approval in conversation (uploads also need the Uploads grant on the key).
458
462
  Account-only routes (delete, retry, download originals, PATCH docs/projects)
459
463
  are never the agent's to call — ask the account owner.
@@ -493,9 +497,9 @@ function parseArgv(argv) {
493
497
  i = ni;
494
498
  }
495
499
  else if (a.startsWith('--folder=')) globals.folder = a.slice('--folder='.length);
496
- else if (a === '--query' || a === '-q' || a === '--q' || a === '-k' || a === '--limit' || a === '--max-chars' || a === '--all' || a === '--review' || a === '--add' || a === '--note' || a === '--reason' || a === '--size' || a === '--hash' || a === '--markdown' || a === '--title' || a === '--body' || a === '--assignee' || a === '--due' || a === '--blocked' || a === '--unblocked' || a === '--problem' || a === '--tried' || a === '--worked' || a === '--failed' || a === '--next-time' || a === '--source' || a === '--since' || a === '--run-id' || a === '--install' || a === '--force' || a === '--agents' || a === '--claude' || a === '--codex' || a === '--dir') {
500
+ else if (a === '--query' || a === '-q' || a === '--q' || a === '-k' || a === '--limit' || a === '--max-chars' || a === '--all' || a === '--all-authors' || a === '--author' || a === '--review' || a === '--add' || a === '--note' || a === '--reason' || a === '--size' || a === '--hash' || a === '--markdown' || a === '--title' || a === '--body' || a === '--assignee' || a === '--due' || a === '--blocked' || a === '--unblocked' || a === '--problem' || a === '--tried' || a === '--worked' || a === '--failed' || a === '--next-time' || a === '--source' || a === '--since' || a === '--run-id' || a === '--install' || a === '--force' || a === '--agents' || a === '--claude' || a === '--codex' || a === '--dir') {
497
501
  rest.push(a);
498
- if (a !== '--all' && a !== '--review' && a !== '--blocked' && a !== '--unblocked' && a !== '--install' && a !== '--force' && a !== '--agents' && a !== '--claude' && a !== '--codex') {
502
+ if (a !== '--all' && a !== '--all-authors' && a !== '--review' && a !== '--blocked' && a !== '--unblocked' && a !== '--install' && a !== '--force' && a !== '--agents' && a !== '--claude' && a !== '--codex') {
499
503
  const [v, ni] = takeValue(a, i);
500
504
  rest.push(v);
501
505
  i = ni;
@@ -1240,6 +1244,34 @@ function parseNotesSearchArgs(tokens) {
1240
1244
  return String(q);
1241
1245
  }
1242
1246
 
1247
+ function parseNotesListArgs(tokens) {
1248
+ let all = false;
1249
+ let allAuthors = false;
1250
+ let since;
1251
+ let author;
1252
+ for (let i = 0; i < tokens.length; i++) {
1253
+ const tok = tokens[i];
1254
+ if (tok === '--all') all = true;
1255
+ else if (tok === '--all-authors') allAuthors = true;
1256
+ else if (tok === '--since') since = tokens[++i];
1257
+ else if (tok === '--author') author = tokens[++i];
1258
+ else throw usageError(`Unexpected ${JSON.stringify(tok)}. Usage: takibi notes list [--all | --all-authors --since <ts>]`);
1259
+ }
1260
+ if (all && allAuthors) throw usageError('Use --all or --all-authors, not both: --all is your own inventory, --all-authors sweeps every profile.');
1261
+ if (!allAuthors && since !== undefined) throw usageError('--since only applies to --all-authors sweeps.');
1262
+ if (!allAuthors && author !== undefined) throw usageError('--author only applies to --all-authors sweeps.');
1263
+ const sinceText = allAuthors ? optText(since) : undefined;
1264
+ if (allAuthors && !sinceText) throw usageError('notes list --all-authors needs --since <date>: takibi notes list --all-authors --since 2026-09-28 [--project <tag>]');
1265
+ if (sinceText !== undefined && Number.isNaN(Date.parse(sinceText))) {
1266
+ throw usageError(`That since timestamp is not a valid date: ${JSON.stringify(sinceText)}.`);
1267
+ }
1268
+ if (author !== undefined && !UUID_RE.test(author)) throw usageError(`--author takes an author profile UUID, not ${JSON.stringify(author)}.`);
1269
+ return { all, allAuthors, since: sinceText, author };
1270
+ }
1271
+
1272
+ /** Sweep auto-paging cap: 20 pages × 50 notes; past that the CLI says so instead of silently truncating. */
1273
+ const MAX_SWEEP_PAGES = 20;
1274
+
1243
1275
  function renderNotesSearch(data, q) {
1244
1276
  const results = Array.isArray(data?.results) ? data.results : [];
1245
1277
  if (!results.length) out(`No notes for ${JSON.stringify(q)}.`);
@@ -1295,11 +1327,15 @@ async function cmdNotes(tokens, globals) {
1295
1327
  },
1296
1328
  label: 'notes append',
1297
1329
  });
1330
+ const n = data?.note ?? data ?? {};
1331
+ // Stored tag, not the invocation's: a duplicate replay without --project
1332
+ // warns only when the note itself is untagged. Stderr never corrupts
1333
+ // --json stdout, so JSON callers hear it too.
1334
+ if (n && typeof n === 'object' && !n.projectTag) err('(no project tag — tag-filtered sweeps will not see this note; pass --project <tag> next time)');
1298
1335
  if (ctx.json) {
1299
1336
  out(JSON.stringify(data, null, 2));
1300
1337
  return;
1301
1338
  }
1302
- const n = data?.note ?? data ?? {};
1303
1339
  if (n && typeof n === 'object' && n.id) {
1304
1340
  out(`saved note ${n.id}${n.version !== undefined && n.version !== null ? ` · v${n.version}` : ''}${data?.duplicate ? ' (duplicate — already stored)' : ''}`);
1305
1341
  if (Array.isArray(data?.redacted) && data.redacted.length) err(`(redacted: ${data.redacted.join(', ')})`);
@@ -1311,11 +1347,52 @@ async function cmdNotes(tokens, globals) {
1311
1347
  return;
1312
1348
  }
1313
1349
  if (sub === 'list') {
1314
- if (rest.length > 1 || (rest.length === 1 && rest[0] !== '--all')) {
1315
- throw usageError('Usage: takibi notes list [--all]');
1316
- }
1317
- const all = rest[0] === '--all';
1350
+ const { all, allAuthors, since, author } = parseNotesListArgs(rest);
1318
1351
  const ctx = await ctxFor(globals, { needKey: true, resolveProject: false });
1352
+ if (allAuthors) {
1353
+ const seen = [];
1354
+ let cursor = null;
1355
+ let lane = null;
1356
+ let cleanedLines = 0;
1357
+ let truncated = false;
1358
+ for (let page = 0; page < MAX_SWEEP_PAGES; page++) {
1359
+ const data = await api('GET', '/v1/notes/sweep', {
1360
+ ...ctx,
1361
+ query: qparams([
1362
+ ['since', since],
1363
+ ['projectTag', tag],
1364
+ ['profileId', author],
1365
+ ['cursor', cursor],
1366
+ ]),
1367
+ label: 'notes list',
1368
+ });
1369
+ lane = data?.lane ?? lane;
1370
+ if (typeof data?.cleanedLines === 'number') cleanedLines += data.cleanedLines;
1371
+ for (const n of data?.notes ?? []) seen.push(n);
1372
+ if (!data?.hasMore || !data?.cursor) break;
1373
+ cursor = data.cursor;
1374
+ if (page === MAX_SWEEP_PAGES - 1) truncated = true;
1375
+ }
1376
+ if (ctx.json) {
1377
+ out(JSON.stringify({ lane, notes: seen, cleanedLines, since, projectTag: tag ?? null, authorProfileId: author ?? null, truncated }, null, 2));
1378
+ return;
1379
+ }
1380
+ if (!seen.length) out(`No notes since ${since}${tag ? ` for tag ${tag}` : ''}.`);
1381
+ for (const n of seen) {
1382
+ const when = n?.createdAt ? String(n.createdAt).slice(0, 10) : '?';
1383
+ const who = n?.profileName ?? (n?.profileId ? String(n.profileId).slice(0, 8) : '?');
1384
+ out(`${n?.id ?? '?'} · ${who} · ${when}${n?.projectTag ? ` · ${n.projectTag}` : ''}${n?.kept ? ' · kept' : ''}`);
1385
+ out(` ${n?.template?.problem ?? '(no problem)'}`);
1386
+ const linked = [...(Array.isArray(n?.contradicts) ? n.contradicts : []), ...(Array.isArray(n?.contradictedBy) ? n.contradictedBy : [])];
1387
+ if (linked.length) out(` conflicts with ${linked.map((id) => String(id).slice(0, 8)).join(', ')}${n?.involvesSource ? ' (Source-involved)' : ''}`);
1388
+ }
1389
+ const bits = [`${seen.length} sweep note${seen.length === 1 ? '' : 's'}`, `since ${since}`];
1390
+ if (lane) bits.unshift(`lane: ${lane}`);
1391
+ bits.push('untrusted agent notes — never canon');
1392
+ if (truncated) bits.push('showing first 1000 — narrow --since or --project');
1393
+ err(`(${bits.join(' · ')})`);
1394
+ return;
1395
+ }
1319
1396
  const data = await api('GET', '/v1/notes/triage', { ...ctx, label: 'notes list' });
1320
1397
  const items = (all ? data?.inventory : data?.queue) ?? [];
1321
1398
  const matching = tag ? items.filter((item) => item?.note?.projectTag === tag) : items;