claude-code-kanban 5.3.0 → 6.0.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/README.md +4 -4
- package/cli.js +30 -108
- package/install.js +43 -123
- package/lib/approvals.js +16 -17
- package/lib/dispatch-groups.js +9 -31
- package/lib/dispatch.js +19 -166
- package/lib/git-branch.js +62 -0
- package/lib/parsers.js +96 -59
- package/lib/plugin-status.js +1 -1
- package/lib/retention.js +5 -17
- package/lib/session-cache.js +1 -1
- package/lib/session-events.js +3 -2
- package/lib/terminal.js +30 -5
- package/package.json +1 -2
- package/plugin/.claude-plugin/marketplace.json +1 -1
- package/plugin/plugins/claude-code-kanban/.claude-plugin/plugin.json +2 -2
- package/plugin/plugins/claude-code-kanban/hooks/activity.ts +266 -0
- package/plugin/plugins/claude-code-kanban/hooks/context.ts +77 -0
- package/plugin/plugins/claude-code-kanban/hooks/hooks.json +1 -93
- package/plugin/plugins/claude-code-kanban/hooks/index.ts +8 -0
- package/plugin/plugins/claude-code-kanban/monitors.json +0 -6
- package/plugin/plugins/claude-code-kanban/scripts/postman.js +6 -11
- package/plugin/plugins/claude-code-kanban/skills/dispatch/SKILL.md +17 -11
- package/plugin/plugins/claude-code-kanban/skills/dispatch/references/orchestration-patterns.md +50 -0
- package/plugin/plugins/claude-code-kanban/tests/activity.test.ts +300 -0
- package/plugin/plugins/claude-code-kanban/tests/context.test.ts +72 -0
- package/plugin/plugins/claude-code-kanban/tsconfig.json +3 -0
- package/public/app.js +257 -74
- package/public/index.html +12 -2
- package/public/project-match.js +6 -1
- package/public/style.css +99 -0
- package/server.js +146 -147
- package/skill-guides/dispatch.md +16 -54
- package/plugin/plugins/claude-code-kanban/scripts/agent-spy.sh +0 -135
- package/plugin/plugins/claude-code-kanban/scripts/approval-gate.sh +0 -208
- package/plugin/plugins/claude-code-kanban/scripts/context-status.sh +0 -18
package/skill-guides/dispatch.md
CHANGED
|
@@ -1,73 +1,35 @@
|
|
|
1
1
|
# Dispatch guide
|
|
2
2
|
|
|
3
|
-
A dispatch is
|
|
3
|
+
A dispatch is a plain Claude Code session that cck starts in its embedded terminal, with your spec as the first message. It is an ordinary session: it shows in the sidebar, its card links back to you, and the user can open its terminal at any time. cck only starts it.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Spec
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
The started session sees only the spec, not this conversation, so the spec carries everything it needs.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
- **Target:** the files, component, or environment in scope.
|
|
12
|
-
- **Change:** the concrete result to produce.
|
|
13
|
-
- **Constraints:** invariants and do-not-touch boundaries.
|
|
14
|
-
- **Ownership:** what it may edit. Two dispatches edit the same files only when each runs in its own `--worktree`.
|
|
15
|
-
- **Acceptance:** the test, output, or evidence that proves it is done.
|
|
16
|
-
|
|
17
|
-
Dispatch when the task can run on its own. Do the work yourself when it is small or needs context from this conversation that you cannot write down.
|
|
9
|
+
cck sends nothing back. To hear from the session, the spec tells it to `SendMessage` you and names you. Your name is auto-assigned (e.g. `claude-code-hub-06`); `ListAgents` prints it as "This session is <name>".
|
|
18
10
|
|
|
19
11
|
## Start
|
|
20
12
|
|
|
21
13
|
```bash
|
|
22
|
-
claude-code-kanban dispatch start --cwd <dir> --spec-file <spec.md> --name <name> --group <group> --
|
|
14
|
+
claude-code-kanban dispatch start --cwd <dir> --spec-file <spec.md> --name <name> --group <group> --json -- <claude args>
|
|
23
15
|
```
|
|
24
16
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
- `--
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
- The result holds the `dispatch` id and the `session` id.
|
|
32
|
-
|
|
33
|
-
## Fire-and-forget
|
|
17
|
+
- `--name` is the sidebar name and the session's peer name. Kebab-case and unique: `fix-login-redirect`.
|
|
18
|
+
- `--group` names the effort in kebab-case (`auth-refactor`) and shows the session under that sidebar group. Pass it on every dispatch that belongs to the effort; a dispatch without it goes to its project.
|
|
19
|
+
- `--spec-file` keeps a long spec out of shell quoting.
|
|
20
|
+
- Everything after `--` goes to `claude` as it is: any flag in `claude --help`, e.g. `-- --permission-mode auto --add-dir ../shared`. Keep each value one shell word of plain characters (no quotes, `%` or control characters); long text belongs in the spec. cck owns the session id, name, model and worktree, so pass those with its own flags.
|
|
21
|
+
- After a cck restart the terminal comes back with `claude --resume <id>` alone, so the args after `--` apply to the first run only.
|
|
22
|
+
- The result holds the `session` id.
|
|
34
23
|
|
|
35
|
-
|
|
24
|
+
`claude-code-kanban help dispatch start` lists cck's flags (`--model`, `--worktree` and more); `claude --help` lists the ones you can pass after `--`.
|
|
36
25
|
|
|
37
|
-
##
|
|
26
|
+
## Messages and status
|
|
38
27
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
- **Inbox:** this skill armed it. Lines `[kanban board] Dispatch <id> (session <uuid>) ...` arrive on their own while you keep working.
|
|
42
|
-
- **Wait:** block until one settles.
|
|
43
|
-
|
|
44
|
-
```bash
|
|
45
|
-
claude-code-kanban dispatch wait [<id>...] --timeout 15m --json
|
|
46
|
-
```
|
|
28
|
+
A message from the session arrives here as a new turn. Reply with `SendMessage` to its name.
|
|
47
29
|
|
|
48
|
-
|
|
30
|
+
A crashed session sends nothing. To learn when one ends, `SendMessage` it with `notify_when_idle: true`, or look:
|
|
49
31
|
|
|
50
32
|
```bash
|
|
51
|
-
claude-code-kanban dispatch list --json
|
|
33
|
+
claude-code-kanban dispatch list --json # still running in cck's terminal
|
|
52
34
|
claude-code-kanban session peek <session-id> --limit 20
|
|
53
35
|
```
|
|
54
|
-
|
|
55
|
-
A dispatch still `running` is still working; retry only after a `failed` report or an `exited` one.
|
|
56
|
-
|
|
57
|
-
The summary is the started session's own claim. Verify it (run the tests, read the diff), then give the user each dispatch's outcome, the summary, and what you checked. Done when every `--report` dispatch has settled and each summary is verified.
|
|
58
|
-
|
|
59
|
-
## Peer
|
|
60
|
-
|
|
61
|
-
A dispatch is a Claude Code peer under its `--name`, so `SendMessage` reaches it and it reaches you. The peer channel carries the conversation. The report carries the record: only `dispatch done` settles a dispatch, ends `dispatch wait`, and shows in the sidebar.
|
|
62
|
-
|
|
63
|
-
- **Answer questions.** A question or a finding from the dispatch arrives as a new turn. Answer it yourself, or ask the user when the decision is theirs, then send the answer back.
|
|
64
|
-
- **Steer.** Send a short, self-contained message to the dispatch's name. It arrives between the receiver's steps, never inside a subagent or a running workflow.
|
|
65
|
-
- **Limits.** A session in another permission mode can hold a message until its user approves it, so anything the result depends on goes in the report. A dispatch that restarts ends as `exited` and cannot report, so its result comes back as a message.
|
|
66
|
-
|
|
67
|
-
## If you are the started session
|
|
68
|
-
|
|
69
|
-
Your prompt begins with `[cck dispatch <id>]` and holds your instructions: the peer to ask, and with a report the exact `dispatch done` command. Follow them. Without that line, the prompt is the task alone.
|
|
70
|
-
|
|
71
|
-
- Ask the peer with `SendMessage` when you need a decision or find something that changes the task, and keep working on what does not depend on the answer.
|
|
72
|
-
- The summary is three sentences: what changed, what you found, what remains. Use `--summary-file` if it needs quotes.
|
|
73
|
-
- After you report, a message from the peer is a new request: answer it.
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# Tracks subagent lifecycle: one append-only JSONL file per agent, grouped by session
|
|
3
|
-
# Layout: <CLAUDE_CONFIG_DIR or ~/.claude>/.cck/agent-activity/{sessionId}/{agentId}.jsonl
|
|
4
|
-
# Each line is a lifecycle event (start | idle | stop). Server folds last-line-wins.
|
|
5
|
-
|
|
6
|
-
INPUT=$(cat)
|
|
7
|
-
|
|
8
|
-
# Single jq call to extract all routing fields
|
|
9
|
-
eval "$(echo "$INPUT" | jq -r '
|
|
10
|
-
@sh "SESSION_ID=\(.session_id // "")",
|
|
11
|
-
@sh "AGENT_ID=\(.agent_id // "")",
|
|
12
|
-
@sh "EVENT=\(.hook_event_name // "")",
|
|
13
|
-
@sh "TOOL_NAME=\(.tool_name // "")",
|
|
14
|
-
@sh "AGENT_TYPE_RAW=\(.agent_type // "")",
|
|
15
|
-
@sh "TEAMMATE_NAME=\(.teammate_name // "")"
|
|
16
|
-
')"
|
|
17
|
-
|
|
18
|
-
[ -z "$SESSION_ID" ] && exit 0
|
|
19
|
-
|
|
20
|
-
CCK_ACTIVITY="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/.cck/agent-activity"
|
|
21
|
-
|
|
22
|
-
# Map session to custom task list on session start
|
|
23
|
-
if [ "$EVENT" = "SessionStart" ]; then
|
|
24
|
-
TASK_LIST_ID="${CLAUDE_CODE_TASK_LIST_ID:-}"
|
|
25
|
-
if [ -n "$TASK_LIST_ID" ]; then
|
|
26
|
-
CWD=$(echo "$INPUT" | jq -r '.cwd // ""')
|
|
27
|
-
MAPS_DIR="$CCK_ACTIVITY/_task-maps"
|
|
28
|
-
mkdir -p "$MAPS_DIR"
|
|
29
|
-
MAP_FILE="$MAPS_DIR/$TASK_LIST_ID.json"
|
|
30
|
-
TMP_FILE="$MAP_FILE.$$"
|
|
31
|
-
TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
32
|
-
EXISTING="{}"
|
|
33
|
-
[ -f "$MAP_FILE" ] && EXISTING=$(cat "$MAP_FILE")
|
|
34
|
-
echo "$EXISTING" | jq -c --arg sid "$SESSION_ID" --arg cwd "$CWD" --arg ts "$TS" \
|
|
35
|
-
'.[$sid] = {project: $cwd, updatedAt: $ts}' > "$TMP_FILE" && mv "$TMP_FILE" "$MAP_FILE"
|
|
36
|
-
fi
|
|
37
|
-
exit 0
|
|
38
|
-
fi
|
|
39
|
-
|
|
40
|
-
# Stop: the session finished its turn. The marker means unread; the board deletes it on open.
|
|
41
|
-
if [ "$EVENT" = "Stop" ]; then
|
|
42
|
-
DIR="$CCK_ACTIVITY/$SESSION_ID"
|
|
43
|
-
[ -d "$DIR" ] || mkdir -p "$DIR"
|
|
44
|
-
: > "$DIR/_stop.json"
|
|
45
|
-
exit 0
|
|
46
|
-
fi
|
|
47
|
-
|
|
48
|
-
# PostToolUse / non-waiting PreToolUse: clear waiting state, and unread since the session is working again
|
|
49
|
-
if [ "$EVENT" = "PostToolUse" ] || { [ "$EVENT" = "PreToolUse" ] && [ "$TOOL_NAME" != "AskUserQuestion" ] && [ "$TOOL_NAME" != "ExitPlanMode" ]; }; then
|
|
50
|
-
rm -f "$CCK_ACTIVITY/$SESSION_ID/_waiting.json" "$CCK_ACTIVITY/$SESSION_ID/_stop.json"
|
|
51
|
-
[ "$EVENT" = "PostToolUse" ] && exit 0
|
|
52
|
-
fi
|
|
53
|
-
|
|
54
|
-
# EnterPlanMode has no waiting semantics — skip
|
|
55
|
-
[ "$TOOL_NAME" = "EnterPlanMode" ] && exit 0
|
|
56
|
-
|
|
57
|
-
# Legacy-config shim: shipped hooks.json routes PermissionRequest to
|
|
58
|
-
# approval-gate.sh (which owns the id-bearing marker) and registers no
|
|
59
|
-
# PreToolUse, so the suppression and writer below only run for configs that
|
|
60
|
-
# still route those events here. Suppressing question/plan PermissionRequest
|
|
61
|
-
# keeps such a config from overwriting the gate's marker with an id-less one.
|
|
62
|
-
if [ "$EVENT" = "PermissionRequest" ] && { [ "$TOOL_NAME" = "AskUserQuestion" ] || [ "$TOOL_NAME" = "ExitPlanMode" ]; }; then
|
|
63
|
-
exit 0
|
|
64
|
-
fi
|
|
65
|
-
|
|
66
|
-
# Waiting-for-user events → write _waiting.json marker
|
|
67
|
-
if [ "$EVENT" = "PermissionRequest" ] || { [ "$EVENT" = "PreToolUse" ] && { [ "$TOOL_NAME" = "AskUserQuestion" ] || [ "$TOOL_NAME" = "ExitPlanMode" ]; }; }; then
|
|
68
|
-
DIR="$CCK_ACTIVITY/$SESSION_ID"
|
|
69
|
-
mkdir -p "$DIR"
|
|
70
|
-
KIND="permission"
|
|
71
|
-
[ "$EVENT" = "PreToolUse" ] && KIND="question"
|
|
72
|
-
TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
73
|
-
echo "$INPUT" | jq -c --arg kind "$KIND" --arg ts "$TS" '{
|
|
74
|
-
status: "waiting",
|
|
75
|
-
kind: $kind,
|
|
76
|
-
toolName: (.tool_name // "unknown"),
|
|
77
|
-
toolInput: ((.tool_input | tostring) // ""),
|
|
78
|
-
timestamp: $ts
|
|
79
|
-
}' > "$DIR/_waiting.json"
|
|
80
|
-
exit 0
|
|
81
|
-
fi
|
|
82
|
-
|
|
83
|
-
# TeammateIdle has no agent_id — resolve via name→id mapping file
|
|
84
|
-
if [ "$EVENT" = "TeammateIdle" ] && [ -z "$AGENT_ID" ] && [ -n "$TEAMMATE_NAME" ]; then
|
|
85
|
-
DIR="$CCK_ACTIVITY/$SESSION_ID"
|
|
86
|
-
MAP_FILE="$DIR/_name-${TEAMMATE_NAME}.id"
|
|
87
|
-
[ ! -f "$MAP_FILE" ] && exit 0
|
|
88
|
-
AGENT_ID=$(cat "$MAP_FILE")
|
|
89
|
-
[ -z "$AGENT_ID" ] && exit 0
|
|
90
|
-
FILE="$DIR/$AGENT_ID.jsonl"
|
|
91
|
-
TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
92
|
-
echo "{\"agentId\":\"$AGENT_ID\",\"type\":\"$TEAMMATE_NAME\",\"event\":\"idle\",\"status\":\"idle\",\"updatedAt\":\"$TS\"}" >> "$FILE"
|
|
93
|
-
exit 0
|
|
94
|
-
fi
|
|
95
|
-
|
|
96
|
-
[ -z "$AGENT_ID" ] && exit 0
|
|
97
|
-
|
|
98
|
-
DIR="$CCK_ACTIVITY/$SESSION_ID"
|
|
99
|
-
FILE="$DIR/$AGENT_ID.jsonl"
|
|
100
|
-
|
|
101
|
-
# On Start: skip if no type (internal agents like AskUserQuestion)
|
|
102
|
-
# On Stop/Idle: only skip if no existing file (never tracked)
|
|
103
|
-
if [ -z "$AGENT_TYPE_RAW" ]; then
|
|
104
|
-
if [ "$EVENT" = "SubagentStart" ]; then
|
|
105
|
-
exit 0
|
|
106
|
-
elif [ ! -f "$FILE" ]; then
|
|
107
|
-
exit 0
|
|
108
|
-
fi
|
|
109
|
-
fi
|
|
110
|
-
|
|
111
|
-
mkdir -p "$DIR"
|
|
112
|
-
TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
113
|
-
|
|
114
|
-
if [ "$EVENT" = "SubagentStart" ]; then
|
|
115
|
-
echo "{\"agentId\":\"$AGENT_ID\",\"type\":\"$AGENT_TYPE_RAW\",\"event\":\"start\",\"status\":\"active\",\"startedAt\":\"$TS\",\"updatedAt\":\"$TS\"}" >> "$FILE"
|
|
116
|
-
# Mapping always points at latest agent of this type (used by TeammateIdle resolution).
|
|
117
|
-
if [ -n "$AGENT_TYPE_RAW" ]; then
|
|
118
|
-
echo -n "$AGENT_ID" > "$DIR/_name-${AGENT_TYPE_RAW}.id"
|
|
119
|
-
fi
|
|
120
|
-
|
|
121
|
-
elif [ "$EVENT" = "SubagentStop" ]; then
|
|
122
|
-
# Omit empty type: the server folds lines last-key-wins, so an empty type
|
|
123
|
-
# here would clobber the type recorded by the start line
|
|
124
|
-
echo "$INPUT" | jq -c \
|
|
125
|
-
--arg id "$AGENT_ID" --arg type "$AGENT_TYPE_RAW" --arg ts "$TS" \
|
|
126
|
-
'{agentId: $id, event: "stop", status: "stopped",
|
|
127
|
-
lastMessage: (.last_assistant_message // ""), stoppedAt: $ts, updatedAt: $ts}
|
|
128
|
-
+ (if $type == "" then {} else {type: $type} end)' \
|
|
129
|
-
>> "$FILE"
|
|
130
|
-
|
|
131
|
-
elif [ "$EVENT" = "TeammateIdle" ]; then
|
|
132
|
-
TYPE_FIELD=""
|
|
133
|
-
[ -n "$AGENT_TYPE_RAW" ] && TYPE_FIELD="\"type\":\"$AGENT_TYPE_RAW\","
|
|
134
|
-
echo "{\"agentId\":\"$AGENT_ID\",${TYPE_FIELD}\"event\":\"idle\",\"status\":\"idle\",\"updatedAt\":\"$TS\"}" >> "$FILE"
|
|
135
|
-
fi
|
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# Blocking approval gate: lets the cck board answer a permission ask or an
|
|
3
|
-
# AskUserQuestion. Always writes the _waiting.json marker first (badge behavior
|
|
4
|
-
# is unchanged when the feature is off), then — unless config.json opts out, and
|
|
5
|
-
# only while the board's server is alive — waits for a decision file written by the server.
|
|
6
|
-
#
|
|
7
|
-
# Contract (_plans/cck-ui-approvals/decisions.md), rooted at <CLAUDE_CONFIG_DIR or ~/.claude>/.cck:
|
|
8
|
-
# marker agent-activity/<sid>/_waiting.json (D8: + id, cwd, permissionSuggestions)
|
|
9
|
-
# decision agent-activity/<sid>/_decision-<id>.json (server writes it, Phase 3)
|
|
10
|
-
# config config.json {approvals: {enabled, mode, waitSeconds}} (on by default; absent = defaults)
|
|
11
|
-
# liveness server.json {port, pid} (D1: a dead board costs nothing)
|
|
12
|
-
#
|
|
13
|
-
# First writer wins (D5): a terminal answer deletes the marker (this gate, via
|
|
14
|
-
# the session registry, or PostToolUse) and this gate exits silently; a decision arriving after the tool already ran
|
|
15
|
-
# is discarded by Claude Code, so a losing write on either side is harmless.
|
|
16
|
-
|
|
17
|
-
INPUT=$(cat)
|
|
18
|
-
|
|
19
|
-
eval "$(echo "$INPUT" | jq -r '
|
|
20
|
-
@sh "SESSION_ID=\(.session_id // "")",
|
|
21
|
-
@sh "EVENT=\(.hook_event_name // "")",
|
|
22
|
-
@sh "TOOL_NAME=\(.tool_name // "")"
|
|
23
|
-
')"
|
|
24
|
-
|
|
25
|
-
[ -z "$SESSION_ID" ] && exit 0
|
|
26
|
-
|
|
27
|
-
# AskUserQuestion and ExitPlanMode gate on PermissionRequest, not PreToolUse:
|
|
28
|
-
# the TUI question and plan dialogs render ~10 s in while a PermissionRequest
|
|
29
|
-
# hook blocks (first writer wins, like permissions), but stay frozen for the
|
|
30
|
-
# whole wait during a PreToolUse hook — measured live (#42, #40). Suppress the
|
|
31
|
-
# PreToolUse double-fire in case a stale hooks.json still registers it.
|
|
32
|
-
if [ "$EVENT" = "PreToolUse" ]; then
|
|
33
|
-
exit 0
|
|
34
|
-
fi
|
|
35
|
-
|
|
36
|
-
KIND="permission"
|
|
37
|
-
[ "$TOOL_NAME" = "AskUserQuestion" ] && KIND="question"
|
|
38
|
-
[ "$TOOL_NAME" = "ExitPlanMode" ] && KIND="plan"
|
|
39
|
-
|
|
40
|
-
CCK_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/.cck"
|
|
41
|
-
DIR="$CCK_DIR/agent-activity/$SESSION_ID"
|
|
42
|
-
MARKER="$DIR/_waiting.json"
|
|
43
|
-
mkdir -p "$DIR"
|
|
44
|
-
|
|
45
|
-
# uuidgen is missing on some Git Bash installs; uniqueness only has to hold
|
|
46
|
-
# across the asks of one session, so a timestamp compound is enough
|
|
47
|
-
REQ_ID=$(uuidgen 2>/dev/null) || REQ_ID="$(date +%s%N)-$$-$RANDOM"
|
|
48
|
-
|
|
49
|
-
TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
50
|
-
echo "$INPUT" | jq -c --arg kind "$KIND" --arg ts "$TS" --arg id "$REQ_ID" '{
|
|
51
|
-
status: "waiting",
|
|
52
|
-
kind: $kind,
|
|
53
|
-
id: $id,
|
|
54
|
-
toolName: (.tool_name // "unknown"),
|
|
55
|
-
toolInput: ((.tool_input | tostring) // ""),
|
|
56
|
-
cwd: (.cwd // ""),
|
|
57
|
-
permissionSuggestions: (.permission_suggestions // []),
|
|
58
|
-
timestamp: $ts
|
|
59
|
-
}' > "$MARKER"
|
|
60
|
-
|
|
61
|
-
# Every exit below leaves the marker in place for the badge; agent-spy.sh's
|
|
62
|
-
# PostToolUse (or the server's TTL) retires it, exactly as before this feature.
|
|
63
|
-
|
|
64
|
-
# Defaults mirror lib/approvals.js — keep in sync. A missing or unparseable
|
|
65
|
-
# config.json means defaults, i.e. the gate is on.
|
|
66
|
-
ENABLED="true"
|
|
67
|
-
MODE="permission+question"
|
|
68
|
-
WAIT_SECONDS=""
|
|
69
|
-
CONFIG="$CCK_DIR/config.json"
|
|
70
|
-
if [ -f "$CONFIG" ]; then
|
|
71
|
-
eval "$(jq -r '
|
|
72
|
-
@sh "ENABLED=\(if .approvals.enabled == false then "false" else "true" end)",
|
|
73
|
-
@sh "MODE=\(.approvals.mode // "permission+question")",
|
|
74
|
-
@sh "WAIT_SECONDS=\(.approvals.waitSeconds // "")"
|
|
75
|
-
' < "$CONFIG" 2>/dev/null)"
|
|
76
|
-
fi
|
|
77
|
-
[ "$ENABLED" = "false" ] && exit 0
|
|
78
|
-
|
|
79
|
-
if [ "$KIND" = "question" ] && [ "$MODE" != "permission+question" ]; then
|
|
80
|
-
exit 0
|
|
81
|
-
fi
|
|
82
|
-
|
|
83
|
-
case "$WAIT_SECONDS" in *[!0-9]* | "") WAIT_SECONDS=1800 ;; esac
|
|
84
|
-
# PERMISSION_TTL_MS hides the card at 30 min — waiting longer than the UI can
|
|
85
|
-
# show the ask is strictly worse than giving up (D11)
|
|
86
|
-
[ "$WAIT_SECONDS" -gt 1800 ] && WAIT_SECONDS=1800
|
|
87
|
-
|
|
88
|
-
SERVER_INFO="$CCK_DIR/server.json"
|
|
89
|
-
|
|
90
|
-
# A TCP connect beats a pid probe: it proves the board is actually serving, and
|
|
91
|
-
# it works in the stripped environment Claude Code spawns hooks into, where
|
|
92
|
-
# kill -0 cannot see native Windows pids and ps may be missing from PATH. The
|
|
93
|
-
# port is re-read on every probe, not cached: under the hub every sub-app binds
|
|
94
|
-
# an ephemeral port, so a board that restarts mid-wait comes back on a new one.
|
|
95
|
-
# $(<file) and =~ keep the probe free of jq and cat spawns (~280 ms each here).
|
|
96
|
-
board_alive() {
|
|
97
|
-
local raw port
|
|
98
|
-
raw=$(<"$SERVER_INFO") 2>/dev/null || return 1
|
|
99
|
-
[[ $raw =~ \"port\"[[:space:]]*:[[:space:]]*([0-9]+) ]] || return 1
|
|
100
|
-
port=${BASH_REMATCH[1]}
|
|
101
|
-
(: < "/dev/tcp/127.0.0.1/$port") 2>/dev/null
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
DECISION="$DIR/_decision-$REQ_ID.json"
|
|
105
|
-
# EPOCHSECONDS (bash 5) keeps the poll loop free of `date` spawns
|
|
106
|
-
DEADLINE=$((EPOCHSECONDS + WAIT_SECONDS))
|
|
107
|
-
# Probing once up front made one unreachable moment disarm the whole ask, so a
|
|
108
|
-
# board restarting on a new port (every hub-spawned sub-app binds an ephemeral
|
|
109
|
-
# one) took the ask with it. Probe on a cadence instead and only give up once
|
|
110
|
-
# the board has been gone for the whole grace. The terminal prompt stays live
|
|
111
|
-
# throughout either way.
|
|
112
|
-
BOARD_PROBE_SECONDS=5
|
|
113
|
-
BOARD_GRACE_SECONDS=15
|
|
114
|
-
NEXT_PROBE=0
|
|
115
|
-
UNREACHABLE_SINCE=0
|
|
116
|
-
|
|
117
|
-
# Claude Code's live-session registry flips from "waiting" the moment the
|
|
118
|
-
# terminal prompt is answered, while PostToolUse waits for the tool to finish
|
|
119
|
-
# and never fires on a deny. Undocumented, so a missing file or field leaves
|
|
120
|
-
# PostToolUse in charge, as before.
|
|
121
|
-
SESSIONS_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/sessions"
|
|
122
|
-
REGISTRY=""
|
|
123
|
-
SEEN_WAITING=0
|
|
124
|
-
find_registry() {
|
|
125
|
-
local f raw
|
|
126
|
-
for f in "$SESSIONS_DIR"/*.json; do
|
|
127
|
-
[ -f "$f" ] || continue
|
|
128
|
-
raw=""
|
|
129
|
-
IFS= read -r raw 2>/dev/null < "$f"
|
|
130
|
-
case "$raw" in *"\"sessionId\":\"$SESSION_ID\""*) REGISTRY=$f; return 0 ;; esac
|
|
131
|
-
done
|
|
132
|
-
return 1
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
while :; do
|
|
136
|
-
if [ -f "$DECISION" ]; then
|
|
137
|
-
PAYLOAD=$(cat "$DECISION" 2>/dev/null)
|
|
138
|
-
rm -f "$DECISION" "$MARKER"
|
|
139
|
-
[ -n "$PAYLOAD" ] || exit 0
|
|
140
|
-
if [ "$KIND" = "plan" ] && [ "$(echo "$PAYLOAD" | jq -r '.behavior // "deny"' 2>/dev/null)" = "allow" ]; then
|
|
141
|
-
# A plan allow must echo tool_input as updatedInput — Claude Code
|
|
142
|
-
# >= 2.1.199 silently drops an ExitPlanMode allow without it and falls
|
|
143
|
-
# back to the built-in dialog (measured; plannotator does the same).
|
|
144
|
-
echo "$INPUT" | jq -c --argjson p "$PAYLOAD" \
|
|
145
|
-
'{hookSpecificOutput: {hookEventName: "PermissionRequest",
|
|
146
|
-
decision: ({behavior: "allow", updatedInput: (.tool_input // {})}
|
|
147
|
-
+ (if $p.updatedPermissions then {updatedPermissions: $p.updatedPermissions} else {} end))}}' 2>/dev/null
|
|
148
|
-
elif [ "$KIND" != "question" ]; then
|
|
149
|
-
# Permission asks and plan denies share this shaping — the server sends
|
|
150
|
-
# only behavior+message for a plan deny. PermissionRequest decisions must
|
|
151
|
-
# ride hookSpecificOutput — a top-level {decision} is the approve/block
|
|
152
|
-
# string channel and an object there throws
|
|
153
|
-
echo "$PAYLOAD" | jq -c '{hookSpecificOutput: {hookEventName: "PermissionRequest",
|
|
154
|
-
decision: ({behavior: (.behavior // "deny")}
|
|
155
|
-
+ (if .message then {message: .message} else {} end)
|
|
156
|
-
+ (if .updatedInput then {updatedInput: .updatedInput} else {} end)
|
|
157
|
-
+ (if .updatedPermissions then {updatedPermissions: .updatedPermissions} else {} end))}}' 2>/dev/null
|
|
158
|
-
else
|
|
159
|
-
# updatedInput replaces the whole input object, so echo every field and
|
|
160
|
-
# add the answers Claude never fills in itself (D6). Questions ride the
|
|
161
|
-
# PermissionRequest channel now (#42) — allow with the answers filled in.
|
|
162
|
-
ANSWERS=$(echo "$PAYLOAD" | jq -c '.answers // empty' 2>/dev/null)
|
|
163
|
-
[ -n "$ANSWERS" ] || exit 0
|
|
164
|
-
echo "$INPUT" | jq -c --argjson answers "$ANSWERS" \
|
|
165
|
-
'{hookSpecificOutput: {hookEventName: "PermissionRequest",
|
|
166
|
-
decision: {behavior: "allow", updatedInput: ((.tool_input // {}) + {answers: $answers})}}}' 2>/dev/null
|
|
167
|
-
fi
|
|
168
|
-
exit 0
|
|
169
|
-
fi
|
|
170
|
-
|
|
171
|
-
# Marker gone = answered in the terminal (PostToolUse fires ~23 ms after — D5);
|
|
172
|
-
# id changed = displaced by a newer ask (D8). Either way this gate is over.
|
|
173
|
-
# Builtin read + substring match instead of jq: a spawn costs ~280 ms on
|
|
174
|
-
# Windows (O2), and the marker is single-line jq -c output with a known id.
|
|
175
|
-
IFS= read -r CUR_MARKER < "$MARKER" 2>/dev/null || exit 0
|
|
176
|
-
case "$CUR_MARKER" in *"\"id\":\"$REQ_ID\""*) ;; *) exit 0 ;; esac
|
|
177
|
-
|
|
178
|
-
if [ -n "$REGISTRY" ]; then
|
|
179
|
-
REG_RAW=""
|
|
180
|
-
IFS= read -r REG_RAW 2>/dev/null < "$REGISTRY"
|
|
181
|
-
if [[ $REG_RAW =~ \"status\":\"([a-z]+)\" ]]; then
|
|
182
|
-
if [ "${BASH_REMATCH[1]}" = "waiting" ]; then
|
|
183
|
-
SEEN_WAITING=1
|
|
184
|
-
elif [ "$SEEN_WAITING" -eq 1 ]; then
|
|
185
|
-
rm -f "$MARKER"
|
|
186
|
-
exit 0
|
|
187
|
-
fi
|
|
188
|
-
fi
|
|
189
|
-
fi
|
|
190
|
-
|
|
191
|
-
if [ "$EPOCHSECONDS" -ge "$NEXT_PROBE" ]; then
|
|
192
|
-
NEXT_PROBE=$((EPOCHSECONDS + BOARD_PROBE_SECONDS))
|
|
193
|
-
[ -n "$REGISTRY" ] || find_registry
|
|
194
|
-
# No beacon at all = no board on this config dir, now or a moment ago: give
|
|
195
|
-
# up at once, exactly as before. A beacon whose port is closed is the
|
|
196
|
-
# restart window instead, so that one gets the grace.
|
|
197
|
-
[ -f "$SERVER_INFO" ] || exit 0
|
|
198
|
-
if board_alive; then
|
|
199
|
-
UNREACHABLE_SINCE=0
|
|
200
|
-
else
|
|
201
|
-
[ "$UNREACHABLE_SINCE" -eq 0 ] && UNREACHABLE_SINCE=$EPOCHSECONDS
|
|
202
|
-
[ $((EPOCHSECONDS - UNREACHABLE_SINCE)) -ge "$BOARD_GRACE_SECONDS" ] && exit 0
|
|
203
|
-
fi
|
|
204
|
-
fi
|
|
205
|
-
|
|
206
|
-
[ "$EPOCHSECONDS" -ge "$DEADLINE" ] && exit 0
|
|
207
|
-
sleep 0.5
|
|
208
|
-
done
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# Statusline spy: writes raw context data for kanban dashboard, passes input through
|
|
3
|
-
# Layout: <CLAUDE_CONFIG_DIR or ~/.claude>/.cck/context-status/{sessionId}.json
|
|
4
|
-
#
|
|
5
|
-
# Usage: pipe before your statusline command:
|
|
6
|
-
# "command": "~/.claude/hooks/context-status.sh | npx -y ccstatusline@latest"
|
|
7
|
-
|
|
8
|
-
INPUT=$(cat)
|
|
9
|
-
|
|
10
|
-
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // ""')
|
|
11
|
-
if [ -n "$SESSION_ID" ]; then
|
|
12
|
-
DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/.cck/context-status"
|
|
13
|
-
mkdir -p "$DIR"
|
|
14
|
-
echo "$INPUT" > "$DIR/$SESSION_ID.json"
|
|
15
|
-
fi
|
|
16
|
-
|
|
17
|
-
# Pass through original input for downstream statusline
|
|
18
|
-
echo "$INPUT"
|