@mmerterden/multi-agent-pipeline 16.25.0 → 16.27.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/CHANGELOG.md +77 -0
- package/README.md +1 -1
- package/README.tr.md +1 -1
- package/install/templates/claude-hooks.json +32 -1
- package/package.json +1 -1
- package/pipeline/commands/multi-agent/help/SKILL.md +2 -0
- package/pipeline/commands/multi-agent/refactor/SKILL.md +23 -1
- package/pipeline/commands/multi-agent/search/SKILL.md +28 -0
- package/pipeline/commands/multi-agent/setup/SKILL.md +18 -44
- package/pipeline/commands/multi-agent/status/SKILL.md +9 -0
- package/pipeline/lib/credential-inventory.sh +142 -18
- package/pipeline/lib/fetch-crashlytics.sh +123 -28
- package/pipeline/multi-agent-refs/features/url-enrichment.md +1 -1
- package/pipeline/multi-agent-refs/features/visual-evidence.md +5 -0
- package/pipeline/multi-agent-refs/keychain.md +65 -20
- package/pipeline/multi-agent-refs/knowledge.md +27 -0
- package/pipeline/multi-agent-refs/phases/operations.md +7 -1
- package/pipeline/multi-agent-refs/phases/phase-1-analysis.md +3 -1
- package/pipeline/multi-agent-refs/phases/phase-4-review.md +1 -8
- package/pipeline/multi-agent-refs/phases/phase-7-report.md +11 -21
- package/pipeline/multi-agent-refs/picker-contract.md +1 -1
- package/pipeline/multi-agent-refs/refactor/observations.md +81 -0
- package/pipeline/multi-agent-refs/setup/firebase.md +151 -0
- package/pipeline/schemas/learnings-ledger.schema.json +5 -0
- package/pipeline/schemas/prefs.schema.json +31 -3
- package/pipeline/schemas/skill-observation.schema.json +73 -0
- package/pipeline/scripts/capture-flush.sh +158 -0
- package/pipeline/scripts/capture-resume.sh +87 -0
- package/pipeline/scripts/crush-json.mjs +283 -0
- package/pipeline/scripts/firebase-app-discovery.sh +114 -0
- package/pipeline/scripts/keychain-save.sh +5 -8
- package/pipeline/scripts/keychain.py +76 -14
- package/pipeline/scripts/learn-from-transcripts.mjs +625 -0
- package/pipeline/scripts/learning-curve.mjs +22 -4
- package/pipeline/scripts/learnings-ledger.mjs +86 -12
- package/pipeline/scripts/note-session.sh +187 -0
- package/pipeline/scripts/observations.mjs +347 -0
- package/pipeline/scripts/offload-ref.sh +45 -2
- package/pipeline/scripts/pre-commit-check.sh +31 -1
- package/pipeline/scripts/scan-agent-config.sh +12 -3
- package/pipeline/scripts/skill-siblings.mjs +187 -0
- package/pipeline/scripts/triage-memory.mjs +73 -9
- package/pipeline/skills/.skill-manifest.json +1 -1
- package/pipeline/skills/shared/core/multi-agent-refactor/SKILL.md +23 -1
- package/pipeline/skills/shared/core/multi-agent-search/SKILL.md +28 -0
- package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +37 -6
- package/pipeline/skills/shared/core/multi-agent-status/SKILL.md +9 -0
|
@@ -25,6 +25,11 @@
|
|
|
25
25
|
* query --issue "<text>" [--file-glob "Auth*"] [--top N]
|
|
26
26
|
* [--repo-slug <slug>] [--json]
|
|
27
27
|
* show --id <id> [--repo-slug <slug>]
|
|
28
|
+
* timeline --anchor <id> [--before N] [--after N] [--repo-slug <slug>]
|
|
29
|
+
* What surrounded a finding when it was recorded. The corpus is
|
|
30
|
+
* append-only and written in order, so line adjacency is
|
|
31
|
+
* chronological adjacency - the neighbours came from the same
|
|
32
|
+
* review pass, and are usually what makes an old row legible.
|
|
28
33
|
* Print the full row behind a pointer emitted by query/profile.
|
|
29
34
|
* path [--repo-slug <slug>] Print the resolved corpus file path
|
|
30
35
|
* stats [--repo-slug <slug>] Print corpus row counts grouped by classification
|
|
@@ -295,8 +300,11 @@ function ingest() {
|
|
|
295
300
|
}
|
|
296
301
|
}
|
|
297
302
|
process.stdout.write(JSON.stringify({ ok: true, slug, ingested: written }) + "\n");
|
|
298
|
-
|
|
299
|
-
|
|
303
|
+
// exitCode, not exit(): even this one-line write is on the same rule, and a
|
|
304
|
+
// rule with an exception is the one that gets copied into the next branch.
|
|
305
|
+
// 2 = nothing new, which callers treat as an idempotent success.
|
|
306
|
+
process.exitCode = written === 0 ? 2 : 0;
|
|
307
|
+
return;
|
|
300
308
|
}
|
|
301
309
|
|
|
302
310
|
function query() {
|
|
@@ -308,7 +316,8 @@ function query() {
|
|
|
308
316
|
const corpus = readCorpus(slug);
|
|
309
317
|
if (corpus.length === 0) {
|
|
310
318
|
process.stdout.write(JSON.stringify({ ok: true, slug, hits: [], total: 0 }) + "\n");
|
|
311
|
-
process.
|
|
319
|
+
process.exitCode = 2;
|
|
320
|
+
return;
|
|
312
321
|
}
|
|
313
322
|
const qTerms = tokens(issueText);
|
|
314
323
|
const fileRe = fileGlob ? globToRegExp(fileGlob) : null;
|
|
@@ -380,7 +389,8 @@ function query() {
|
|
|
380
389
|
process.stdout.write(
|
|
381
390
|
JSON.stringify({ ok: true, slug, total: filtered.length, hits: ranked }) + "\n",
|
|
382
391
|
);
|
|
383
|
-
process.
|
|
392
|
+
process.exitCode = 0;
|
|
393
|
+
return;
|
|
384
394
|
}
|
|
385
395
|
|
|
386
396
|
// Drill-down: a compact prior-art line carries only its id, so the agent that
|
|
@@ -394,17 +404,65 @@ function showCmd() {
|
|
|
394
404
|
const row = readCorpus(slug).find((r) => rowId(r) === want);
|
|
395
405
|
if (!row) {
|
|
396
406
|
process.stdout.write(JSON.stringify({ ok: false, slug, id: want, found: false }) + "\n");
|
|
397
|
-
process.
|
|
407
|
+
process.exitCode = 2;
|
|
408
|
+
return;
|
|
398
409
|
}
|
|
399
410
|
process.stdout.write(JSON.stringify({ ok: true, slug, id: want, row }) + "\n");
|
|
400
|
-
process.
|
|
411
|
+
process.exitCode = 0;
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Neighbourhood: what surrounded a finding when it was recorded.
|
|
417
|
+
*
|
|
418
|
+
* Search answers "which rows match", and that is a different question from
|
|
419
|
+
* "what else happened around this one". A finding almost never travels alone -
|
|
420
|
+
* the same review pass produced the ones next to it, and those are often the
|
|
421
|
+
* context that makes it legible a year later. Without this, the only way to ask
|
|
422
|
+
* was to fetch the whole corpus and page through it, so nobody asked.
|
|
423
|
+
*
|
|
424
|
+
* The corpus is append-only and written in order, so line adjacency IS
|
|
425
|
+
* chronological adjacency. That is the whole implementation: no index, no
|
|
426
|
+
* timestamp arithmetic, no second data structure to keep in sync.
|
|
427
|
+
*/
|
|
428
|
+
function timelineCmd() {
|
|
429
|
+
const id = flags.anchor && flags.anchor !== true ? String(flags.anchor) : null;
|
|
430
|
+
if (!id) die("timeline needs --anchor <id>", 1);
|
|
431
|
+
const slug = flags["repo-slug"] ? String(flags["repo-slug"]) : repoSlug();
|
|
432
|
+
const before = flags.before && flags.before !== true ? Math.max(0, Number(flags.before)) : 3;
|
|
433
|
+
const after = flags.after && flags.after !== true ? Math.max(0, Number(flags.after)) : 3;
|
|
434
|
+
const want = id.startsWith("T:") ? id : `T:${id}`;
|
|
435
|
+
const corpus = readCorpus(slug);
|
|
436
|
+
const at = corpus.findIndex((r) => rowId(r) === want);
|
|
437
|
+
if (at === -1) {
|
|
438
|
+
process.stdout.write(JSON.stringify({ ok: false, slug, anchor: want, found: false }) + "\n");
|
|
439
|
+
process.exitCode = 2;
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
const from = Math.max(0, at - before);
|
|
443
|
+
const to = Math.min(corpus.length, at + after + 1);
|
|
444
|
+
const rows = corpus.slice(from, to).map((r, i) => ({
|
|
445
|
+
id: rowId(r),
|
|
446
|
+
offset: from + i - at,
|
|
447
|
+
task_id: r.task_id,
|
|
448
|
+
classification: r.classification,
|
|
449
|
+
severity: r.severity,
|
|
450
|
+
file: r.file,
|
|
451
|
+
issue: r.issue,
|
|
452
|
+
}));
|
|
453
|
+
process.stdout.write(
|
|
454
|
+
JSON.stringify({ ok: true, slug, anchor: want, before, after, rows }, null, 2) + "\n",
|
|
455
|
+
);
|
|
456
|
+
process.exitCode = 0;
|
|
457
|
+
return;
|
|
401
458
|
}
|
|
402
459
|
|
|
403
460
|
function pathCmd() {
|
|
404
461
|
process.stdout.write(
|
|
405
462
|
corpusPath(flags["repo-slug"] ? String(flags["repo-slug"]) : repoSlug()) + "\n",
|
|
406
463
|
);
|
|
407
|
-
process.
|
|
464
|
+
process.exitCode = 0;
|
|
465
|
+
return;
|
|
408
466
|
}
|
|
409
467
|
|
|
410
468
|
function statsCmd() {
|
|
@@ -424,7 +482,8 @@ function statsCmd() {
|
|
|
424
482
|
out.bytes = s.size;
|
|
425
483
|
}
|
|
426
484
|
process.stdout.write(JSON.stringify(out) + "\n");
|
|
427
|
-
process.
|
|
485
|
+
process.exitCode = 0;
|
|
486
|
+
return;
|
|
428
487
|
}
|
|
429
488
|
|
|
430
489
|
switch (SUB) {
|
|
@@ -437,6 +496,9 @@ switch (SUB) {
|
|
|
437
496
|
case "show":
|
|
438
497
|
showCmd();
|
|
439
498
|
break;
|
|
499
|
+
case "timeline":
|
|
500
|
+
timelineCmd();
|
|
501
|
+
break;
|
|
440
502
|
case "path":
|
|
441
503
|
pathCmd();
|
|
442
504
|
break;
|
|
@@ -444,6 +506,8 @@ switch (SUB) {
|
|
|
444
506
|
statsCmd();
|
|
445
507
|
break;
|
|
446
508
|
default:
|
|
447
|
-
process.stderr.write(
|
|
509
|
+
process.stderr.write(
|
|
510
|
+
"usage: triage-memory.mjs <ingest|query|show|timeline|path|stats> [flags]\n",
|
|
511
|
+
);
|
|
448
512
|
process.exit(64);
|
|
449
513
|
}
|
|
@@ -12,7 +12,7 @@ argument-hint: 'bugs | best-practices | drift | multi-agent-toolkit | run-errors
|
|
|
12
12
|
|
|
13
13
|
Deep-analyses the current project, extracts the global best-practices worth adopting (adapted to our stack), hunts real bugs and improvement areas, checks whether any skills we derived from an upstream source have drifted, researches the companion multi-agent-toolkit MCP server against current MCP practice, scores everything, drafts a single prioritized plan, asks the user for approval, applies the approved items, and asks about sync at the end.
|
|
14
14
|
|
|
15
|
-
**Input**: $ARGUMENTS (optional - area to focus on: "security", "performance", "tests", "bugs", "best-practices", "drift", "multi-agent-toolkit", etc.)
|
|
15
|
+
**Input**: $ARGUMENTS (optional - `backlog` runs Step 0e alone; otherwise an area to focus on: "security", "performance", "tests", "bugs", "best-practices", "drift", "multi-agent-toolkit", etc.)
|
|
16
16
|
|
|
17
17
|
## Flow
|
|
18
18
|
|
|
@@ -134,6 +134,14 @@ Each line is one run: `{ t, id, u, c, rp, ph, v, errs[] }`. The `errs[]` entries
|
|
|
134
134
|
|
|
135
135
|
This band mirrors the admin dashboard's "Gelişim alanları" panel, but reads the local ledger so it needs no auth and works offline. If `$ARGUMENTS` names a focus area, still read the ledger - a recurring run error in that area is the strongest possible signal.
|
|
136
136
|
|
|
137
|
+
Then read what the transcripts themselves record, which is the half no model noticed:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
node "$HOME/.claude/scripts/learn-from-transcripts.mjs" --json
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
It correlates rather than interprets - a failed read against the read that then worked, an identical invocation that kept failing, a search that was too narrow, a call the user refused twice, a file too big to read whole. Dry run by default; `--apply` writes them to the learnings ledger as `source: transcript-mining` and renders a marked block in `CLAUDE.local.md`. Zero candidates alongside a non-zero `toolResultsExamined` means there was nothing to find; zero of both means the read is broken.
|
|
144
|
+
|
|
137
145
|
Output (plan band F):
|
|
138
146
|
|
|
139
147
|
```
|
|
@@ -149,6 +157,20 @@ Rules for this band:
|
|
|
149
157
|
- Never quote a user's identity as blame. The `u` field is for counting distinct affected users, not for naming anyone in the plan.
|
|
150
158
|
- If the ledger is empty or absent, skip band F silently - it is additive signal, never a gate.
|
|
151
159
|
|
|
160
|
+
## Step 0e: BACKLOG - friction this pipeline already noticed
|
|
161
|
+
|
|
162
|
+
Every other band in this command derives its findings from scratch, which is right for a sweep and has one consequence nobody chose: a friction noticed mid-task is gone by the next run unless it was fixed within the hour. Most are not, because stopping mid-task to fix them is the wrong call. So they are written down when noticed and decided here.
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
node "$HOME/.claude/scripts/observations.mjs" scan --status open --json
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Exit 3 is `SCAN BROKEN` - files on disk, none parsed. That is a broken reader, not an empty queue: halt the band and say so rather than reporting "0 open".
|
|
169
|
+
|
|
170
|
+
Split every open observation into actionable / sibling-propagation / to-decline, and resolve the sibling surfaces mechanically with `skill-siblings.mjs`, never from memory - a command is authored in `commands/`, mirrored into `skills/shared/core/`, and installed again into three host trees, so a fix applied to one copy drifts from the rest with every gate still green. Output as plan band G, same table shape as the other bands.
|
|
171
|
+
|
|
172
|
+
`/multi-agent:refactor backlog` runs this band alone. The full sweep is unchanged and still includes it. Buckets, the status vocabulary, why `parked` needs a named unblocking event, and how to write an observation: `$HOME/.claude/multi-agent-refs/refactor/observations.md`.
|
|
173
|
+
|
|
152
174
|
## Step 1: SCAN
|
|
153
175
|
|
|
154
176
|
```
|
|
@@ -38,6 +38,34 @@ Text (default): colored, `task_id · project · branch · matches` + title + dat
|
|
|
38
38
|
|
|
39
39
|
JSON: `{ query, matched_tasks, tasks: [{ task_id, project, branch, date, matches, score, title, snippet }] }`.
|
|
40
40
|
|
|
41
|
+
## Four layers, and what each one costs
|
|
42
|
+
|
|
43
|
+
Retrieval is cheap or expensive depending on which layer answers, and the order
|
|
44
|
+
matters more than the query does. Filter first, fetch second - inverting these
|
|
45
|
+
is how a single lookup eats a phase's whole context budget.
|
|
46
|
+
|
|
47
|
+
| # | Call | Returns | Rough cost per hit |
|
|
48
|
+
|---|---|---|---|
|
|
49
|
+
| 1 | `search` / `--semantic` | pointers: id, file, one-line issue | ~50-100 tokens |
|
|
50
|
+
| 2 | `timeline --anchor <id>` | the rows recorded around one hit | ~50-100 tokens each |
|
|
51
|
+
| 3 | `show --id <id>` | one full row | ~500-1000 tokens |
|
|
52
|
+
| 4 | the offloaded artefact behind a `[[ref:<id>]]` | the raw payload | thousands |
|
|
53
|
+
|
|
54
|
+
Never start at layer 3 or 4. Narrow at layer 1, widen at layer 2 only around the
|
|
55
|
+
hits that survived, and fetch full rows only for the handful that are actually
|
|
56
|
+
going to be read. A `show` on twenty ids costs as much as the entire search that
|
|
57
|
+
found them and answers a question nobody asked.
|
|
58
|
+
|
|
59
|
+
Layer 2 is the one that gets forgotten. A finding rarely stands alone: the same
|
|
60
|
+
review pass produced its neighbours, and those are usually what makes an old row
|
|
61
|
+
legible. Both stores answer it, and the corpus is append-only so line adjacency
|
|
62
|
+
IS chronological adjacency:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
node "$HOME/.claude/scripts/triage-memory.mjs" timeline --anchor T:<id> --before 3 --after 3
|
|
66
|
+
node "$HOME/.claude/scripts/learnings-ledger.mjs" timeline --anchor L:<id> --before 3 --after 3
|
|
67
|
+
```
|
|
68
|
+
|
|
41
69
|
## Steps
|
|
42
70
|
|
|
43
71
|
1. Script:
|
|
@@ -227,7 +227,7 @@ Standard key names (used when creating NEW tokens). Discovery may find existing
|
|
|
227
227
|
| `figma` | `${USER}_Figma_Access_Token` | PAT | Figma Developer Settings (max 90 days) |
|
|
228
228
|
| `figma_mcp` | `${USER}_Figma_Mcp_Access_Token` | OAuth | Automatic via Claude Code Figma MCP remote auth |
|
|
229
229
|
| `fortify` | `${USER}_Fortify_Access_Token` | API Token | Fortify SSC -> Token Management (VPN required) |
|
|
230
|
-
| `firebase` | `${USER}_Firebase_Access_Json` | Firebase JSON
|
|
230
|
+
| `firebase` | `${USER}_Firebase_Access_Json` | Firebase service-account JSON | Firebase Console -> Project settings -> Service accounts -> Generate new private key. Store the file as issued; project id is read straight out of it. |
|
|
231
231
|
| `jenkins` | `${USER}_Jenkins_Access_Token` | API Token | Jenkins -> User -> Configure -> API Token |
|
|
232
232
|
| - | Git Identities | JSON (preferences) | Stored in `$HOME/.claude/multi-agent-preferences.json` - not Keychain |
|
|
233
233
|
|
|
@@ -243,14 +243,11 @@ pbcopy < /dev/null # clear clipboard
|
|
|
243
243
|
# Plain text (direct value)
|
|
244
244
|
~/.copilot/lib/credential-store.sh set "<SERVICE_NAME>" "<VALUE>"
|
|
245
245
|
|
|
246
|
-
# JSON file (
|
|
247
|
-
~/.copilot/lib/credential-store.sh set "<SERVICE_NAME>" "$(
|
|
246
|
+
# JSON file (stored as issued - the store round-trips newlines byte-exactly)
|
|
247
|
+
~/.copilot/lib/credential-store.sh set "<SERVICE_NAME>" "$(cat /path/to/file.json)"
|
|
248
248
|
|
|
249
249
|
# Read
|
|
250
250
|
~/.copilot/lib/credential-store.sh get "<SERVICE_NAME>"
|
|
251
|
-
|
|
252
|
-
# Decode JSON
|
|
253
|
-
~/.copilot/lib/credential-store.sh get "<SERVICE_NAME>" | base64 -d
|
|
254
251
|
```
|
|
255
252
|
|
|
256
253
|
For maximum stdin hygiene (the secret never lands on argv / shell history), use the Python helper directly with the `-` sentinel:
|
|
@@ -325,6 +322,40 @@ A credential that resolves but is rejected (401/403) follows the Expired-token
|
|
|
325
322
|
decision (Regenerate / Use a different token / Skip and continue) rather than
|
|
326
323
|
being silently dropped.
|
|
327
324
|
|
|
325
|
+
### Firebase / Crashlytics Access (optional, any platform)
|
|
326
|
+
|
|
327
|
+
Discovery maps a stored service-account JSON like any other credential, but it
|
|
328
|
+
cannot see the thing that decides whether Crashlytics is readable: a freshly
|
|
329
|
+
generated `firebase-adminsdk-*` account authenticates perfectly and holds no
|
|
330
|
+
Crashlytics role. Measure it, do not assume it:
|
|
331
|
+
|
|
332
|
+
```bash
|
|
333
|
+
bash "$HOME/.claude/lib/fetch-crashlytics.sh" --probe
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Ask (picker): **Complete tier 1** / **Set up tier 2** / **Skip**.
|
|
337
|
+
|
|
338
|
+
**Tier 1 - service account.** Store the JSON exactly as Firebase Console issued
|
|
339
|
+
it; it is multi-line and the store round-trips it byte for byte. Never base64 it -
|
|
340
|
+
a wrapper on the way in that nothing unwraps on the way out is how this path
|
|
341
|
+
stayed broken. When the probe says `tier-1-no-grant`, the fix is one line to an
|
|
342
|
+
Owner: grant `roles/firebasecrashlytics.viewer` on the project to that service
|
|
343
|
+
account. Read-only, and narrower than `roles/firebase.viewer`.
|
|
344
|
+
|
|
345
|
+
**Tier 2 - interactive session + MCP.** `firebase login` opens a browser and does
|
|
346
|
+
not work from inside an agent harness; the user runs it in their own terminal.
|
|
347
|
+
Both halves are required - a login with no MCP server registered reaches nothing.
|
|
348
|
+
It is the bridge while a role request moves, never the durable answer: autopilot
|
|
349
|
+
and url-enrichment run with nobody at the keyboard.
|
|
350
|
+
|
|
351
|
+
**appId discovery.** `scripts/firebase-app-discovery.sh <repo> --json` reads the
|
|
352
|
+
bundle-id / appId pairs the repo's `GoogleService-Info*.plist` and
|
|
353
|
+
`google-services.json` already carry, shaped for
|
|
354
|
+
`prefs.global.firebase.accounts[].apps[]`. Merging them saves the fetcher a
|
|
355
|
+
Management API round trip per run; skipping it changes nothing else.
|
|
356
|
+
|
|
357
|
+
Full mechanics and the v1alpha caveat: `multi-agent-refs/setup/firebase.md`.
|
|
358
|
+
|
|
328
359
|
### Security Rules
|
|
329
360
|
|
|
330
361
|
- Never paste tokens into terminal or chat - use `pbpaste` for input
|
|
@@ -36,6 +36,15 @@ Show every active and completed task as a table.
|
|
|
36
36
|
💡 log #1 | resume #N | kill #N
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
6. **What is waiting to be learned** (one line, only when non-zero):
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
node "$HOME/.claude/scripts/learn-from-transcripts.mjs" --json 2>/dev/null
|
|
43
|
+
node "$HOME/.claude/scripts/observations.mjs" scan --status open --json 2>/dev/null
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Report `N lesson(s) minable from transcripts - /multi-agent:refactor to review` and `N open pipeline observation(s)` when either count is above zero, and print nothing when both are zero. Neither runs a model and neither writes: the miner is dry-run by default. Zero candidates with a non-zero `toolResultsExamined` means nothing to find; zero of both means the read is broken, which is worth saying rather than reporting a clean queue.
|
|
47
|
+
|
|
39
48
|
5. **Short command suggestions** - Based on state:
|
|
40
49
|
- If there is a paused task → suggest `resume #N`
|
|
41
50
|
- If there is a done task → suggest `log #N`
|