takibibase 1.13.0 → 1.14.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 +11 -2
  2. package/package.json +1 -1
  3. package/takibi.mjs +59 -6
package/SKILL.md CHANGED
@@ -58,6 +58,14 @@ error hints.
58
58
  it: after the handoff you no longer hold the card, so a follow-up
59
59
  `status` call would be refused. You cannot release a card to nobody or
60
60
  hand on done/archived cards.
61
+ - `takibi tasks follow-up <id> --title "…"` — spin a fresh linked todo
62
+ off any card, including a finished one (`--body`, `--due <date>`,
63
+ `--assignee` for an exact board name or UUID — unknown names error
64
+ with the eligible list, and naming anyone other than yourself needs
65
+ Orchestrator). The new card lands in the source's project at To do,
66
+ unassigned unless `--assignee` names someone; the source keeps its
67
+ status, holder, and history, and nothing carries over. Never move a
68
+ done card back to To do to continue it — follow up instead.
61
69
  - `@name` in a task names the handoff target: when your part is
62
70
  finished, run `tasks handoff <id> name` (usually with `--review
63
71
  --note`). The mention itself assigns nothing — the handoff does.
@@ -155,12 +163,13 @@ abstains honestly instead of answering halfway.
155
163
  - Notes export/keep/remove need owner approval already given in the
156
164
  conversation. Export only creates a draft; verify it before adding
157
165
  content to Sources.
158
- - Task claim/status/handoff/artifact/escalation writes only with owner approval
166
+ - Task claim/status/handoff/artifact/escalation/follow-up writes only with owner approval
159
167
  already given in conversation. Claim-first: a plain key must claim a card before
160
168
  moving or touching it; only orchestrators/owners accept (`review→done`),
161
169
  assign or release cards they do not hold, or archive. A holder may hand
162
170
  its own card to another eligible profile (`tasks handoff`). Title/body edits need the create cap —
163
- claim-only keys can drive a card but cannot rewrite its text.
171
+ claim-only keys can drive a card but cannot rewrite its text. Follow-ups need
172
+ the create cap too, and any granted creator may spin one off a card it does not hold.
164
173
  - Boards need a whole-collection grant: folder-only or tag-only keys 403
