handoff-mcp-server 0.26.0 → 0.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.
Files changed (72) hide show
  1. package/README.md +31 -4
  2. package/bin/handoff-mcp.js +56 -13
  3. package/bin/resolve-binary.js +122 -0
  4. package/package.json +14 -9
  5. package/Cargo.lock +0 -686
  6. package/Cargo.toml +0 -30
  7. package/scripts/cargo-env.sh +0 -29
  8. package/scripts/handoff-memory-hook.py +0 -208
  9. package/scripts/install-local.sh +0 -109
  10. package/scripts/postinstall.js +0 -50
  11. package/scripts/sync-plugin-skills.sh +0 -35
  12. package/scripts/sync-plugin-version.sh +0 -85
  13. package/scripts/sync-workflow-inline.sh +0 -138
  14. package/src/cli.rs +0 -551
  15. package/src/context/injection.rs +0 -276
  16. package/src/context/mod.rs +0 -129
  17. package/src/lib.rs +0 -5
  18. package/src/main.rs +0 -157
  19. package/src/mcp/handlers/assignees.rs +0 -254
  20. package/src/mcp/handlers/auto_schedule.rs +0 -489
  21. package/src/mcp/handlers/bulk_update.rs +0 -155
  22. package/src/mcp/handlers/calendar.rs +0 -196
  23. package/src/mcp/handlers/capacity.rs +0 -318
  24. package/src/mcp/handlers/check_criterion.rs +0 -70
  25. package/src/mcp/handlers/config.rs +0 -402
  26. package/src/mcp/handlers/config_crud.rs +0 -183
  27. package/src/mcp/handlers/dashboard.rs +0 -214
  28. package/src/mcp/handlers/docs.rs +0 -2288
  29. package/src/mcp/handlers/docs_query.rs +0 -1335
  30. package/src/mcp/handlers/fork_session.rs +0 -91
  31. package/src/mcp/handlers/get_session.rs +0 -48
  32. package/src/mcp/handlers/get_task.rs +0 -53
  33. package/src/mcp/handlers/import_context.rs +0 -470
  34. package/src/mcp/handlers/init.rs +0 -28
  35. package/src/mcp/handlers/list_sessions.rs +0 -187
  36. package/src/mcp/handlers/list_tasks.rs +0 -308
  37. package/src/mcp/handlers/load_context.rs +0 -361
  38. package/src/mcp/handlers/log_time.rs +0 -67
  39. package/src/mcp/handlers/memory.rs +0 -961
  40. package/src/mcp/handlers/merge_sessions.rs +0 -103
  41. package/src/mcp/handlers/metrics.rs +0 -196
  42. package/src/mcp/handlers/milestones.rs +0 -102
  43. package/src/mcp/handlers/mod.rs +0 -140
  44. package/src/mcp/handlers/refer.rs +0 -307
  45. package/src/mcp/handlers/referrals.rs +0 -74
  46. package/src/mcp/handlers/save_context.rs +0 -354
  47. package/src/mcp/handlers/task_checklist.rs +0 -507
  48. package/src/mcp/handlers/timer.rs +0 -529
  49. package/src/mcp/handlers/update_session.rs +0 -197
  50. package/src/mcp/handlers/update_task.rs +0 -452
  51. package/src/mcp/mod.rs +0 -6
  52. package/src/mcp/protocol.rs +0 -41
  53. package/src/mcp/resources.rs +0 -57
  54. package/src/mcp/router.rs +0 -154
  55. package/src/mcp/tools.rs +0 -1522
  56. package/src/mcp/types.rs +0 -108
  57. package/src/setup.rs +0 -1212
  58. package/src/storage/config.rs +0 -578
  59. package/src/storage/docs/frontmatter.rs +0 -509
  60. package/src/storage/docs/mod.rs +0 -835
  61. package/src/storage/docs/model.rs +0 -708
  62. package/src/storage/docs/reassemble.rs +0 -167
  63. package/src/storage/docs/split.rs +0 -377
  64. package/src/storage/git.rs +0 -47
  65. package/src/storage/memory/injected.rs +0 -340
  66. package/src/storage/memory/mod.rs +0 -236
  67. package/src/storage/memory/model.rs +0 -127
  68. package/src/storage/mod.rs +0 -96
  69. package/src/storage/referrals.rs +0 -248
  70. package/src/storage/sessions.rs +0 -859
  71. package/src/storage/tasks.rs +0 -957
  72. package/templates/claude-md-section.md +0 -12
