handoff-mcp-server 0.19.1 → 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 +140 -4
- package/package.json +5 -2
- package/scripts/sync-plugin-skills.sh +23 -1
- package/scripts/sync-workflow-inline.sh +138 -0
- package/skills/handoff/SKILL.md +61 -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/bulk_update.rs +30 -2
- 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 +120 -13
- package/src/mcp/handlers/memory.rs +33 -53
- package/src/mcp/handlers/mod.rs +13 -0
- package/src/mcp/handlers/update_task.rs +22 -4
- package/src/mcp/tools.rs +185 -13
- 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 +197 -5
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
|
@@ -115,6 +115,75 @@ version. Note that `install-local.sh` **only rebuilds the binary and refreshes
|
|
|
115
115
|
the plugin cache** — it does not register the marketplace or enable the plugin,
|
|
116
116
|
so steps 2 and 3 are a one-time bootstrap.
|
|
117
117
|
|
|
118
|
+
### Updating
|
|
119
|
+
|
|
120
|
+
If you installed handoff-mcp as a plugin, it updates along **two independent
|
|
121
|
+
paths**, and you need both. The plugin does not bundle the binary:
|
|
122
|
+
`plugin.json` registers the MCP server as `command: "handoff-mcp"`, which
|
|
123
|
+
Claude Code resolves on your `PATH`. So the marketplace ships the skills and
|
|
124
|
+
the plugin manifest, while npm or cargo ships the executable that actually
|
|
125
|
+
implements the MCP tools.
|
|
126
|
+
|
|
127
|
+
(Installed without the plugin, via `cargo install` or `npm install -g` alone?
|
|
128
|
+
Then only step 1 and the restart apply.)
|
|
129
|
+
|
|
130
|
+
Updating only the plugin leaves you on the old MCP tools. Updating only the
|
|
131
|
+
binary leaves you on the old skills.
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
# 1. Binary — this is what implements the MCP tools
|
|
135
|
+
npm install -g handoff-mcp-server@latest
|
|
136
|
+
# or: cargo install handoff-mcp --force
|
|
137
|
+
|
|
138
|
+
# 2. Marketplace catalog — fetch the new version list
|
|
139
|
+
/plugin marketplace update handoff-mcp-marketplace
|
|
140
|
+
|
|
141
|
+
# 3. Plugin — skills and manifest
|
|
142
|
+
/plugin update handoff-mcp@handoff-mcp-marketplace
|
|
143
|
+
|
|
144
|
+
# 4. Restart Claude Code
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Update the optional plugins the same way if you installed them:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
/plugin update handoff-task-loop@handoff-mcp-marketplace
|
|
151
|
+
/plugin update handoff-mcp-hooks@handoff-mcp-marketplace
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Steps 1 and 3 update different things, and neither substitutes for the other.
|
|
155
|
+
`/plugin update` never touches the binary — it swaps the cached plugin directory,
|
|
156
|
+
which contains no executable at all — so no amount of restarting will give you
|
|
157
|
+
new MCP tools if you skipped step 1. Conversely, step 1 rewrites the file on disk
|
|
158
|
+
but the MCP server Claude Code already spawned keeps running the old image, so
|
|
159
|
+
you get the new tools only after step 4.
|
|
160
|
+
|
|
161
|
+
Step 4 is therefore not optional, and `/reload-plugins` is not a substitute: it
|
|
162
|
+
refreshes skills, not the MCP server process.
|
|
163
|
+
|
|
164
|
+
Do not expect step 1 to take effect on its own. An installer that overwrites a
|
|
165
|
+
*running* binary in place fails on Linux with `Text file busy`; installers that
|
|
166
|
+
replace the file instead (unlink, then create — what `install-local.sh` does)
|
|
167
|
+
succeed, and leave the already-running server executing the now-deleted old
|
|
168
|
+
image until it restarts. Either way, the new tools appear only after step 4.
|
|
169
|
+
|
|
170
|
+
Verify the update landed:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
which handoff-mcp # the binary Claude Code will actually run
|
|
174
|
+
handoff-mcp --version
|
|
175
|
+
claude plugin list # plugin version, per marketplace
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
`claude plugin list` reports the version you actually have installed. Note that
|
|
179
|
+
`claude plugin details` reads the marketplace source instead, so it shows the
|
|
180
|
+
version on offer whether or not you have updated to it — don't use it to
|
|
181
|
+
confirm an update.
|
|
182
|
+
|
|
183
|
+
**Local development checkout**: `./scripts/install-local.sh` does both halves at
|
|
184
|
+
once (rebuilds the binary into `~/.local/bin` and refreshes the plugin cache).
|
|
185
|
+
Restart Claude Code afterwards.
|
|
186
|
+
|
|
118
187
|
**Troubleshooting**
|
|
119
188
|
|
|
120
189
|
- **Plugin or skills don't show up** — run `/reload-plugins`, or restart Claude
|
|
@@ -123,6 +192,43 @@ so steps 2 and 3 are a one-time bootstrap.
|
|
|
123
192
|
`/plugin marketplace update handoff-mcp-marketplace`, then reinstall.
|
|
124
193
|
- **MCP server won't start** — open `/plugin` → **Errors** tab, and confirm the
|
|
125
194
|
binary is on your `PATH` (`which handoff-mcp`).
|
|
195
|
+
- **You updated, but the MCP tools still behave like the old version** — you
|
|
196
|
+
almost certainly updated the plugin without updating the binary, or you
|
|
197
|
+
updated the binary but did not restart Claude Code. Work through the four
|
|
198
|
+
steps above in order.
|
|
199
|
+
- **An older `handoff-mcp` earlier on your `PATH` shadows the new one.** This is
|
|
200
|
+
the most common cause of "I updated and nothing changed", because
|
|
201
|
+
`npm install -g` and `cargo install` write to *different* directories. List
|
|
202
|
+
every copy and see which one wins:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
type -a handoff-mcp # every match, in resolution order
|
|
206
|
+
for d in ${PATH//:/ }; do
|
|
207
|
+
[ -x "$d/handoff-mcp" ] && echo "$d -> $("$d/handoff-mcp" --version)"
|
|
208
|
+
done
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
The first line is the one Claude Code runs. Remove the stale copies (e.g.
|
|
212
|
+
`cargo uninstall handoff-mcp`, or delete the old file), or put the directory
|
|
213
|
+
holding the current binary earlier on your `PATH`.
|
|
214
|
+
|
|
215
|
+
- **`--version` says the right number but the behavior is old.** A version
|
|
216
|
+
string only changes when a release bumps it, so two builds of the *same*
|
|
217
|
+
version — a stale `cargo install` and a fresh one — report identically.
|
|
218
|
+
Compare the file itself rather than the version:
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
ls -l "$(which handoff-mcp)" # check the mtime
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
When in doubt, reinstall the binary and restart Claude Code.
|
|
225
|
+
|
|
226
|
+
- **Old versions pile up in the plugin cache.** Claude Code keeps each installed
|
|
227
|
+
version in its own directory under
|
|
228
|
+
`~/.claude/plugins/cache/handoff-mcp-marketplace/<plugin>/<version>/`. This is
|
|
229
|
+
harmless — the active version is recorded in
|
|
230
|
+
`~/.claude/plugins/installed_plugins.json` — but you can reclaim the space by
|
|
231
|
+
deleting the directories for versions you no longer use.
|
|
126
232
|
|
|
127
233
|
### cargo
|
|
128
234
|
|
|
@@ -313,6 +419,26 @@ rename) so a concurrent reader never sees a partially-written file.
|
|
|
313
419
|
For usage best practices (granularity, scope_paths, conflict handling, cleanup), see `skills/handoff-memory/SKILL.md`.
|
|
314
420
|
See [Project Memory](#project-memory-1) below for what it is and how to wire automatic injection.
|
|
315
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
|
+
|
|
316
442
|
### Task Data Model
|
|
317
443
|
|
|
318
444
|
Tasks are stored as a directory tree with status encoded in filenames:
|
|
@@ -478,14 +604,23 @@ what the agent intends, call `handoff_memory_query`, and inject the matching mem
|
|
|
478
604
|
the same memory is **not injected twice in one session** — and an *edited* memory
|
|
479
605
|
(new content hash) is re-injected.
|
|
480
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
|
+
|
|
481
611
|
| Event | Calls | Effect |
|
|
482
612
|
|-------|-------|--------|
|
|
483
|
-
| `UserPromptSubmit` | `handoff_memory_query` (prompt text) | Inject memories relevant to the prompt |
|
|
484
|
-
| `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 |
|
|
485
615
|
|
|
486
616
|
`handoff_memory_cleanup` (merge exact duplicates, gc old sidecars) is not wired
|
|
487
617
|
to a hook — call it manually or from a CLI/cron job when you want housekeeping.
|
|
488
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
|
+
|
|
489
624
|
> **Wire hooks in your *user/global* settings, not in the repo.** Hooks are a
|
|
490
625
|
> personal workflow choice; the handoff-mcp repo does not ship a `.claude/`
|
|
491
626
|
> hooks config, and you should not commit one into a shared project. Put the
|
|
@@ -569,8 +704,9 @@ If you already ran `handoff-mcp setup` before this change, migrate with
|
|
|
569
704
|
- **Manual edit**: open `~/.claude/settings.json` and delete the
|
|
570
705
|
`hooks.SessionStart` entry whose `tool` is `handoff_memory_cleanup` (remove
|
|
571
706
|
the whole `SessionStart` key if that was its only entry).
|
|
572
|
-
- **Plugin users**: `/plugin update
|
|
573
|
-
restart Claude Code.
|
|
707
|
+
- **Plugin users**: `/plugin update handoff-mcp-hooks@handoff-mcp-marketplace`
|
|
708
|
+
to pick up the new `hooks.json`, then restart Claude Code. This one ships
|
|
709
|
+
inside the plugin, so no binary update is needed.
|
|
574
710
|
|
|
575
711
|
Restart Claude Code after any of the above for the change to take effect.
|
|
576
712
|
|
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>",
|
|
@@ -21,7 +21,10 @@
|
|
|
21
21
|
"handoff-mcp": "./bin/handoff-mcp.js"
|
|
22
22
|
},
|
|
23
23
|
"scripts": {
|
|
24
|
-
"postinstall": "node scripts/postinstall.js"
|
|
24
|
+
"postinstall": "node scripts/postinstall.js",
|
|
25
|
+
"test:js": "node --test \"plugin-task-loop/workflows/lib/*.test.js\"",
|
|
26
|
+
"sync:workflow": "./scripts/sync-workflow-inline.sh",
|
|
27
|
+
"check:workflow": "./scripts/sync-workflow-inline.sh --check"
|
|
25
28
|
},
|
|
26
29
|
"files": [
|
|
27
30
|
"bin/handoff-mcp.js",
|
|
@@ -1,11 +1,33 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Sync skills/ -> plugin/skills/ for plugin distribution.
|
|
3
|
-
#
|
|
3
|
+
#
|
|
4
|
+
# plugin/skills/ is generated, but it is COMMITTED: the marketplace serves the
|
|
5
|
+
# plugin straight from the repository, so a skill that is not committed there
|
|
6
|
+
# never reaches an installed plugin. Run this after editing skills/, and commit
|
|
7
|
+
# the result.
|
|
8
|
+
#
|
|
9
|
+
# sync-plugin-skills.sh sync skills/ -> plugin/skills/
|
|
10
|
+
# sync-plugin-skills.sh --check exit non-zero if they differ (no writes)
|
|
4
11
|
set -euo pipefail
|
|
5
12
|
|
|
6
13
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
7
14
|
ROOT="$(dirname "$SCRIPT_DIR")"
|
|
8
15
|
|
|
16
|
+
if [ "${1:-}" = "--check" ]; then
|
|
17
|
+
if [ ! -d "$ROOT/plugin/skills" ]; then
|
|
18
|
+
echo "ERROR: plugin/skills/ is missing. Run './scripts/sync-plugin-skills.sh'." >&2
|
|
19
|
+
exit 1
|
|
20
|
+
fi
|
|
21
|
+
if ! diff -rq "$ROOT/skills/" "$ROOT/plugin/skills/" >/dev/null 2>&1; then
|
|
22
|
+
echo "ERROR: plugin/skills/ is out of sync with skills/:" >&2
|
|
23
|
+
diff -rq "$ROOT/skills/" "$ROOT/plugin/skills/" >&2 || true
|
|
24
|
+
echo "Run './scripts/sync-plugin-skills.sh' and commit the result." >&2
|
|
25
|
+
exit 1
|
|
26
|
+
fi
|
|
27
|
+
echo "OK: plugin/skills/ is in sync with skills/."
|
|
28
|
+
exit 0
|
|
29
|
+
fi
|
|
30
|
+
|
|
9
31
|
rm -rf "$ROOT/plugin/skills"
|
|
10
32
|
cp -a "$ROOT/skills" "$ROOT/plugin/skills"
|
|
11
33
|
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# sync-workflow-inline.sh — keep workflow scripts self-contained.
|
|
4
|
+
#
|
|
5
|
+
# The Workflow runtime cannot `import` (probed: "import() is not available in
|
|
6
|
+
# workflow scripts"; `require` is undefined), and workflow scripts have a
|
|
7
|
+
# top-level `return` so Node cannot import *them* either. Neither side can reach
|
|
8
|
+
# the other, yet we still want the shared logic unit-tested.
|
|
9
|
+
#
|
|
10
|
+
# Resolution: the pure logic lives in testable ES modules (the source of truth).
|
|
11
|
+
# Each module's INLINE region is mirrored verbatim into the workflow script
|
|
12
|
+
# between GENERATED markers. This script performs — or verifies — that mirror.
|
|
13
|
+
#
|
|
14
|
+
# ./scripts/sync-workflow-inline.sh # rewrite the generated blocks
|
|
15
|
+
# ./scripts/sync-workflow-inline.sh --check # fail if out of sync (CI/hook)
|
|
16
|
+
#
|
|
17
|
+
# Mirrors the contract of scripts/sync-plugin-version.sh.
|
|
18
|
+
|
|
19
|
+
set -euo pipefail
|
|
20
|
+
|
|
21
|
+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
22
|
+
cd "$REPO_ROOT"
|
|
23
|
+
|
|
24
|
+
TARGET="plugin-task-loop/workflows/session-execute.js"
|
|
25
|
+
|
|
26
|
+
# Modules mirrored into TARGET, in the order they must appear.
|
|
27
|
+
# Each entry is a bare module name; source is lib/<name>.js.
|
|
28
|
+
#
|
|
29
|
+
# Order is load-bearing where one module's inline region calls another's:
|
|
30
|
+
# `context-injection` calls resolveProfile()/profileStages() from `profile`, so
|
|
31
|
+
# `profile` must be mirrored ahead of it (const bindings are in the temporal dead
|
|
32
|
+
# zone until their block is evaluated).
|
|
33
|
+
# `task-graph` is self-contained (pure union-find over the assignment arrays), so
|
|
34
|
+
# its position is free; it sits last to keep the dependency-ordered prefix intact.
|
|
35
|
+
MODULES=(verdict-logic profile context-injection task-graph)
|
|
36
|
+
|
|
37
|
+
CHECK_MODE=0
|
|
38
|
+
if [ "${1:-}" = "--check" ]; then
|
|
39
|
+
CHECK_MODE=1
|
|
40
|
+
elif [ $# -gt 0 ]; then
|
|
41
|
+
echo "usage: $0 [--check]" >&2
|
|
42
|
+
exit 2
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
[ -f "$TARGET" ] || { echo "ERROR: missing $TARGET" >&2; exit 1; }
|
|
46
|
+
|
|
47
|
+
count_marker() { grep -cFx "$1" "$2" || true; }
|
|
48
|
+
|
|
49
|
+
WORK="$(mktemp -d)"
|
|
50
|
+
trap 'rm -rf "$WORK"' EXIT
|
|
51
|
+
|
|
52
|
+
# Start from the current target; each module rewrites its own block in turn.
|
|
53
|
+
cp "$TARGET" "$WORK/rendered"
|
|
54
|
+
|
|
55
|
+
for module in "${MODULES[@]}"; do
|
|
56
|
+
SOURCE="plugin-task-loop/workflows/lib/${module}.js"
|
|
57
|
+
[ -f "$SOURCE" ] || { echo "ERROR: missing $SOURCE" >&2; exit 1; }
|
|
58
|
+
|
|
59
|
+
SRC_BEGIN="// --- BEGIN INLINE: ${module} ---"
|
|
60
|
+
SRC_END="// --- END INLINE: ${module} ---"
|
|
61
|
+
DST_BEGIN="// --- BEGIN GENERATED: ${module} (source: lib/${module}.js) ---"
|
|
62
|
+
DST_END="// --- END GENERATED: ${module} ---"
|
|
63
|
+
|
|
64
|
+
for marker in "$SRC_BEGIN" "$SRC_END"; do
|
|
65
|
+
n=$(count_marker "$marker" "$SOURCE")
|
|
66
|
+
[ "$n" -eq 1 ] || { echo "ERROR: '$marker' appears $n time(s) in $SOURCE (want 1)" >&2; exit 1; }
|
|
67
|
+
done
|
|
68
|
+
for marker in "$DST_BEGIN" "$DST_END"; do
|
|
69
|
+
n=$(count_marker "$marker" "$WORK/rendered")
|
|
70
|
+
[ "$n" -eq 1 ] || { echo "ERROR: '$marker' appears $n time(s) in $TARGET (want 1)" >&2; exit 1; }
|
|
71
|
+
done
|
|
72
|
+
|
|
73
|
+
EXTRACTED="$WORK/${module}.body"
|
|
74
|
+
awk -v b="$SRC_BEGIN" -v e="$SRC_END" '
|
|
75
|
+
$0 == b { inblk = 1; next }
|
|
76
|
+
$0 == e { inblk = 0; next }
|
|
77
|
+
inblk { print }
|
|
78
|
+
' "$SOURCE" > "$EXTRACTED"
|
|
79
|
+
|
|
80
|
+
if [ ! -s "$EXTRACTED" ]; then
|
|
81
|
+
echo "ERROR: extracted inline region from $SOURCE is empty" >&2
|
|
82
|
+
exit 1
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
# The mirrored region must be self-contained.
|
|
86
|
+
if grep -Eq '^[[:space:]]*(import|export)[[:space:]]' "$EXTRACTED"; then
|
|
87
|
+
echo "ERROR: the inline region of $SOURCE contains import/export." >&2
|
|
88
|
+
echo " Workflow scripts cannot import. Keep those outside the markers." >&2
|
|
89
|
+
exit 1
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
# A destination marker inside the copied body would corrupt the target — the
|
|
93
|
+
# awk pass would end the generated block early and duplicate the remainder,
|
|
94
|
+
# after which every --check fails with a confusing "appears 2 time(s)".
|
|
95
|
+
for marker in "$DST_BEGIN" "$DST_END"; do
|
|
96
|
+
if grep -qFx "$marker" "$EXTRACTED"; then
|
|
97
|
+
echo "ERROR: the inline region of $SOURCE contains the destination marker:" >&2
|
|
98
|
+
echo " $marker" >&2
|
|
99
|
+
echo " Remove it — it would corrupt $TARGET on the next sync." >&2
|
|
100
|
+
exit 1
|
|
101
|
+
fi
|
|
102
|
+
done
|
|
103
|
+
|
|
104
|
+
awk -v b="$DST_BEGIN" -v e="$DST_END" -v body="$EXTRACTED" -v src="$SOURCE" '
|
|
105
|
+
$0 == b {
|
|
106
|
+
print
|
|
107
|
+
print "// AUTO-GENERATED — DO NOT EDIT BY HAND."
|
|
108
|
+
print "// Source: " src
|
|
109
|
+
print "// Regenerate: ./scripts/sync-workflow-inline.sh"
|
|
110
|
+
while ((getline line < body) > 0) print line
|
|
111
|
+
close(body)
|
|
112
|
+
skip = 1
|
|
113
|
+
next
|
|
114
|
+
}
|
|
115
|
+
$0 == e { skip = 0; print; next }
|
|
116
|
+
!skip { print }
|
|
117
|
+
' "$WORK/rendered" > "$WORK/next"
|
|
118
|
+
mv "$WORK/next" "$WORK/rendered"
|
|
119
|
+
done
|
|
120
|
+
|
|
121
|
+
if [ "$CHECK_MODE" -eq 1 ]; then
|
|
122
|
+
if ! diff -u "$TARGET" "$WORK/rendered" > /dev/null 2>&1; then
|
|
123
|
+
echo "ERROR: $TARGET is out of sync with its source modules" >&2
|
|
124
|
+
echo " Run ./scripts/sync-workflow-inline.sh to regenerate." >&2
|
|
125
|
+
echo >&2
|
|
126
|
+
diff -u "$TARGET" "$WORK/rendered" >&2 || true
|
|
127
|
+
exit 1
|
|
128
|
+
fi
|
|
129
|
+
echo "OK: $TARGET is in sync with ${MODULES[*]}"
|
|
130
|
+
exit 0
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
if diff -q "$TARGET" "$WORK/rendered" > /dev/null 2>&1; then
|
|
134
|
+
echo "OK: $TARGET already in sync (no change)"
|
|
135
|
+
else
|
|
136
|
+
cat "$WORK/rendered" > "$TARGET"
|
|
137
|
+
echo "Synced: ${MODULES[*]} -> $TARGET"
|
|
138
|
+
fi
|
package/skills/handoff/SKILL.md
CHANGED
|
@@ -62,6 +62,38 @@ description: "Session handoff — load context at start, save at end, track task
|
|
|
62
62
|
aggregation time by `handoff_get_metrics`/`handoff_get_capacity`. To turn the
|
|
63
63
|
requirement off, set `settings.require_estimate_hours = false`.
|
|
64
64
|
|
|
65
|
+
### Checklist before every `handoff_update_task` that creates a task
|
|
66
|
+
|
|
67
|
+
Run through this as you write the call, not after it is rejected. A leaf task
|
|
68
|
+
missing `estimate_hours` is refused, costing a round trip.
|
|
69
|
+
|
|
70
|
+
- [ ] `title` — present (required for any new task)
|
|
71
|
+
- [ ] `done_criteria` — verifiable items, not restatements of the title
|
|
72
|
+
- [ ] `schedule.estimate_hours` — **> 0, raw human-effort hours.** Skip only if
|
|
73
|
+
the task is a parent (has children) or its status is `blocked`/`skipped`
|
|
74
|
+
- [ ] `priority` — `low` / `medium` / `high`
|
|
75
|
+
- [ ] `labels` — at least one, so the task is findable by filter
|
|
76
|
+
- [ ] `assignee` — matches a key in `config.toml [assignees.<key>]`
|
|
77
|
+
|
|
78
|
+
A minimal accepted payload:
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"task": {
|
|
83
|
+
"title": "Add retry to the upload path",
|
|
84
|
+
"status": "todo",
|
|
85
|
+
"priority": "high",
|
|
86
|
+
"labels": ["upload", "reliability"],
|
|
87
|
+
"assignee": "ai",
|
|
88
|
+
"schedule": { "estimate_hours": 2.0 },
|
|
89
|
+
"done_criteria": [{ "item": "Upload retries 3x on 5xx, then surfaces the error" }]
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
When updating an existing task, you do **not** resend `estimate_hours` — the
|
|
95
|
+
stored value satisfies the requirement. Send only the fields you are changing.
|
|
96
|
+
|
|
65
97
|
### Appending to Task Notes
|
|
66
98
|
- Use `notes_append` (not `notes`) in `handoff_update_task` or
|
|
67
99
|
`handoff_bulk_update_tasks` to add text to existing task notes without
|
|
@@ -201,6 +233,35 @@ Use `handoff_bulk_update_tasks` for:
|
|
|
201
233
|
- Batch status changes (e.g., closing all review tasks).
|
|
202
234
|
- Batch assignee changes (e.g., reassigning a team member's tasks).
|
|
203
235
|
- Each task update is independent — failures on one task don't roll back others.
|
|
236
|
+
- The `estimate_hours` rule applies here too: an update leaving a leaf task in
|
|
237
|
+
`todo`/`in_progress`/`review`/`done` without `schedule.estimate_hours` is
|
|
238
|
+
rejected and reported in `errors[]`. Supply the estimate in the same update
|
|
239
|
+
when moving a task out of `blocked`/`skipped`. Parent tasks and the statuses
|
|
240
|
+
`blocked`/`skipped` are exempt.
|
|
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).
|
|
204
265
|
|
|
205
266
|
### Configuration Management
|
|
206
267
|
|