handoff-mcp-server 0.20.0 → 0.22.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/Cargo.lock +52 -11
- package/Cargo.toml +2 -1
- package/README.md +31 -2
- package/package.json +1 -1
- package/skills/handoff/SKILL.md +24 -0
- package/skills/handoff-docs/SKILL.md +233 -0
- package/skills/handoff-load/SKILL.md +10 -2
- package/skills/handoff-memory/SKILL.md +20 -0
- package/src/context/injection.rs +239 -0
- package/src/context/mod.rs +129 -0
- package/src/lib.rs +1 -0
- package/src/mcp/handlers/docs.rs +1043 -0
- package/src/mcp/handlers/docs_query.rs +1335 -0
- package/src/mcp/handlers/get_task.rs +9 -0
- package/src/mcp/handlers/import_context.rs +1 -0
- package/src/mcp/handlers/memory.rs +33 -53
- package/src/mcp/handlers/mod.rs +13 -0
- package/src/mcp/handlers/update_task.rs +2 -0
- package/src/mcp/tools.rs +170 -0
- package/src/storage/docs/mod.rs +478 -0
- package/src/storage/docs/model.rs +537 -0
- package/src/storage/docs/reassemble.rs +167 -0
- package/src/storage/docs/split.rs +377 -0
- package/src/storage/mod.rs +1 -0
- package/src/storage/tasks.rs +131 -0
package/Cargo.lock
CHANGED
|
@@ -37,9 +37,9 @@ checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
|
|
37
37
|
|
|
38
38
|
[[package]]
|
|
39
39
|
name = "cc"
|
|
40
|
-
version = "1.2.
|
|
40
|
+
version = "1.2.67"
|
|
41
41
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
42
|
-
checksum = "
|
|
42
|
+
checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38"
|
|
43
43
|
dependencies = [
|
|
44
44
|
"find-msvc-tools",
|
|
45
45
|
"shlex",
|
|
@@ -123,6 +123,15 @@ dependencies = [
|
|
|
123
123
|
"slab",
|
|
124
124
|
]
|
|
125
125
|
|
|
126
|
+
[[package]]
|
|
127
|
+
name = "getopts"
|
|
128
|
+
version = "0.2.24"
|
|
129
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
130
|
+
checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df"
|
|
131
|
+
dependencies = [
|
|
132
|
+
"unicode-width",
|
|
133
|
+
]
|
|
134
|
+
|
|
126
135
|
[[package]]
|
|
127
136
|
name = "getrandom"
|
|
128
137
|
version = "0.4.3"
|
|
@@ -136,11 +145,12 @@ dependencies = [
|
|
|
136
145
|
|
|
137
146
|
[[package]]
|
|
138
147
|
name = "handoff-mcp"
|
|
139
|
-
version = "0.
|
|
148
|
+
version = "0.22.0"
|
|
140
149
|
dependencies = [
|
|
141
150
|
"anyhow",
|
|
142
151
|
"chrono",
|
|
143
152
|
"lexsim",
|
|
153
|
+
"pulldown-cmark",
|
|
144
154
|
"serde",
|
|
145
155
|
"serde_json",
|
|
146
156
|
"tempfile",
|
|
@@ -208,9 +218,9 @@ dependencies = [
|
|
|
208
218
|
|
|
209
219
|
[[package]]
|
|
210
220
|
name = "lexsim"
|
|
211
|
-
version = "0.
|
|
221
|
+
version = "0.6.0"
|
|
212
222
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
213
|
-
checksum = "
|
|
223
|
+
checksum = "b64f48de8c9d203c0ed3ac8c68e9ba1072206d5072551c08889a4bbae9d84677"
|
|
214
224
|
dependencies = [
|
|
215
225
|
"unicode-normalization",
|
|
216
226
|
"unicode-segmentation",
|
|
@@ -236,9 +246,9 @@ checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
|
|
236
246
|
|
|
237
247
|
[[package]]
|
|
238
248
|
name = "memchr"
|
|
239
|
-
version = "2.8.
|
|
249
|
+
version = "2.8.3"
|
|
240
250
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
241
|
-
checksum = "
|
|
251
|
+
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
|
242
252
|
|
|
243
253
|
[[package]]
|
|
244
254
|
name = "num-traits"
|
|
@@ -270,6 +280,25 @@ dependencies = [
|
|
|
270
280
|
"unicode-ident",
|
|
271
281
|
]
|
|
272
282
|
|
|
283
|
+
[[package]]
|
|
284
|
+
name = "pulldown-cmark"
|
|
285
|
+
version = "0.13.4"
|
|
286
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
287
|
+
checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e"
|
|
288
|
+
dependencies = [
|
|
289
|
+
"bitflags",
|
|
290
|
+
"getopts",
|
|
291
|
+
"memchr",
|
|
292
|
+
"pulldown-cmark-escape",
|
|
293
|
+
"unicase",
|
|
294
|
+
]
|
|
295
|
+
|
|
296
|
+
[[package]]
|
|
297
|
+
name = "pulldown-cmark-escape"
|
|
298
|
+
version = "0.11.0"
|
|
299
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
300
|
+
checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae"
|
|
301
|
+
|
|
273
302
|
[[package]]
|
|
274
303
|
name = "quote"
|
|
275
304
|
version = "1.0.46"
|
|
@@ -300,9 +329,9 @@ dependencies = [
|
|
|
300
329
|
|
|
301
330
|
[[package]]
|
|
302
331
|
name = "rustversion"
|
|
303
|
-
version = "1.0.
|
|
332
|
+
version = "1.0.23"
|
|
304
333
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
305
|
-
checksum = "
|
|
334
|
+
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
|
306
335
|
|
|
307
336
|
[[package]]
|
|
308
337
|
name = "serde"
|
|
@@ -415,9 +444,9 @@ dependencies = [
|
|
|
415
444
|
|
|
416
445
|
[[package]]
|
|
417
446
|
name = "tinyvec"
|
|
418
|
-
version = "1.
|
|
447
|
+
version = "1.12.0"
|
|
419
448
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
420
|
-
checksum = "
|
|
449
|
+
checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f"
|
|
421
450
|
dependencies = [
|
|
422
451
|
"tinyvec_macros",
|
|
423
452
|
]
|
|
@@ -469,6 +498,12 @@ version = "0.1.2"
|
|
|
469
498
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
470
499
|
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
|
471
500
|
|
|
501
|
+
[[package]]
|
|
502
|
+
name = "unicase"
|
|
503
|
+
version = "2.9.0"
|
|
504
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
505
|
+
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
|
|
506
|
+
|
|
472
507
|
[[package]]
|
|
473
508
|
name = "unicode-ident"
|
|
474
509
|
version = "1.0.24"
|
|
@@ -490,6 +525,12 @@ version = "1.13.3"
|
|
|
490
525
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
491
526
|
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
|
|
492
527
|
|
|
528
|
+
[[package]]
|
|
529
|
+
name = "unicode-width"
|
|
530
|
+
version = "0.2.2"
|
|
531
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
532
|
+
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
|
533
|
+
|
|
493
534
|
[[package]]
|
|
494
535
|
name = "wasm-bindgen"
|
|
495
536
|
version = "0.2.126"
|
package/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "handoff-mcp"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.22.0"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
description = "MCP server that gives AI coding agents persistent memory across sessions"
|
|
6
6
|
license = "MIT"
|
|
@@ -21,6 +21,7 @@ chrono = { version = "0.4", features = ["serde"] }
|
|
|
21
21
|
anyhow = "1"
|
|
22
22
|
thiserror = "2"
|
|
23
23
|
lexsim = ">=0.1.0, <1.0.0"
|
|
24
|
+
pulldown-cmark = "0.13"
|
|
24
25
|
|
|
25
26
|
[dev-dependencies]
|
|
26
27
|
tempfile = "3.27.0"
|
package/README.md
CHANGED
|
@@ -419,6 +419,26 @@ rename) so a concurrent reader never sees a partially-written file.
|
|
|
419
419
|
For usage best practices (granularity, scope_paths, conflict handling, cleanup), see `skills/handoff-memory/SKILL.md`.
|
|
420
420
|
See [Project Memory](#project-memory-1) below for what it is and how to wire automatic injection.
|
|
421
421
|
|
|
422
|
+
### Document Management
|
|
423
|
+
|
|
424
|
+
| Tool | Purpose |
|
|
425
|
+
|------|---------|
|
|
426
|
+
| `handoff_doc_save` | Create or update a document (auto-splits into fragments) |
|
|
427
|
+
| `handoff_doc_get` | Read a document — full, meta, or single fragment |
|
|
428
|
+
| `handoff_doc_list` | List/search documents with BM25 and filters |
|
|
429
|
+
| `handoff_doc_delete` | Delete a document and its fragments |
|
|
430
|
+
| `handoff_doc_reassemble` | Reconstruct original Markdown from fragments |
|
|
431
|
+
| `handoff_doc_tree` | Walk family tree (ancestors/descendants/related) |
|
|
432
|
+
| `handoff_doc_query` | Context injection — staged full/outline, hook-driven |
|
|
433
|
+
| `handoff_doc_analyze` | Read-only heuristic scan (import step 1) |
|
|
434
|
+
| `handoff_doc_import` | Atomic bulk write after analysis (import step 3) |
|
|
435
|
+
|
|
436
|
+
Documents live in `.handoff/docs/` (`_doc.*.json` metadata + `_frag.*.{json,md}`
|
|
437
|
+
fragments). Large Markdown is split into fragments on save; `handoff_doc_reassemble`
|
|
438
|
+
reconstructs the original with drift detection, and `handoff_doc_query` feeds
|
|
439
|
+
staged (outline-first, then full-text) context to the agent — the same mechanism
|
|
440
|
+
that powers the hook-driven injection described below.
|
|
441
|
+
|
|
422
442
|
### Task Data Model
|
|
423
443
|
|
|
424
444
|
Tasks are stored as a directory tree with status encoded in filenames:
|
|
@@ -584,14 +604,23 @@ what the agent intends, call `handoff_memory_query`, and inject the matching mem
|
|
|
584
604
|
the same memory is **not injected twice in one session** — and an *edited* memory
|
|
585
605
|
(new content hash) is re-injected.
|
|
586
606
|
|
|
607
|
+
The same hooks also call `handoff_doc_query`, so relevant documents (saved via
|
|
608
|
+
`handoff_doc_save`) are staged into context alongside memories — outline first,
|
|
609
|
+
then full text as relevance/budget allows.
|
|
610
|
+
|
|
587
611
|
| Event | Calls | Effect |
|
|
588
612
|
|-------|-------|--------|
|
|
589
|
-
| `UserPromptSubmit` | `handoff_memory_query` (prompt text) | Inject memories relevant to the prompt |
|
|
590
|
-
| `PreToolUse` (`Edit\|Write\|MultiEdit`) | `handoff_memory_query` (file path) | Inject memories scoped to the file being edited |
|
|
613
|
+
| `UserPromptSubmit` | `handoff_memory_query` (prompt text), `handoff_doc_query` (prompt text) | Inject memories and documents relevant to the prompt |
|
|
614
|
+
| `PreToolUse` (`Edit\|Write\|MultiEdit`) | `handoff_memory_query` (file path), `handoff_doc_query` (file path) | Inject memories and documents scoped to the file being edited |
|
|
591
615
|
|
|
592
616
|
`handoff_memory_cleanup` (merge exact duplicates, gc old sidecars) is not wired
|
|
593
617
|
to a hook — call it manually or from a CLI/cron job when you want housekeeping.
|
|
594
618
|
|
|
619
|
+
> The bundled `plugin-hooks/hooks/hooks.json` (used by the "Handoff MCP — Memory
|
|
620
|
+
> & Document Hooks" plugin) already wires both `handoff_memory_query` and
|
|
621
|
+
> `handoff_doc_query` on `UserPromptSubmit` and `PreToolUse`; the JSON examples
|
|
622
|
+
> below show the equivalent hand-written config for non-plugin setups.
|
|
623
|
+
|
|
595
624
|
> **Wire hooks in your *user/global* settings, not in the repo.** Hooks are a
|
|
596
625
|
> personal workflow choice; the handoff-mcp repo does not ship a `.claude/`
|
|
597
626
|
> hooks config, and you should not commit one into a shared project. Put the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "handoff-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "MCP server that gives AI coding agents persistent memory across sessions",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "AlphaElements <66808803+alphaelements@users.noreply.github.com>",
|
package/skills/handoff/SKILL.md
CHANGED
|
@@ -239,6 +239,30 @@ Use `handoff_bulk_update_tasks` for:
|
|
|
239
239
|
when moving a task out of `blocked`/`skipped`. Parent tasks and the statuses
|
|
240
240
|
`blocked`/`skipped` are exempt.
|
|
241
241
|
|
|
242
|
+
### Document Management
|
|
243
|
+
|
|
244
|
+
For structured, multi-section documents (specs, designs, ADRs, guides) that
|
|
245
|
+
are too large for a single memory entry, use the doc tools instead — see the
|
|
246
|
+
`handoff-docs` skill for full parameter details and workflows:
|
|
247
|
+
|
|
248
|
+
| Tool | Purpose |
|
|
249
|
+
|---|---|
|
|
250
|
+
| `handoff_doc_save` | Create/update a document — auto-splits `body` into fragments at `##` (configurable via `split_level`) |
|
|
251
|
+
| `handoff_doc_get` | Read a document — `full` (reassembled), `meta` (manifest only), or `fragment` (one section) |
|
|
252
|
+
| `handoff_doc_list` | List/search documents (BM25), filter by `doc_type`/`tags`/`task_id` |
|
|
253
|
+
| `handoff_doc_delete` | Delete a document and its fragments (also unlinks it from tasks) |
|
|
254
|
+
| `handoff_doc_reassemble` | Reconstruct the original Markdown from fragments, with drift detection |
|
|
255
|
+
| `handoff_doc_tree` | Traverse the family tree — parent/child (structural) + related (semantic: `supersedes`/`references`/`implements`/`extends`/`conflicts`) |
|
|
256
|
+
| `handoff_doc_query` | Context injection (hook-driven) — staged `full`/`outline` results by fragment size |
|
|
257
|
+
| `handoff_doc_analyze` | Read-only heuristic scan of a file/directory — step 1 of importing existing docs |
|
|
258
|
+
| `handoff_doc_import` | Atomic bulk write of analyzed + AI-reviewed documents — step 3 of importing existing docs |
|
|
259
|
+
|
|
260
|
+
`handoff_doc_save(task_ids: [...])` creates a **bidirectional** doc↔task
|
|
261
|
+
link: the document gains a `task_ids` entry and each linked task gains a
|
|
262
|
+
`TaskLink` in its `task_links`. Look it up from either side with
|
|
263
|
+
`handoff_doc_list(task_id: ...)` (docs linked to a task) or
|
|
264
|
+
`handoff_get_task(id: ...)` (inspect `task_links` on the task record).
|
|
265
|
+
|
|
242
266
|
### Configuration Management
|
|
243
267
|
|
|
244
268
|
Use `handoff_update_config` to manage project settings via dot-notation keys:
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: handoff-docs
|
|
3
|
+
description: "Document management — save, read, search, import, and traverse structured project documents (specs, designs, ADRs, guides, notes). Triggers on 'ドキュメント保存', '仕様書を管理', '設計書をインポート', 'save this doc', 'import specs', 'document management', or when the user asks to persist/organize/search multi-section markdown that is too structured for a single memory entry."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Handoff Docs Skill
|
|
7
|
+
|
|
8
|
+
## When to use
|
|
9
|
+
|
|
10
|
+
- The user asks to save a spec, design doc, ADR, guide, or note so it survives across sessions
|
|
11
|
+
- The user wants to import an existing pile of Markdown files (e.g. a `wiki/` or `specs/` directory) into structured document management
|
|
12
|
+
- You need to read a specific section of a large document without pulling the whole thing into context
|
|
13
|
+
- You need to trace how documents relate to each other (parent/child hierarchy, or semantic links like "this design implements that spec")
|
|
14
|
+
- A document should be linked to one or more tasks so it surfaces automatically while that task is being worked on
|
|
15
|
+
|
|
16
|
+
If the knowledge is a short, standalone lesson/rule/convention/gotcha (< 1 page,
|
|
17
|
+
no internal sections), use `handoff-memory` instead — see "Memory vs Documents"
|
|
18
|
+
in `skills/handoff-memory/SKILL.md` for the boundary.
|
|
19
|
+
|
|
20
|
+
## The 9 Doc Tools
|
|
21
|
+
|
|
22
|
+
| Tool | Purpose |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `handoff_doc_save` | Create or update a document. Splits `body` into fragments automatically. |
|
|
25
|
+
| `handoff_doc_get` | Read a document — `full` (reassembled body), `meta` (manifest only), or `fragment` (one section). |
|
|
26
|
+
| `handoff_doc_list` | List/search documents (BM25 over title + fragment bodies), filter by `doc_type`, `tags`, `task_id`. |
|
|
27
|
+
| `handoff_doc_delete` | Delete a document and all its fragments; unlinks it from any linked tasks. |
|
|
28
|
+
| `handoff_doc_reassemble` | Reconstruct the original Markdown from fragments, with drift detection. |
|
|
29
|
+
| `handoff_doc_tree` | Walk the family tree (ancestors/descendants/related) for a document. |
|
|
30
|
+
| `handoff_doc_query` | Context injection — hook-driven, staged `full`/`outline` results ranked by relevance. |
|
|
31
|
+
| `handoff_doc_analyze` | Read-only heuristic scan of a file or directory — step 1 of the import flow. |
|
|
32
|
+
| `handoff_doc_import` | Atomic bulk write of analyzed + AI-reviewed documents — step 3 of the import flow. |
|
|
33
|
+
|
|
34
|
+
### `handoff_doc_save`
|
|
35
|
+
|
|
36
|
+
| Param | Required | Description |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| `title` | yes | Document title |
|
|
39
|
+
| `body` | yes | Full Markdown source — this is what gets split into fragments |
|
|
40
|
+
| `doc_type` | no | One of `spec`, `design`, `adr`, `guide`, `note` |
|
|
41
|
+
| `tags` | no | Free-form tags, folded into the BM25 index |
|
|
42
|
+
| `scope_paths` | no | Path prefixes this doc applies to — boosts relevance in `doc_query` when the matching file is being edited |
|
|
43
|
+
| `parent_id` | no | Places this document under a parent in the family tree |
|
|
44
|
+
| `related` | no | Array of `{ id, rel }` — semantic links to other documents (see Family Tree below) |
|
|
45
|
+
| `task_ids` | no | Task IDs to bidirectionally link (see Task Linking below) |
|
|
46
|
+
| `split_level` | no | ATX heading level to split on (default: `2`, i.e. `##`) |
|
|
47
|
+
| `auto_inject` | no | Injection hint: `auto` (default) \| `full` \| `outline` \| `none` |
|
|
48
|
+
| `doc_id` | no | Provide to update an existing document; omit to create a new one |
|
|
49
|
+
|
|
50
|
+
### `handoff_doc_get`
|
|
51
|
+
|
|
52
|
+
| Param | Required | Description |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| `doc_id` | yes | Document to read |
|
|
55
|
+
| `format` | no | `full` (default-equivalent reassembly) \| `meta` (manifest only, no body) \| `fragment` |
|
|
56
|
+
| `seq` | when `format=fragment` | Fragment sequence number to return |
|
|
57
|
+
|
|
58
|
+
Use `meta` when you only need to walk the graph (titles, tags, relations)
|
|
59
|
+
without paying the token cost of fragment bodies. Use `fragment` with a `seq`
|
|
60
|
+
from an outline injection (see Staged Injection) to fetch exactly the section
|
|
61
|
+
you need.
|
|
62
|
+
|
|
63
|
+
### `handoff_doc_list`
|
|
64
|
+
|
|
65
|
+
| Param | Required | Description |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| `query` | no | BM25 search over title + fragment bodies |
|
|
68
|
+
| `doc_type` | no | Filter by type |
|
|
69
|
+
| `tags` | no | Filter by tags |
|
|
70
|
+
| `task_id` | no | Documents linked to this task |
|
|
71
|
+
| `include_body` | no | Default `false` — metadata-only listing |
|
|
72
|
+
|
|
73
|
+
### `handoff_doc_delete`
|
|
74
|
+
|
|
75
|
+
| Param | Required | Description |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| `doc_id` | yes | Document to delete, along with all its fragments |
|
|
78
|
+
|
|
79
|
+
Deleting also removes the document from any linked task's `task_links`.
|
|
80
|
+
|
|
81
|
+
### `handoff_doc_reassemble`
|
|
82
|
+
|
|
83
|
+
| Param | Required | Description |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| `doc_id` | yes | Document to reconstruct |
|
|
86
|
+
| `output_path` | no | If set, also writes the reassembled Markdown to this path |
|
|
87
|
+
|
|
88
|
+
Fragments are concatenated in `seq` order with original heading markers
|
|
89
|
+
preserved — `save(body) → reassemble()` is byte-identical. If a fragment was
|
|
90
|
+
edited directly after the split, its `content_hash` no longer matches and
|
|
91
|
+
`reassemble` reports the drift instead of silently returning stale content.
|
|
92
|
+
|
|
93
|
+
### `handoff_doc_tree`
|
|
94
|
+
|
|
95
|
+
| Param | Required | Description |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| `doc_id` | yes | Root of the traversal |
|
|
98
|
+
| `depth` | no | How many parent/child levels to return |
|
|
99
|
+
| `include_related` | no | Whether to also include semantically `related` documents |
|
|
100
|
+
|
|
101
|
+
### `handoff_doc_query`
|
|
102
|
+
|
|
103
|
+
| Param | Required | Description |
|
|
104
|
+
|---|---|---|
|
|
105
|
+
| `text` | no | Prompt/query text (BM25 relevance ranking) |
|
|
106
|
+
| `file_paths` | no | Files being worked on — boosts documents whose `scope_paths` prefix-match |
|
|
107
|
+
| `task_id` | no | Boosts documents linked to this task |
|
|
108
|
+
| `session_id` | no | Enables per-session dedup — a fragment already injected this session is skipped unless its content changed |
|
|
109
|
+
| `limit` | no | Max fragments to return |
|
|
110
|
+
| `mark_injected` | no | Whether to record injection for dedup (default `true`) |
|
|
111
|
+
| `suppress_doc_ids` | no | Document ids to exclude entirely from this call's results |
|
|
112
|
+
| `suppress_until_changed` | no | With `suppress_doc_ids` and `session_id`: persists the suppression in the session sidecar so those documents stay excluded from future calls until their `content_hash` changes (default `false`) |
|
|
113
|
+
|
|
114
|
+
This is the hook-driven tool — see Staged Injection below for how results are shaped.
|
|
115
|
+
|
|
116
|
+
### `handoff_doc_analyze`
|
|
117
|
+
|
|
118
|
+
| Param | Required | Description |
|
|
119
|
+
|---|---|---|
|
|
120
|
+
| `path` | yes | File or directory to scan |
|
|
121
|
+
| `recursive` | no | Recurse into subdirectories |
|
|
122
|
+
| `flatten` | no | Skip hierarchy inference (no `parent_id` guessing) |
|
|
123
|
+
|
|
124
|
+
Read-only — writes nothing. Returns a conditioning report with
|
|
125
|
+
`auto_resolved` entries (high-confidence `doc_type`/`tags`/`scope_paths`) and
|
|
126
|
+
`needs_review` entries (broken links, missing relationships, near-duplicates)
|
|
127
|
+
each carrying a concrete `suggestion` the AI can approve, edit, or reject.
|
|
128
|
+
|
|
129
|
+
### `handoff_doc_import`
|
|
130
|
+
|
|
131
|
+
| Param | Required | Description |
|
|
132
|
+
|---|---|---|
|
|
133
|
+
| `analyzed` | yes | The `handoff_doc_analyze` output (possibly after AI review) |
|
|
134
|
+
| `overrides` | no | Per-file corrections (`doc_type`, relationship resolutions, etc.) |
|
|
135
|
+
| `task_ids` | no | Link every imported document to these tasks |
|
|
136
|
+
|
|
137
|
+
Writes all `_doc.*.json` + `_frag.*.{json,md}` atomically in one transaction,
|
|
138
|
+
including any task links — matches the "validate whole tree, then write"
|
|
139
|
+
pattern used by `handoff_import_context` for tasks.
|
|
140
|
+
|
|
141
|
+
## Staged Injection (outline vs full)
|
|
142
|
+
|
|
143
|
+
`handoff_doc_query` avoids flooding context with large documents:
|
|
144
|
+
|
|
145
|
+
| Mode | When | What's injected |
|
|
146
|
+
|---|---|---|
|
|
147
|
+
| `full` | Fragment body <= `doc_inline_threshold` tokens (default 300) | Metadata + the entire fragment body |
|
|
148
|
+
| `outline` | Fragment body > threshold | Metadata + heading list only — no body. Read a specific section with `handoff_doc_get(format="fragment", seq=N)` |
|
|
149
|
+
|
|
150
|
+
This means short documents (ADRs, conventions, short notes) get pulled in
|
|
151
|
+
ready-to-use, while long documents (full specs, design docs) only announce
|
|
152
|
+
their structure — the AI decides which section is actually worth fetching.
|
|
153
|
+
|
|
154
|
+
### `auto_inject` override
|
|
155
|
+
|
|
156
|
+
Set on `handoff_doc_save` (or later via an update) to force behavior
|
|
157
|
+
regardless of size:
|
|
158
|
+
|
|
159
|
+
| Value | Effect |
|
|
160
|
+
|---|---|
|
|
161
|
+
| `auto` (default) | Size-based automatic choice between `full`/`outline` |
|
|
162
|
+
| `full` | Always inject the full body |
|
|
163
|
+
| `outline` | Always inject headings only, even if small |
|
|
164
|
+
| `none` | Never auto-inject — only surfaced via explicit `handoff_doc_get` |
|
|
165
|
+
|
|
166
|
+
## Family Tree
|
|
167
|
+
|
|
168
|
+
Two distinct kinds of document relationship:
|
|
169
|
+
|
|
170
|
+
- **parent/child** (`parent_id`) — structural hierarchy, e.g. a directory of
|
|
171
|
+
specs where each file's document is a child of the directory's document.
|
|
172
|
+
- **related** (`related: [{ id, rel }]`) — semantic links between documents
|
|
173
|
+
that are not structurally nested. `rel` is one of:
|
|
174
|
+
|
|
175
|
+
| `rel` | Meaning |
|
|
176
|
+
|---|---|
|
|
177
|
+
| `supersedes` | Replaces the target (a version bump) |
|
|
178
|
+
| `references` | Loose cross-reference |
|
|
179
|
+
| `implements` | This document implements the spec the target describes |
|
|
180
|
+
| `extends` | Extends the target without replacing it |
|
|
181
|
+
| `conflicts` | Known contradiction that needs resolving |
|
|
182
|
+
|
|
183
|
+
Use `handoff_doc_tree` to walk both kinds together (`include_related: true`)
|
|
184
|
+
or just the structural hierarchy.
|
|
185
|
+
|
|
186
|
+
## Task Linking
|
|
187
|
+
|
|
188
|
+
`handoff_doc_save(task_ids: [...])` creates a **bidirectional** link:
|
|
189
|
+
|
|
190
|
+
1. The document's own `task_ids` field is set.
|
|
191
|
+
2. Each linked task gets a `TaskLink { target: doc_id, link_type: "doc", label: <doc title> }` entry in its `task_links`.
|
|
192
|
+
3. Deleting the document removes it from the linked tasks' `task_links` automatically.
|
|
193
|
+
|
|
194
|
+
Look up the relationship from either side:
|
|
195
|
+
- `handoff_doc_list(task_id: "T-79")` — documents linked to a task.
|
|
196
|
+
- `handoff_get_task(task_id: "T-79")` — inspect `task_links` on the task record to
|
|
197
|
+
see which documents (and other targets) it links to.
|
|
198
|
+
|
|
199
|
+
Note: there is no dedicated `doc_id` filter on `handoff_list_tasks` — the
|
|
200
|
+
`task_links` field is populated and readable per-task via `handoff_get_task`,
|
|
201
|
+
but a document → "which tasks link to me" listing must be done by scanning
|
|
202
|
+
`task_links` yourself, not via a built-in filter.
|
|
203
|
+
|
|
204
|
+
## Fragment Granularity
|
|
205
|
+
|
|
206
|
+
- Default split boundary: ATX heading level 2 (`##`).
|
|
207
|
+
- Override per-call with `split_level` on `handoff_doc_save` (e.g. `1` to
|
|
208
|
+
split only on `#`, or `3` for finer-grained `###` sections).
|
|
209
|
+
- Content before the first qualifying heading becomes fragment `seq: 0` (the
|
|
210
|
+
preamble). Nested headings below `split_level` stay inside their parent
|
|
211
|
+
fragment rather than becoming their own fragment.
|
|
212
|
+
|
|
213
|
+
## Import Workflow (3 steps)
|
|
214
|
+
|
|
215
|
+
Use this instead of calling `doc_save` file-by-file when bringing in an
|
|
216
|
+
existing pile of Markdown — cross-document relationships and duplicates can
|
|
217
|
+
only be validated when every file is visible at once.
|
|
218
|
+
|
|
219
|
+
1. **`handoff_doc_analyze(path, recursive, flatten)`** — read-only scan.
|
|
220
|
+
Returns `auto_resolved` (confident guesses) and `needs_review` (broken
|
|
221
|
+
links, missing relationships, near-duplicates), each with a `suggestion`.
|
|
222
|
+
2. **AI reviews the report** — approve `auto_resolved` entries as-is, and for
|
|
223
|
+
each `needs_review` item either accept the `suggestion`, correct it, or
|
|
224
|
+
reject it. Nothing is written yet.
|
|
225
|
+
3. **`handoff_doc_import(analyzed, overrides, task_ids)`** — takes the
|
|
226
|
+
analyzed payload plus the AI's overrides and writes the whole tree
|
|
227
|
+
atomically, including task links.
|
|
228
|
+
|
|
229
|
+
## `doc_type` Values
|
|
230
|
+
|
|
231
|
+
`spec` (requirements/behavior contracts) · `design` (architecture/design
|
|
232
|
+
docs) · `adr` (architecture decision records) · `guide` (how-to/operational
|
|
233
|
+
docs) · `note` (fallback — anything that doesn't fit the above).
|
|
@@ -33,5 +33,13 @@ user-invocable: true
|
|
|
33
33
|
- **決定事項**: confidence が `unverified` のものを警告
|
|
34
34
|
- **申し送り**: `caution` を最初に、`context` → `suggestion` の順
|
|
35
35
|
- **コンテキストポインタ**: 次に読むべきファイル一覧
|
|
36
|
-
6.
|
|
37
|
-
|
|
36
|
+
6. **関連ドキュメントの確認**:
|
|
37
|
+
- `in_progress` タスクにリンクされたドキュメントがあれば、フックの
|
|
38
|
+
`handoff_doc_query`(`UserPromptSubmit`/`PreToolUse`)が自動注入する
|
|
39
|
+
— セッション開始時点で明示的に呼ぶ必要はない
|
|
40
|
+
- 特定のセクションだけ読みたい場合は `handoff_doc_get(doc_id, format="fragment", seq=N)`
|
|
41
|
+
を手動で呼ぶ(outline 注入で見出し一覧だけ渡された場合など)
|
|
42
|
+
- ドキュメント管理の詳細(段階的注入・家系図・インポート手順)は
|
|
43
|
+
`handoff-docs` スキルを参照
|
|
44
|
+
7. ユーザーに現状サマリーを日本語で報告
|
|
45
|
+
8. 次のアクションを提案(「何から始めますか?」)
|
|
@@ -137,3 +137,23 @@ JSON configuration.
|
|
|
137
137
|
|
|
138
138
|
Rule changes: discover via memory → codify in skills/CLAUDE.md. Memory is the
|
|
139
139
|
source of evidence; skills and CLAUDE.md are the source of authority.
|
|
140
|
+
|
|
141
|
+
## Memory vs Documents
|
|
142
|
+
|
|
143
|
+
| | Memory | Documents |
|
|
144
|
+
|---|---|---|
|
|
145
|
+
| Shape | Short, atomic lesson/rule/convention/gotcha | Structured, multi-section Markdown (specs, designs, ADRs, guides) |
|
|
146
|
+
| Storage | One flat entry (`.handoff/memory/`) | Split into fragments with a family tree (`.handoff/docs/`) |
|
|
147
|
+
| Relationships | None | parent/child (structural) + `related` (semantic: supersedes/references/implements/extends/conflicts) |
|
|
148
|
+
| Task linking | Via `scope_paths` boost only | Bidirectional `task_ids` ↔ `task_links` |
|
|
149
|
+
| Injection | Always inline (BM25 similarity) | Staged — `full` body for small fragments, `outline` (headings only) for large ones |
|
|
150
|
+
|
|
151
|
+
**Boundary rule**: if it is a standalone piece of knowledge that fits in a
|
|
152
|
+
paragraph or two (< 1 page, no internal sections), save it as a memory. If it
|
|
153
|
+
has structure — sections, a hierarchy, cross-references to other documents —
|
|
154
|
+
use document management (`handoff_doc_save`; see the `handoff-docs` skill).
|
|
155
|
+
|
|
156
|
+
There is **no migration path** between the two stores — they are independent.
|
|
157
|
+
If a memory grows into something that needs sections and cross-references,
|
|
158
|
+
recreate it as a document with `handoff_doc_save` and delete the memory with
|
|
159
|
+
`handoff_memory_delete` rather than expecting an automatic conversion.
|