@@ -1,29 +0,0 @@
1
- # shellcheck shell=sh
2
- # cargo-env.sh — make cargo commands work across heterogeneous dev environments.
3
- #
4
- # Some environments (e.g. a Docker image where CARGO_HOME=/usr/local/cargo is
5
- # owned by root) expose a CARGO_HOME that the current user cannot write to.
6
- # cargo then fails to download/cache crates or fetch the advisory-db, breaking
7
- # the git hooks even though nothing is wrong with the code.
8
- #
9
- # This script detects an unwritable CARGO_HOME and transparently falls back to
10
- # the per-user $HOME/.cargo, which every supported environment can write to.
11
- # When CARGO_HOME is already writable (local macOS, a synced Mac, or simply
12
- # unset so cargo uses its default ~/.cargo) it leaves the environment untouched.
13
- #
14
- # Usage (from a hook or shell):
15
- # . scripts/cargo-env.sh && cargo <args>
16
-
17
- # Resolve the directory cargo would use right now.
18
- _cargo_home="${CARGO_HOME:-$HOME/.cargo}"
19
-
20
- # Probe writability with a temp file; fall back to $HOME/.cargo if we can't write.
21
- if ! ( mkdir -p "$_cargo_home" 2>/dev/null && touch "$_cargo_home/.cargo-env-wtest" 2>/dev/null ); then
22
- export CARGO_HOME="$HOME/.cargo"
23
- mkdir -p "$CARGO_HOME" 2>/dev/null || true
24
- else
25
- rm -f "$_cargo_home/.cargo-env-wtest" 2>/dev/null || true
26
- export CARGO_HOME="$_cargo_home"
27
- fi
28
-
29
- unset _cargo_home
@@ -1,208 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Claude Code hook wrapper for handoff-mcp project memory.
3
-
4
- This is the **fallback path** for memory auto-injection. The preferred wiring is
5
- a native ``mcp_tool`` hook that calls ``handoff_memory_query`` / ``handoff_memory_cleanup``
6
- directly (see README "Project Memory"); this script exists for Claude Code
7
- versions that lack the ``mcp_tool`` hook type, by translating a Claude Code hook
8
- event into a single JSON-RPC ``tools/call`` against the handoff-mcp server and
9
- emitting the result as ``hookSpecificOutput.additionalContext``.
10
-
11
- It speaks the server's line-delimited JSON-RPC over stdio: one request object on
12
- one line in, one response object on one line out. ``handoff_memory_query`` /
13
- ``handoff_memory_cleanup`` return their payload as a JSON *string* inside
14
- ``result.content[0].text`` (so both this wrapper and the native hook path parse
15
- it identically), which this script re-parses.
16
-
17
- Wiring (in ``~/.claude/settings.json`` — do NOT commit this into a repo):
18
-
19
- {
20
- "hooks": {
21
- "UserPromptSubmit": [
22
- { "hooks": [ { "type": "command",
23
- "command": "handoff-mcp-memory-hook" } ] }
24
- ],
25
- "PreToolUse": [
26
- { "matcher": "Edit|Write|MultiEdit",
27
- "hooks": [ { "type": "command",
28
- "command": "handoff-mcp-memory-hook" } ] }
29
- ],
30
- "SessionStart": [
31
- { "hooks": [ { "type": "command",
32
- "command": "handoff-mcp-memory-hook" } ] }
33
- ]
34
- }
35
- }
36
-
37
- The same script handles all three events; it picks the tool from
38
- ``hook_event_name`` on stdin.
39
-
40
- Environment:
41
- - ``HANDOFF_MCP_BIN`` — path to the ``handoff-mcp`` binary (default: ``handoff-mcp``
42
- resolved on ``PATH``).
43
-
44
- Design contract: this script must **never break the session**. On any error
45
- (binary missing, bad JSON, timeout) it prints nothing and exits 0 — a memory
46
- miss is silent, never a blocked prompt.
47
- """
48
-
49
- # `X | None` annotations are evaluated lazily so this runs on Python 3.7+
50
- # (a hook host may not have 3.10). The annotations are never evaluated at runtime.
51
- from __future__ import annotations
52
-
53
- import json
54
- import os
55
- import shutil
56
- import subprocess
57
- import sys
58
-
59
- # Map a Claude Code hook event to the memory tool it drives.
60
- QUERY_EVENTS = {"UserPromptSubmit", "PreToolUse"}
61
- CLEANUP_EVENTS = {"SessionStart"}
62
-
63
- # How long to wait for the server to answer one request, in seconds. The query
64
- # is in-memory and sub-millisecond; this is just a safety net so a hung binary
65
- # can never stall the prompt.
66
- TIMEOUT_SECONDS = 5.0
67
-
68
-
69
- def _server_bin() -> str | None:
70
- """Resolve the handoff-mcp binary path, or None if it can't be found."""
71
- explicit = os.environ.get("HANDOFF_MCP_BIN")
72
- if explicit:
73
- return explicit if os.path.exists(explicit) else None
74
- return shutil.which("handoff-mcp")
75
-
76
-
77
- def _call(bin_path: str, tool: str, arguments: dict) -> dict | None:
78
- """Send one ``tools/call`` line and parse the inner JSON payload.
79
-
80
- Returns the decoded inner object (e.g. ``{"memories": [...]}`), or None on
81
- any transport / decode failure. The server is stateless per line, so no
82
- ``initialize`` handshake is needed — a bare ``tools/call`` is answered.
83
- """
84
- request = {
85
- "jsonrpc": "2.0",
86
- "id": 1,
87
- "method": "tools/call",
88
- "params": {"name": tool, "arguments": arguments},
89
- }
90
- try:
91
- proc = subprocess.run(
92
- [bin_path],
93
- input=json.dumps(request) + "\n",
94
- capture_output=True,
95
- text=True,
96
- timeout=TIMEOUT_SECONDS,
97
- )
98
- except (OSError, subprocess.TimeoutExpired):
99
- return None
100
-
101
- # The response is the first non-empty stdout line (stderr carries the
102
- # startup banner and is ignored).
103
- line = next((ln for ln in proc.stdout.splitlines() if ln.strip()), None)
104
- if not line:
105
- return None
106
- try:
107
- envelope = json.loads(line)
108
- text = envelope["result"]["content"][0]["text"]
109
- return json.loads(text)
110
- except (json.JSONDecodeError, KeyError, IndexError, TypeError):
111
- return None
112
-
113
-
114
- def _file_paths_from_tool_input(tool_input) -> list[str]:
115
- """Pull file paths out of a PreToolUse ``tool_input`` (Edit/Write/MultiEdit)."""
116
- if not isinstance(tool_input, dict):
117
- return []
118
- paths = []
119
- fp = tool_input.get("file_path")
120
- if isinstance(fp, str) and fp:
121
- paths.append(fp)
122
- # MultiEdit and similar may carry an edits array; each can name a file.
123
- for edit in tool_input.get("edits", []) or []:
124
- if isinstance(edit, dict):
125
- efp = edit.get("file_path")
126
- if isinstance(efp, str) and efp:
127
- paths.append(efp)
128
- return paths
129
-
130
-
131
- def _emit(event: str, context: str) -> None:
132
- """Print a Claude Code hook output object carrying additionalContext."""
133
- if not context:
134
- return
135
- out = {
136
- "hookSpecificOutput": {
137
- "hookEventName": event,
138
- "additionalContext": context,
139
- }
140
- }
141
- print(json.dumps(out))
142
-
143
-
144
- def _format_memories(payload: dict) -> str:
145
- """Turn a ``handoff_memory_query`` payload into an injectable context block."""
146
- memories = payload.get("memories") or []
147
- if not memories:
148
- return ""
149
- lines = ["Relevant project memories (handoff-mcp):"]
150
- for m in memories:
151
- text = (m.get("text") or "").strip()
152
- if not text:
153
- continue
154
- kind = m.get("kind") or "memory"
155
- lines.append(f"- [{kind}] {text}")
156
- # Only a header with no bullets means nothing useful to inject.
157
- return "\n".join(lines) if len(lines) > 1 else ""
158
-
159
-
160
- def main() -> int:
161
- raw = sys.stdin.read()
162
- try:
163
- hook = json.loads(raw) if raw.strip() else {}
164
- except json.JSONDecodeError:
165
- return 0 # malformed hook input — stay silent, never block
166
-
167
- event = hook.get("hook_event_name") or ""
168
- bin_path = _server_bin()
169
- if not bin_path:
170
- return 0 # server not installed — nothing to inject
171
-
172
- project_dir = hook.get("cwd") or os.getcwd()
173
- session_id = hook.get("session_id")
174
-
175
- if event in QUERY_EVENTS:
176
- # UserPromptSubmit → match the prompt. PreToolUse → match the file(s).
177
- prompt = hook.get("prompt") or ""
178
- tool_name = hook.get("tool_name")
179
- file_paths = _file_paths_from_tool_input(hook.get("tool_input"))
180
- # Text fed to BM25: the prompt for UserPromptSubmit, else the file paths
181
- # (the server also adds basenames + tool_name to the query).
182
- text = prompt if prompt else " ".join(file_paths)
183
- arguments = {"project_dir": project_dir, "text": text}
184
- if session_id:
185
- arguments["session_id"] = session_id
186
- if tool_name:
187
- arguments["tool_name"] = tool_name
188
- if file_paths:
189
- arguments["file_paths"] = file_paths
190
-
191
- payload = _call(bin_path, "handoff_memory_query", arguments)
192
- if payload:
193
- _emit(event, _format_memories(payload))
194
- return 0
195
-
196
- if event in CLEANUP_EVENTS:
197
- # Housekeeping only: merge exact duplicates and gc sidecars. We do not
198
- # inject the cleanup recommendations as context (that is for an explicit
199
- # AI-driven pass), so there is no additionalContext to emit here.
200
- _call(bin_path, "handoff_memory_cleanup", {"project_dir": project_dir})
201
- return 0
202
-
203
- # Unknown event — nothing to do.
204
- return 0
205
-
206
-
207
- if __name__ == "__main__":
208
- sys.exit(main())
@@ -1,109 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Install handoff-mcp locally: binary, skills, and plugin caches.
3
- # Usage: ./scripts/install-local.sh
4
- #
5
- # SCOPE: this script rebuilds the binary and refreshes the plugin CACHE only.
6
- # It does NOT register the marketplace (known_marketplaces.json) or enable the
7
- # plugin (enabledPlugins in settings.json), and it only updates
8
- # installed_plugins.json entries that already exist. So on a fresh machine it
9
- # does not activate the plugin by itself — bootstrap once with:
10
- # /plugin marketplace add /absolute/path/to/handoff-mcp
11
- # /plugin install handoff-mcp@handoff-mcp-marketplace
12
- # /reload-plugins
13
- # Thereafter, re-run this script to pick up code changes, then restart Claude
14
- # Code (or /reload-plugins). See README "Installing the local development
15
- # version instead".
16
- set -euo pipefail
17
-
18
- SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
19
- ROOT="$(dirname "$SCRIPT_DIR")"
20
- CLAUDE_DIR="${HOME}/.claude"
21
- CACHE_DIR="${CLAUDE_DIR}/plugins/cache/handoff-mcp-marketplace"
22
- INSTALLED_JSON="${CLAUDE_DIR}/plugins/installed_plugins.json"
23
-
24
- cd "$ROOT"
25
-
26
- # ---------- 1. Binary ----------
27
- echo "==> Building release binary..."
28
- cargo build --release
29
-
30
- mkdir -p "${HOME}/.local/bin"
31
- rm -f "${HOME}/.local/bin/handoff-mcp"
32
- cp target/release/handoff-mcp "${HOME}/.local/bin/handoff-mcp"
33
- echo " Installed: $(${HOME}/.local/bin/handoff-mcp --version)"
34
-
35
- # ---------- 2. Bundled skills ----------
36
- echo "==> Syncing skills..."
37
- skill_count=0
38
- for skill in skills/*/; do
39
- name=$(basename "$skill")
40
- mkdir -p "${CLAUDE_DIR}/skills/${name}"
41
- cp "${skill}SKILL.md" "${CLAUDE_DIR}/skills/${name}/SKILL.md"
42
- skill_count=$((skill_count + 1))
43
- done
44
- echo " ${skill_count} skills synced"
45
-
46
- # ---------- 3. Sync plugin distribution skills ----------
47
- echo "==> Syncing plugin distribution skills..."
48
- "${SCRIPT_DIR}/sync-plugin-skills.sh"
49
-
50
- # ---------- 4. Plugin caches ----------
51
- echo "==> Syncing plugin caches..."
52
-
53
- sync_plugin() {
54
- local src_dir="$1"
55
- local plugin_json="${src_dir}/.claude-plugin/plugin.json"
56
-
57
- if [ ! -f "$plugin_json" ]; then
58
- echo " SKIP: ${src_dir} (no .claude-plugin/plugin.json)"
59
- return
60
- fi
61
-
62
- local name version cache_dest
63
- name=$(python3 -c "import json,sys; print(json.load(sys.stdin)['name'])" < "$plugin_json")
64
- version=$(python3 -c "import json,sys; print(json.load(sys.stdin)['version'])" < "$plugin_json")
65
- cache_dest="${CACHE_DIR}/${name}/${version}"
66
-
67
- rm -rf "$cache_dest"
68
- mkdir -p "$cache_dest"
69
-
70
- # Copy everything except .claude-plugin/ first, then copy .claude-plugin/
71
- find "$src_dir" -mindepth 1 -maxdepth 1 -not -name '.claude-plugin' -exec cp -a {} "$cache_dest/" \;
72
- cp -a "${src_dir}/.claude-plugin" "$cache_dest/.claude-plugin"
73
-
74
- echo " ${name}@${version} -> ${cache_dest}"
75
-
76
- # Update installed_plugins.json timestamp
77
- if [ -f "$INSTALLED_JSON" ] && command -v python3 >/dev/null 2>&1; then
78
- local git_sha
79
- git_sha=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
80
- local now
81
- now=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
82
- python3 -c "
83
- import json, sys
84
- key = '${name}@handoff-mcp-marketplace'
85
- with open('${INSTALLED_JSON}', 'r') as f:
86
- data = json.load(f)
87
- if key in data.get('plugins', {}):
88
- for entry in data['plugins'][key]:
89
- entry['installPath'] = '${cache_dest}'
90
- entry['version'] = '${version}'
91
- entry['lastUpdated'] = '${now}'
92
- entry['gitCommitSha'] = '${git_sha}'
93
- with open('${INSTALLED_JSON}', 'w') as f:
94
- json.dump(data, f, indent=4)
95
- f.write('\n')
96
- " 2>/dev/null || true
97
- fi
98
- }
99
-
100
- sync_plugin "${ROOT}/plugin"
101
- sync_plugin "${ROOT}/plugin-hooks"
102
- sync_plugin "${ROOT}/plugin-task-loop"
103
-
104
- # ---------- 5. Verify ----------
105
- echo ""
106
- echo "==> Done. Restart Claude Code to pick up changes."
107
- echo " Binary: ${HOME}/.local/bin/handoff-mcp"
108
- echo " Skills: ${CLAUDE_DIR}/skills/"
109
- echo " Plugins: ${CACHE_DIR}/"
@@ -1,50 +0,0 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
-
4
- const { execSync } = require("child_process");
5
- const fs = require("fs");
6
- const path = require("path");
7
-
8
- const ROOT = path.resolve(__dirname, "..");
9
- const BIN_DIR = path.join(ROOT, "bin");
10
- const BINARY = path.join(BIN_DIR, "handoff-mcp-bin");
11
-
12
- if (fs.existsSync(BINARY)) {
13
- try {
14
- execSync(`"${BINARY}" --help`, { stdio: "ignore", timeout: 5000 });
15
- process.exit(0);
16
- } catch {
17
- // prebuilt binary exists but can't run on this platform — rebuild
18
- fs.unlinkSync(BINARY);
19
- }
20
- }
21
-
22
- try {
23
- execSync("cargo --version", { stdio: "ignore" });
24
- } catch {
25
- console.error(
26
- "Error: Rust toolchain not found.\n" +
27
- "handoff-mcp-server requires Rust to build from source.\n" +
28
- "Install Rust: https://rustup.rs/"
29
- );
30
- process.exit(1);
31
- }
32
-
33
- console.log("Building handoff-mcp from source...");
34
- try {
35
- execSync("cargo build --release", { cwd: ROOT, stdio: "inherit" });
36
- } catch {
37
- console.error("Error: cargo build failed.");
38
- process.exit(1);
39
- }
40
-
41
- const built = path.join(ROOT, "target", "release", "handoff-mcp");
42
- if (!fs.existsSync(built)) {
43
- console.error("Error: binary not found after build.");
44
- process.exit(1);
45
- }
46
-
47
- fs.mkdirSync(BIN_DIR, { recursive: true });
48
- fs.copyFileSync(built, BINARY);
49
- fs.chmodSync(BINARY, 0o755);
50
- console.log("handoff-mcp installed successfully.");
@@ -1,35 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Sync skills/ -> plugin/skills/ for plugin distribution.
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)
11
- set -euo pipefail
12
-
13
- SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
14
- ROOT="$(dirname "$SCRIPT_DIR")"
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
-
31
- rm -rf "$ROOT/plugin/skills"
32
- cp -a "$ROOT/skills" "$ROOT/plugin/skills"
33
-
34
- echo "Synced skills/ -> plugin/skills/"
35
- diff -rq "$ROOT/skills/" "$ROOT/plugin/skills/" && echo "No differences." || echo "WARNING: differences remain after sync."
@@ -1,85 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Sync plugin/marketplace manifest versions to package.json's version.
3
- #
4
- # package.json is the source of truth (kept in sync with Cargo.toml manually
5
- # per CLAUDE.md "Version sync" rule). This script propagates that version to:
6
- # - plugin/.claude-plugin/plugin.json
7
- # - plugin-hooks/.claude-plugin/plugin.json
8
- # - plugin-task-loop/.claude-plugin/plugin.json
9
- # - .claude-plugin/marketplace.json (all three "version" entries)
10
- #
11
- # Uses regex line replacement (not json.dump) so untouched formatting in
12
- # these hand-maintained manifests is preserved byte-for-byte.
13
- #
14
- # Usage:
15
- # ./scripts/sync-plugin-version.sh # fix mismatches in place
16
- # ./scripts/sync-plugin-version.sh --check # exit 1 if anything is out of sync, fix nothing
17
- set -euo pipefail
18
-
19
- SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
20
- ROOT="$(dirname "$SCRIPT_DIR")"
21
- cd "$ROOT"
22
-
23
- MODE="fix"
24
- if [ "${1:-}" = "--check" ]; then
25
- MODE="check"
26
- fi
27
-
28
- python3 - "$MODE" <<'PYEOF'
29
- import json
30
- import re
31
- import sys
32
-
33
- mode = sys.argv[1]
34
-
35
- with open("package.json") as f:
36
- root_version = json.load(f)["version"]
37
-
38
- version_line_re = re.compile(r'^(\s*"version"\s*:\s*)"[^"]*"(,?\s*(?:\r?\n)?)$')
39
-
40
- def sync_file(path, mismatches):
41
- with open(path) as f:
42
- lines = f.readlines()
43
- changed = False
44
- out = []
45
- for line in lines:
46
- m = version_line_re.match(line)
47
- if m:
48
- current = re.search(r'"version"\s*:\s*"([^"]*)"', line).group(1)
49
- if current != root_version:
50
- mismatches.append((path, current))
51
- changed = True
52
- line = f'{m.group(1)}"{root_version}"{m.group(2)}'
53
- out.append(line)
54
- if changed and mode == "fix":
55
- with open(path, "w") as f:
56
- f.writelines(out)
57
- return changed
58
-
59
- mismatches = []
60
- plugin_files = [
61
- "plugin/.claude-plugin/plugin.json",
62
- "plugin-hooks/.claude-plugin/plugin.json",
63
- "plugin-task-loop/.claude-plugin/plugin.json",
64
- ".claude-plugin/marketplace.json",
65
- ]
66
-
67
- for path in plugin_files:
68
- sync_file(path, mismatches)
69
-
70
- if mode == "check":
71
- if mismatches:
72
- print(f"Version mismatch: package.json is {root_version}, but found:")
73
- for path, version in mismatches:
74
- print(f" {path}: {version}")
75
- print("Run ./scripts/sync-plugin-version.sh to fix.")
76
- sys.exit(1)
77
- print(f"All plugin/marketplace manifests match package.json ({root_version}).")
78
- else:
79
- if mismatches:
80
- print(f"Synced {len(mismatches)} version occurrence(s) to {root_version}:")
81
- for path, old_version in mismatches:
82
- print(f" {path}: {old_version} -> {root_version}")
83
- else:
84
- print(f"Already in sync at version {root_version}. No changes made.")
85
- PYEOF
@@ -1,138 +0,0 @@
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