165
174
  on every task route (the full-collection server message says so verbatim
166
175
  when the key holds the required cap; keys lacking the cap get the generic
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "takibibase",
3
- "version": "1.13.0",
3
+ "version": "1.14.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
@@ -37,7 +37,7 @@ import { homedir } from 'node:os';
37
37
  import { basename, join } from 'node:path';
38
38
 
39
39
  /** Baked fallback; the published package re-reads package.json next door. */
40
- const BAKED_VERSION = '1.13.0';
40
+ const BAKED_VERSION = '1.14.0';
41
41
  const CLI_INFO = (() => {
42
42
  try {
43
43
  const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'));
@@ -364,6 +364,7 @@ Commands:
364
364
  tasks claim <id> Claim an open to-do card (atomic, one winner)
365
365
  tasks status <id> <st> Move a card: todo | in_progress | review | done [--note <text>]
366
366
  tasks handoff <id> <profile> Hand a card you hold to another profile [--review --note <text>]
367
+ tasks follow-up <id> Spin a fresh linked todo off any card [--title/--body/--assignee/--due]
367
368
  tasks escalate <id> Ask the Founder for help on a held card [--reason <text>]
368
369
  tasks deescalate <id> Clear your escalation ("never mind")
369
370
  tasks artifact add <taskId> <url> Attach a link artifact [--note/--size/--hash]
@@ -403,6 +404,8 @@ Per-command options:
403
404
  tasks handoff: <profile> is a name or UUID on this board; --review submits your work
404
405
  for review in the same call, --note <text> leaves the review note with it
405
406
  tasks escalate: --reason <text> (required, max 500 chars — what do you need from the Founder?)
407
+ tasks follow-up: --title <text> (required, max 300 chars), --body <text> (max 20000 chars),
408
+ --assignee <name-or-uuid>, --due <date>; the source card is never modified
406
409
  tasks get: sent-back cards carry previousReviewNote, the last handoff note
407
410
  tasks artifact add <url>: --note <text> --size <bytes> --hash <hex> (links only)
408
411
  tasks artifact add --markdown <file|->: --title <text> --note <text> (content from file or stdin -, max 50000 chars; title max 140 chars)
@@ -429,7 +432,7 @@ to probe the API and \`takibi projects\` to see this key's scope
429
432
  Agents: \`takibi skill --install\` puts the takibi-use skill where
430
433
  assistants look for it.
431
434
 
432
- Reads are free. Export stamps included notes. Task claim/status/handoff/artifact/escalation writes and doc uploads
435
+ Reads are free. Export stamps included notes. Task claim/status/handoff/artifact/escalation/follow-up writes and doc uploads
433
436
  need owner approval in conversation (uploads also need the Uploads grant on the key).
434
437
  Account-only routes (delete, retry, download originals, PATCH docs/projects)
435
438
  are never the agent's to call — ask the account owner.
@@ -469,7 +472,7 @@ function parseArgv(argv) {
469
472
  i = ni;
470
473
  }
471
474
  else if (a.startsWith('--folder=')) globals.folder = a.slice('--folder='.length);
472
- 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 === '--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') {
475
+ 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 === '--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') {
473
476
  rest.push(a);
474
477
  if (a !== '--all' && a !== '--review' && a !== '--install' && a !== '--force' && a !== '--agents' && a !== '--claude' && a !== '--codex') {
475
478
  const [v, ni] = takeValue(a, i);
@@ -548,6 +551,7 @@ const taskLine = (t) => {
548
551
  if (t.escalationReason) bits.push(`· escalated: ${t.escalationReason}`);
549
552
  if (t.blocked) bits.push(`· blocked: ${t.blockedReason || 'no reason given'}`);
550
553
  if (t.artifacts?.length) bits.push(`· ${t.artifacts.length} artifact${t.artifacts.length === 1 ? '' : 's'}`);
554
+ if (t.followups?.length) bits.push(`· ${t.followups.length} follow-up${t.followups.length === 1 ? '' : 's'}`);
551
555
  if (t.archivedAt) bits.push('· archived');
552
556
  if (t.dueAt) bits.push(`· due ${t.dueAt}`);
553
557
  return bits.join(' ');
@@ -581,6 +585,11 @@ function renderTask(t) {
581
585
  if (t.reviewNote) out(`review: ${t.reviewNote}`);
582
586
  if (t.previousReviewNote) out(`previous review note: ${t.previousReviewNote}`);
583
587
  if (t.body) out(`\n${t.body}`);
588
+ if (t.followsUpOn) out(`follow-up of: ${t.followsUpOn.title} [${t.followsUpOn.id}]`);
589
+ if (t.followups?.length) {
590
+ out('follow-ups:');
591
+ for (const f of t.followups) out(` - ${f.title} · ${f.status}${f.assigneeName ? ` · ${f.assigneeName}` : ''}${f.archivedAt ? ' · archived' : ''} [${f.id}]`);
592
+ }
584
593
  if (t.artifacts?.length) {
585
594
  out('');
586
595
  for (const a of t.artifacts) out(` - ${artifactRow(a)}`);
@@ -677,13 +686,13 @@ async function listTasks(ctx, includeArchived) {
677
686
  * eligibility); names resolve exactly against GET /v1/tasks/assignees for
678
687
  * this board — same exact-match-or-error rule as --project names.
679
688
  */
680
- async function resolveAssignee(value, ctx) {
689
+ async function resolveAssignee(value, ctx, label = 'tasks handoff') {
681
690
  const v = String(value).trim();
682
691
  if (UUID_RE.test(v)) return { id: v };
683
692
  const data = await api('GET', '/v1/tasks/assignees', {
684
693
  ...ctx,
685
694
  query: qparams([['projectId', ctx.projectId]]),
686
- label: 'tasks handoff',
695
+ label,
687
696
  });
688
697
  const eligible = data?.assignees ?? [];
689
698
  const matches = eligible.filter((a) => a.name === v);
@@ -917,7 +926,51 @@ async function cmdTasks(tokens, globals) {
917
926
  else renderTask(data.task);
918
927
  return;
919
928
  }
920
- throw usageError(`Unknown tasks command ${JSON.stringify(sub)}. Use list | get | claim | status | handoff | escalate | deescalate | artifact add | artifact show.`);
929
+ if (sub === 'follow-up') {
930
+ const usage = 'Usage: takibi tasks follow-up <id> --title <text> [--body <text>] [--assignee <name-or-uuid>] [--due <date>]';
931
+ const [id, ...flagTokens] = rest;
932
+ if (!id || id.startsWith('--')) throw usageError(usage);
933
+ let title;
934
+ let body;
935
+ let assignee;
936
+ let due;
937
+ for (let i = 0; i < flagTokens.length; i++) {
938
+ const f = flagTokens[i];
939
+ if (f === '--title') title = flagTokens[++i];
940
+ else if (f === '--body') body = flagTokens[++i];
941
+ else if (f === '--assignee') assignee = flagTokens[++i];
942
+ else if (f === '--due') due = flagTokens[++i];
943
+ else throw usageError(`Unexpected ${JSON.stringify(f)}. ${usage}`);
944
+ }
945
+ // Validate everything before touching the key or network, mirroring
946
+ // the server limits so failures name the flag instead of 400ing.
947
+ if (title === undefined || title.trim() === '') throw usageError('--title needs non-empty text.');
948
+ if (title.length > 300) throw usageError('--title is max 300 chars.');
949
+ if (body !== undefined && body.length > 20000) throw usageError('--body is max 20000 chars.');
950
+ if (due !== undefined && due.trim() === '') throw usageError('--due needs a date.');
951
+ if (assignee !== undefined && String(assignee).trim() === '') throw usageError('--assignee needs a profile name or UUID.');
952
+ // No --project: the endpoint scopes by the source card's own project.
953
+ const ctx = await ctxFor(globals, { needKey: true, resolveProject: false });
954
+ if (assignee !== undefined) {
955
+ const v = String(assignee).trim();
956
+ if (!UUID_RE.test(v)) {
957
+ // Names resolve against the source card's own board.
958
+ const src = await api('GET', `/v1/tasks/${id}`, { ...ctx, label: 'tasks follow-up' });
959
+ assignee = (await resolveAssignee(v, { ...ctx, projectId: src.task.projectId }, 'tasks follow-up')).id;
960
+ } else {
961
+ assignee = v;
962
+ }
963
+ }
964
+ const data = await api('POST', `/v1/tasks/${id}/follow-ups`, {
965
+ ...ctx,
966
+ body: { title: title.trim(), body, assigneeProfileId: assignee, dueAt: due },
967
+ label: 'tasks follow-up',
968
+ });
969
+ if (ctx.json) out(JSON.stringify(data, null, 2));
970
+ else renderTask(data.task);
971
+ return;
972
+ }
973
+ throw usageError(`Unknown tasks command ${JSON.stringify(sub)}. Use list | get | claim | status | handoff | follow-up | escalate | deescalate | artifact add | artifact show.`);
921
974
  }
922
975
 
923
976
  async function cmdDoc(tokens, globals) {