@orchestrator-claude/cli 3.12.0 → 3.12.2

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/dist/index.d.ts CHANGED
@@ -12,5 +12,5 @@
12
12
  /**
13
13
  * CLI version
14
14
  */
15
- export declare const CLI_VERSION = "3.12.0";
15
+ export declare const CLI_VERSION = "3.12.2";
16
16
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -24,7 +24,7 @@ import { OutputFormatter } from './formatters/OutputFormatter.js';
24
24
  /**
25
25
  * CLI version
26
26
  */
27
- export const CLI_VERSION = '3.12.0';
27
+ export const CLI_VERSION = '3.12.2';
28
28
  /**
29
29
  * Main CLI function
30
30
  */
@@ -1,322 +1,203 @@
1
1
  #!/bin/bash
2
- # Post-Phase Checkpoint Hook
3
- # Automatically creates git checkpoints when workflow phases complete
2
+ # Post-Phase Checkpoint Hook (RFC-012 compatible)
3
+ # Registers a checkpoint in PostgreSQL via the Orchestrator REST API.
4
+ # Replaces the deprecated orchestrator-index.json approach (TD-105 F-04).
4
5
  #
5
6
  # Triggered by: PostToolUse on Task tool (after agent completes)
6
- # Behavior: Detects phase transitions and creates git commits
7
+ # Behavior: Infers the just-completed phase from the active workflow state
8
+ # and registers a checkpoint via the REST API.
7
9
  #
8
- # Requirements:
9
- # - jq (for JSON parsing)
10
- # - git (for checkpoint commits)
10
+ # Required env vars:
11
+ # ORCHESTRATOR_API_URL - Base URL (e.g. http://localhost:3000)
12
+ # ORCHESTRATOR_PROJECT_TOKEN - Bearer token for API authentication
11
13
  #
12
- # Debug mode: ORCH_CHECKPOINT_DEBUG=1 to enable verbose logging
14
+ # Optional env vars:
15
+ # ORCHESTRATOR_PROJECT_ID - Project ID for project-scoped workflow query
16
+ # ORCH_CHECKPOINT_DEBUG=1 - Enable verbose logging
17
+ #
18
+ # Always exits 0 (non-blocking).
13
19
 
14
- set -e
20
+ set -euo pipefail
15
21
 
16
- # Configuration
17
22
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
18
- PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
19
- INDEX_FILE="$PROJECT_ROOT/.orchestrator/orchestrator-index.json"
20
- STATE_DIR="$PROJECT_ROOT/.orchestrator/.state"
21
- LOG_FILE="$STATE_DIR/checkpoint-hook.log"
22
- LAST_PHASE_FILE="$STATE_DIR/last-checkpointed-phase"
23
-
24
- # Ensure state directory exists
25
- mkdir -p "$STATE_DIR"
26
-
27
- # Debug mode
23
+ STATE_DIR="$SCRIPT_DIR/../../.orchestrator/.state"
24
+ LOG_FILE="${STATE_DIR}/checkpoint-hook.log"
25
+ LAST_PHASE_FILE="${STATE_DIR}/last-checkpointed-phase"
28
26
  DEBUG="${ORCH_CHECKPOINT_DEBUG:-0}"
29
27
 
30
- # Logging functions
31
- log_debug() {
32
- if [ "$DEBUG" = "1" ]; then
33
- echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] [DEBUG] $1" >> "$LOG_FILE"
34
- echo "[DEBUG] $1" >&2
35
- fi
36
- }
37
-
38
- log_info() {
39
- echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] [INFO] $1" >> "$LOG_FILE"
40
- if [ "$DEBUG" = "1" ]; then
41
- echo "[INFO] $1" >&2
42
- fi
43
- }
28
+ mkdir -p "$STATE_DIR"
44
29
 
45
- log_warn() {
46
- echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] [WARN] $1" >> "$LOG_FILE"
47
- echo "[WARN] $1" >&2
48
- }
30
+ log_info() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] [INFO] $1" >> "$LOG_FILE"; }
31
+ log_warn() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] [WARN] $1" >> "$LOG_FILE"; echo "[WARN] $1" >&2; }
32
+ log_debug() { [ "$DEBUG" = "1" ] && echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] [DEBUG] $1" >> "$LOG_FILE" || true; }
49
33
 
50
- log_error() {
51
- echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] [ERROR] $1" >> "$LOG_FILE"
52
- echo "[ERROR] $1" >&2
53
- }
34
+ # ── prerequisites ─────────────────────────────────────────────────────────────
54
35
 
55
- # Check prerequisites
56
36
  check_prerequisites() {
57
- # Check jq
58
- if ! command -v jq &> /dev/null; then
59
- log_error "jq is not installed. Checkpoints disabled."
37
+ if ! command -v curl &>/dev/null; then
38
+ log_warn "curl not found. Checkpoints disabled."
60
39
  return 1
61
40
  fi
62
-
63
- # Check git
64
- if ! command -v git &> /dev/null; then
65
- log_error "git is not installed. Checkpoints disabled."
41
+ if ! command -v jq &>/dev/null; then
42
+ log_warn "jq not found. Checkpoints disabled."
66
43
  return 1
67
44
  fi
68
-
69
- # Check if we're in a git repo
70
- if ! git -C "$PROJECT_ROOT" rev-parse --is-inside-work-tree &> /dev/null; then
71
- log_warn "Not a git repository. Checkpoints disabled."
45
+ if [ -z "${ORCHESTRATOR_API_URL:-}" ]; then
46
+ log_debug "ORCHESTRATOR_API_URL not set. Checkpoints disabled."
72
47
  return 1
73
48
  fi
74
-
75
- # Check index file
76
- if [ ! -f "$INDEX_FILE" ]; then
77
- log_debug "No orchestrator-index.json found. Skipping."
49
+ if [ -z "${ORCHESTRATOR_PROJECT_TOKEN:-}" ]; then
50
+ log_debug "ORCHESTRATOR_PROJECT_TOKEN not set. Checkpoints disabled."
78
51
  return 1
79
52
  fi
80
-
81
53
  return 0
82
54
  }
83
55
 
84
- # Get current phase from orchestrator-index.json
85
- get_current_phase() {
86
- local phase
87
- phase=$(jq -r '.activeWorkflow.currentPhase // empty' "$INDEX_FILE" 2>/dev/null)
88
- echo "$phase"
56
+ # ── API helpers ────────────────────────────────────────────────────────────────
57
+
58
+ get_active_workflow() {
59
+ local project_id="${ORCHESTRATOR_PROJECT_ID:-}"
60
+ local url="${ORCHESTRATOR_API_URL}/api/v1/workflows?status=in_progress&limit=1"
61
+ [ -n "$project_id" ] && url="${url}&projectId=${project_id}"
62
+
63
+ local tmpfile
64
+ tmpfile=$(mktemp 2>/dev/null) || return 1
65
+
66
+ curl -sf \
67
+ -H "Authorization: Bearer ${ORCHESTRATOR_PROJECT_TOKEN}" \
68
+ -H "Content-Type: application/json" \
69
+ "${url}" \
70
+ -o "$tmpfile" 2>/dev/null || { rm -f "$tmpfile"; return 1; }
71
+
72
+ local result
73
+ result=$(node -e "
74
+ const fs=require('fs');
75
+ try {
76
+ const d=fs.readFileSync('${tmpfile}','utf8');
77
+ const j=JSON.parse(d);
78
+ const wf=Array.isArray(j)?j[0]:j;
79
+ if(wf&&wf.id){ process.stdout.write(JSON.stringify(wf)); process.exit(0); }
80
+ process.exit(1);
81
+ } catch { process.exit(1); }" 2>/dev/null)
82
+ local rc=$?
83
+ rm -f "$tmpfile"
84
+ [ $rc -eq 0 ] && echo "$result" || return 1
89
85
  }
90
86
 
91
- # Get workflow ID
92
- get_workflow_id() {
93
- local wf_id
94
- wf_id=$(jq -r '.activeWorkflow.id // empty' "$INDEX_FILE" 2>/dev/null)
95
- echo "$wf_id"
87
+ create_checkpoint() {
88
+ local workflow_id="$1"
89
+ local phase="$2"
90
+ local description="$3"
91
+
92
+ local body
93
+ body=$(jq -n \
94
+ --arg phase "$phase" \
95
+ --arg desc "$description" \
96
+ '{ phase: $phase, description: $desc }')
97
+
98
+ local http_code
99
+ http_code=$(curl -sf -o /dev/null -w "%{http_code}" \
100
+ -X POST \
101
+ -H "Authorization: Bearer ${ORCHESTRATOR_PROJECT_TOKEN}" \
102
+ -H "Content-Type: application/json" \
103
+ -d "$body" \
104
+ "${ORCHESTRATOR_API_URL}/api/v1/workflows/${workflow_id}/checkpoints" 2>/dev/null) || echo ""
105
+
106
+ echo "$http_code"
96
107
  }
97
108
 
98
- # Get workflow status
99
- get_workflow_status() {
100
- local status
101
- status=$(jq -r '.activeWorkflow.status // empty' "$INDEX_FILE" 2>/dev/null)
102
- echo "$status"
109
+ # ── dedup guard ────────────────────────────────────────────────────────────────
110
+
111
+ get_saved_last_phase() {
112
+ [ -f "$LAST_PHASE_FILE" ] && cat "$LAST_PHASE_FILE" || echo ""
103
113
  }
104
114
 
105
- # Get last checkpointed phase from checkpoints array (one with commitHash)
106
- get_last_checkpointed_phase() {
107
- local phase
108
- # Find the most recent checkpoint that has a commitHash (indicating it was git-committed)
109
- phase=$(jq -r '
110
- [.checkpoints[] | select(.commitHash != null and .commitHash != "")]
111
- | sort_by(.createdAt)
112
- | last
113
- | .phase // empty
114
- ' "$INDEX_FILE" 2>/dev/null)
115
- echo "$phase"
115
+ save_last_phase() {
116
+ echo "$1" > "$LAST_PHASE_FILE"
116
117
  }
117
118
 
118
- # Determine completed phase based on current phase
119
- # When we see currentPhase = PLAN, it means SPECIFY just completed
120
- get_completed_phase() {
121
- local current_phase="$1"
122
- local workflow_status="$2"
119
+ # ── phase inference ────────────────────────────────────────────────────────────
123
120
 
124
- case "$workflow_status" in
125
- "completed")
126
- echo "IMPLEMENT"
127
- return
128
- ;;
121
+ # When workflow is at PLAN, it means SPECIFY just completed, etc.
122
+ get_completed_phase() {
123
+ local current="$1"
124
+ local status="$2"
125
+ case "$status" in
126
+ "completed") echo "implement"; return ;;
129
127
  esac
130
-
131
- case "$current_phase" in
132
- "PLAN")
133
- echo "SPECIFY"
134
- ;;
135
- "TASKS")
136
- echo "PLAN"
137
- ;;
138
- "IMPLEMENT")
139
- echo "TASKS"
140
- ;;
141
- *)
142
- echo ""
143
- ;;
128
+ case "${current,,}" in
129
+ "plan") echo "specify" ;;
130
+ "tasks") echo "plan" ;;
131
+ "implement") echo "tasks" ;;
132
+ *) echo "" ;;
144
133
  esac
145
134
  }
146
135
 
147
- # Create git checkpoint
148
- create_git_checkpoint() {
149
- local phase="$1"
150
- local commit_hash=""
151
-
152
- log_info "Creating git checkpoint for phase: $phase"
153
-
154
- # Stage all changes
155
- git -C "$PROJECT_ROOT" add -A 2>/dev/null || {
156
- log_warn "Failed to stage changes"
157
- return 1
158
- }
159
-
160
- # Check if there are staged changes
161
- if git -C "$PROJECT_ROOT" diff --cached --quiet 2>/dev/null; then
162
- log_info "No changes to commit for phase: $phase"
163
- echo ""
164
- return 0
165
- fi
166
-
167
- # Create commit with standard message
168
- local commit_msg="[orchestrator] ${phase}: Auto-checkpoint after ${phase} phase"
169
-
170
- if git -C "$PROJECT_ROOT" commit -m "$commit_msg" --no-verify 2>/dev/null; then
171
- commit_hash=$(git -C "$PROJECT_ROOT" rev-parse HEAD 2>/dev/null)
172
- log_info "Checkpoint commit created: $commit_hash"
173
- echo "$commit_hash"
174
- else
175
- log_warn "Failed to create commit"
176
- echo ""
177
- return 1
178
- fi
179
- }
180
-
181
- # Update orchestrator-index.json with checkpoint
182
- update_orchestrator_index() {
183
- local workflow_id="$1"
184
- local phase="$2"
185
- local commit_hash="$3"
186
- local created_at
187
-
188
- created_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
189
- local checkpoint_id="cp_${workflow_id}_$(date +%s)"
190
- local description="Auto-checkpoint after ${phase} phase"
191
-
192
- log_debug "Updating index with checkpoint: $checkpoint_id"
193
-
194
- # Create temp file for atomic update
195
- local temp_file="${INDEX_FILE}.tmp.$$"
196
-
197
- # Use jq to add checkpoint and update statistics
198
- if jq --arg id "$checkpoint_id" \
199
- --arg wfId "$workflow_id" \
200
- --arg phase "$phase" \
201
- --arg hash "$commit_hash" \
202
- --arg date "$created_at" \
203
- --arg desc "$description" '
204
- # Add checkpoint to array
205
- .checkpoints += [{
206
- id: $id,
207
- workflowId: $wfId,
208
- phase: $phase,
209
- commitHash: $hash,
210
- createdAt: $date,
211
- description: $desc
212
- }] |
213
- # Update statistics
214
- .statistics.totalCheckpoints = (.checkpoints | length) |
215
- .statistics.lastActivity = $date
216
- ' "$INDEX_FILE" > "$temp_file" 2>/dev/null; then
217
-
218
- # Validate JSON
219
- if jq empty "$temp_file" 2>/dev/null; then
220
- mv "$temp_file" "$INDEX_FILE"
221
- log_info "Index updated with checkpoint: $checkpoint_id"
222
- return 0
223
- else
224
- log_error "Generated invalid JSON"
225
- rm -f "$temp_file"
226
- return 1
227
- fi
228
- else
229
- log_error "Failed to update index with jq"
230
- rm -f "$temp_file"
231
- return 1
232
- fi
233
- }
234
-
235
- # Save last checkpointed phase
236
- save_last_phase() {
237
- local phase="$1"
238
- echo "$phase" > "$LAST_PHASE_FILE"
239
- }
240
-
241
- # Get saved last phase (from state file, not index)
242
- get_saved_last_phase() {
243
- if [ -f "$LAST_PHASE_FILE" ]; then
244
- cat "$LAST_PHASE_FILE"
245
- else
246
- echo ""
247
- fi
248
- }
136
+ # ── main ───────────────────────────────────────────────────────────────────────
249
137
 
250
- # Main execution
251
138
  main() {
252
139
  log_debug "Hook triggered"
253
140
 
254
- # Check prerequisites
255
141
  if ! check_prerequisites; then
256
- log_debug "Prerequisites not met, exiting"
257
- exit 0 # Don't fail the hook
142
+ exit 0
258
143
  fi
259
144
 
260
- # Get current state
261
- local current_phase
262
- current_phase=$(get_current_phase)
145
+ local workflow_json
146
+ workflow_json=$(get_active_workflow 2>/dev/null) || workflow_json=""
263
147
 
264
- if [ -z "$current_phase" ]; then
265
- log_debug "No active workflow phase found"
148
+ if [ -z "$workflow_json" ]; then
149
+ log_debug "No active workflow found via API."
266
150
  exit 0
267
151
  fi
268
152
 
269
- local workflow_id
270
- workflow_id=$(get_workflow_id)
153
+ local workflow_id current_phase workflow_status
154
+ workflow_id=$(echo "$workflow_json" | jq -r '.id // empty' 2>/dev/null || echo "")
155
+ current_phase=$(echo "$workflow_json" | jq -r '.currentPhase // empty' 2>/dev/null || echo "")
156
+ workflow_status=$(echo "$workflow_json" | jq -r '.status // empty' 2>/dev/null || echo "")
271
157
 
272
- local workflow_status
273
- workflow_status=$(get_workflow_status)
274
-
275
- log_debug "Current phase: $current_phase, Status: $workflow_status, Workflow: $workflow_id"
158
+ if [ -z "$workflow_id" ] || [ -z "$current_phase" ]; then
159
+ log_debug "Could not parse workflow ID or phase."
160
+ exit 0
161
+ fi
276
162
 
277
- # Determine which phase just completed
278
163
  local completed_phase
279
164
  completed_phase=$(get_completed_phase "$current_phase" "$workflow_status")
280
165
 
281
166
  if [ -z "$completed_phase" ]; then
282
- log_debug "No completed phase detected from current phase: $current_phase"
167
+ log_debug "No completed phase inferred from current_phase=${current_phase}."
283
168
  exit 0
284
169
  fi
285
170
 
286
- # Check if we already checkpointed this phase
287
- local last_checkpointed
288
- last_checkpointed=$(get_last_checkpointed_phase)
289
-
290
- local saved_last_phase
291
- saved_last_phase=$(get_saved_last_phase)
292
-
293
- log_debug "Completed phase: $completed_phase, Last checkpointed: $last_checkpointed, Saved: $saved_last_phase"
294
-
295
- # Skip if already checkpointed
296
- if [ "$completed_phase" = "$last_checkpointed" ] || [ "$completed_phase" = "$saved_last_phase" ]; then
297
- log_debug "Phase $completed_phase already checkpointed, skipping"
171
+ # Dedup guard — skip if we already checkpointed this phase
172
+ local saved_last
173
+ saved_last=$(get_saved_last_phase)
174
+ if [ "$completed_phase" = "$saved_last" ]; then
175
+ log_debug "Phase ${completed_phase} already checkpointed. Skipping."
298
176
  exit 0
299
177
  fi
300
178
 
301
- # Create checkpoint
302
- local commit_hash
303
- commit_hash=$(create_git_checkpoint "$completed_phase")
179
+ local description="Auto-checkpoint after ${completed_phase} phase"
180
+ local http_code
181
+ http_code=$(create_checkpoint "$workflow_id" "$completed_phase" "$description")
304
182
 
305
- # Update index (even if no commit, to track the checkpoint)
306
- if [ -n "$commit_hash" ]; then
307
- if update_orchestrator_index "$workflow_id" "$completed_phase" "$commit_hash"; then
183
+ case "$http_code" in
184
+ 201|200)
308
185
  save_last_phase "$completed_phase"
309
- log_info "Checkpoint complete for phase: $completed_phase (commit: $commit_hash)"
310
- else
311
- log_error "Failed to update index for phase: $completed_phase"
312
- fi
313
- else
314
- log_info "No changes to checkpoint for phase: $completed_phase"
315
- save_last_phase "$completed_phase"
316
- fi
186
+ log_info "Checkpoint registered: workflow=${workflow_id}, phase=${completed_phase}"
187
+ ;;
188
+ 409)
189
+ save_last_phase "$completed_phase"
190
+ log_info "Checkpoint already exists (409): phase=${completed_phase}"
191
+ ;;
192
+ "")
193
+ log_warn "API unreachable. Checkpoint skipped for phase=${completed_phase}."
194
+ ;;
195
+ *)
196
+ log_warn "Unexpected HTTP ${http_code} creating checkpoint for phase=${completed_phase}."
197
+ ;;
198
+ esac
317
199
 
318
200
  exit 0
319
201
  }
320
202
 
321
- # Run main function
322
203
  main "$@"
@@ -20,6 +20,13 @@ FILE_PATH=$(orch_json_field "$STDIN_DATA" "tool_input.file_path")
20
20
 
21
21
  orch_log "workflow-guard: file_path=$FILE_PATH"
22
22
 
23
+ # Explicit bypass via env var (for direct implementation without dogfooding)
24
+ if [ "${SKIP_WORKFLOW_GUARD:-}" = "true" ]; then
25
+ orch_log "workflow-guard: ALLOW (SKIP_WORKFLOW_GUARD=true)"
26
+ echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","additionalContext":"Workflow guard bypassed via SKIP_WORKFLOW_GUARD=true."}}'
27
+ exit 0
28
+ fi
29
+
23
30
  # Only guard src/ and tests/ paths (production code)
24
31
  case "$FILE_PATH" in
25
32
  */src/*|*/tests/*)
@@ -42,8 +49,27 @@ esac
42
49
  WORKFLOW_ID=$(orch_get_active_workflow 2>/dev/null) || WORKFLOW_ID=""
43
50
 
44
51
  if [ -n "$WORKFLOW_ID" ]; then
45
- orch_log "workflow-guard: ALLOW (active workflow: $WORKFLOW_ID)"
46
- echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"allow\",\"additionalContext\":\"Active workflow: ${WORKFLOW_ID}. Write allowed.\"}}"
52
+ # Check if running inside a sub-agent (agent_id present) or main agent (absent)
53
+ AGENT_ID=$(orch_json_field "$STDIN_DATA" "agent_id")
54
+
55
+ if [ -n "$AGENT_ID" ]; then
56
+ # Sub-agent writing — ALLOW
57
+ AGENT_TYPE=$(orch_json_field "$STDIN_DATA" "agent_type")
58
+ orch_log "workflow-guard: ALLOW (sub-agent: ${AGENT_TYPE:-unknown}, workflow: $WORKFLOW_ID)"
59
+ echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"allow\",\"additionalContext\":\"Active workflow: ${WORKFLOW_ID}. Sub-agent ${AGENT_TYPE:-unknown} write allowed.\"}}"
60
+ exit 0
61
+ fi
62
+
63
+ # Main agent writing directly — check if SKIP_SUBAGENT_GUARD allows it
64
+ if [ "${SKIP_SUBAGENT_GUARD:-}" = "true" ]; then
65
+ orch_log "workflow-guard: ALLOW (SKIP_SUBAGENT_GUARD=true, workflow: $WORKFLOW_ID)"
66
+ echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"allow\",\"additionalContext\":\"Active workflow: ${WORKFLOW_ID}. Direct write allowed via SKIP_SUBAGENT_GUARD.\"}}"
67
+ exit 0
68
+ fi
69
+
70
+ # Main agent writing directly — DENY, must use sub-agent
71
+ orch_log "workflow-guard: DENY (main agent direct write, no sub-agent invocation)"
72
+ echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"Workflow Guard: Direct code writes are blocked. You must invoke a sub-agent (e.g. implementer) to write code. The sub-agent will have write access.\",\"additionalContext\":\"Use the Agent tool to spawn an implementer sub-agent for code changes. The workflow-guard allows writes only from sub-agents (identified by agent_id in hook input).\"}}"
47
73
  exit 0
48
74
  fi
49
75
 
@@ -64,17 +64,6 @@
64
64
  }
65
65
  ],
66
66
  "PreToolUse": [
67
- {
68
- "matcher": "Task",
69
- "hooks": [
70
- {
71
- "type": "command",
72
- "command": ".claude/hooks/track-agent-invocation.sh start",
73
- "timeout": 5000,
74
- "on_failure": "ignore"
75
- }
76
- ]
77
- },
78
67
  {
79
68
  "matcher": "mcp__orchestrator-tools__advancePhase",
80
69
  "hooks": [
@@ -98,9 +87,22 @@
98
87
  ]
99
88
  }
100
89
  ],
101
- "PostToolUse": [
90
+ "SubagentStart": [
91
+ {
92
+ "matcher": "implementer|specifier|planner|task-generator|reviewer|researcher|orchestrator",
93
+ "hooks": [
94
+ {
95
+ "type": "command",
96
+ "command": ".claude/hooks/track-agent-invocation.sh start",
97
+ "timeout": 5000,
98
+ "on_failure": "ignore"
99
+ }
100
+ ]
101
+ }
102
+ ],
103
+ "SubagentStop": [
102
104
  {
103
- "matcher": "Task",
105
+ "matcher": "implementer|specifier|planner|task-generator|reviewer|researcher|orchestrator",
104
106
  "hooks": [
105
107
  {
106
108
  "type": "command",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orchestrator-claude/cli",
3
- "version": "3.12.0",
3
+ "version": "3.12.2",
4
4
  "description": "Orchestrator CLI - Project scaffolding, migration and management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,322 +1,203 @@
1
1
  #!/bin/bash
2
- # Post-Phase Checkpoint Hook
3
- # Automatically creates git checkpoints when workflow phases complete
2
+ # Post-Phase Checkpoint Hook (RFC-012 compatible)
3
+ # Registers a checkpoint in PostgreSQL via the Orchestrator REST API.
4
+ # Replaces the deprecated orchestrator-index.json approach (TD-105 F-04).
4
5
  #
5
6
  # Triggered by: PostToolUse on Task tool (after agent completes)
6
- # Behavior: Detects phase transitions and creates git commits
7
+ # Behavior: Infers the just-completed phase from the active workflow state
8
+ # and registers a checkpoint via the REST API.
7
9
  #
8
- # Requirements:
9
- # - jq (for JSON parsing)
10
- # - git (for checkpoint commits)
10
+ # Required env vars:
11
+ # ORCHESTRATOR_API_URL - Base URL (e.g. http://localhost:3000)
12
+ # ORCHESTRATOR_PROJECT_TOKEN - Bearer token for API authentication
11
13
  #
12
- # Debug mode: ORCH_CHECKPOINT_DEBUG=1 to enable verbose logging
14
+ # Optional env vars:
15
+ # ORCHESTRATOR_PROJECT_ID - Project ID for project-scoped workflow query
16
+ # ORCH_CHECKPOINT_DEBUG=1 - Enable verbose logging
17
+ #
18
+ # Always exits 0 (non-blocking).
13
19
 
14
- set -e
20
+ set -euo pipefail
15
21
 
16
- # Configuration
17
22
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
18
- PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
19
- INDEX_FILE="$PROJECT_ROOT/.orchestrator/orchestrator-index.json"
20
- STATE_DIR="$PROJECT_ROOT/.orchestrator/.state"
21
- LOG_FILE="$STATE_DIR/checkpoint-hook.log"
22
- LAST_PHASE_FILE="$STATE_DIR/last-checkpointed-phase"
23
-
24
- # Ensure state directory exists
25
- mkdir -p "$STATE_DIR"
26
-
27
- # Debug mode
23
+ STATE_DIR="$SCRIPT_DIR/../../.orchestrator/.state"
24
+ LOG_FILE="${STATE_DIR}/checkpoint-hook.log"
25
+ LAST_PHASE_FILE="${STATE_DIR}/last-checkpointed-phase"
28
26
  DEBUG="${ORCH_CHECKPOINT_DEBUG:-0}"
29
27
 
30
- # Logging functions
31
- log_debug() {
32
- if [ "$DEBUG" = "1" ]; then
33
- echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] [DEBUG] $1" >> "$LOG_FILE"
34
- echo "[DEBUG] $1" >&2
35
- fi
36
- }
37
-
38
- log_info() {
39
- echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] [INFO] $1" >> "$LOG_FILE"
40
- if [ "$DEBUG" = "1" ]; then
41
- echo "[INFO] $1" >&2
42
- fi
43
- }
28
+ mkdir -p "$STATE_DIR"
44
29
 
45
- log_warn() {
46
- echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] [WARN] $1" >> "$LOG_FILE"
47
- echo "[WARN] $1" >&2
48
- }
30
+ log_info() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] [INFO] $1" >> "$LOG_FILE"; }
31
+ log_warn() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] [WARN] $1" >> "$LOG_FILE"; echo "[WARN] $1" >&2; }
32
+ log_debug() { [ "$DEBUG" = "1" ] && echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] [DEBUG] $1" >> "$LOG_FILE" || true; }
49
33
 
50
- log_error() {
51
- echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] [ERROR] $1" >> "$LOG_FILE"
52
- echo "[ERROR] $1" >&2
53
- }
34
+ # ── prerequisites ─────────────────────────────────────────────────────────────
54
35
 
55
- # Check prerequisites
56
36
  check_prerequisites() {
57
- # Check jq
58
- if ! command -v jq &> /dev/null; then
59
- log_error "jq is not installed. Checkpoints disabled."
37
+ if ! command -v curl &>/dev/null; then
38
+ log_warn "curl not found. Checkpoints disabled."
60
39
  return 1
61
40
  fi
62
-
63
- # Check git
64
- if ! command -v git &> /dev/null; then
65
- log_error "git is not installed. Checkpoints disabled."
41
+ if ! command -v jq &>/dev/null; then
42
+ log_warn "jq not found. Checkpoints disabled."
66
43
  return 1
67
44
  fi
68
-
69
- # Check if we're in a git repo
70
- if ! git -C "$PROJECT_ROOT" rev-parse --is-inside-work-tree &> /dev/null; then
71
- log_warn "Not a git repository. Checkpoints disabled."
45
+ if [ -z "${ORCHESTRATOR_API_URL:-}" ]; then
46
+ log_debug "ORCHESTRATOR_API_URL not set. Checkpoints disabled."
72
47
  return 1
73
48
  fi
74
-
75
- # Check index file
76
- if [ ! -f "$INDEX_FILE" ]; then
77
- log_debug "No orchestrator-index.json found. Skipping."
49
+ if [ -z "${ORCHESTRATOR_PROJECT_TOKEN:-}" ]; then
50
+ log_debug "ORCHESTRATOR_PROJECT_TOKEN not set. Checkpoints disabled."
78
51
  return 1
79
52
  fi
80
-
81
53
  return 0
82
54
  }
83
55
 
84
- # Get current phase from orchestrator-index.json
85
- get_current_phase() {
86
- local phase
87
- phase=$(jq -r '.activeWorkflow.currentPhase // empty' "$INDEX_FILE" 2>/dev/null)
88
- echo "$phase"
56
+ # ── API helpers ────────────────────────────────────────────────────────────────
57
+
58
+ get_active_workflow() {
59
+ local project_id="${ORCHESTRATOR_PROJECT_ID:-}"
60
+ local url="${ORCHESTRATOR_API_URL}/api/v1/workflows?status=in_progress&limit=1"
61
+ [ -n "$project_id" ] && url="${url}&projectId=${project_id}"
62
+
63
+ local tmpfile
64
+ tmpfile=$(mktemp 2>/dev/null) || return 1
65
+
66
+ curl -sf \
67
+ -H "Authorization: Bearer ${ORCHESTRATOR_PROJECT_TOKEN}" \
68
+ -H "Content-Type: application/json" \
69
+ "${url}" \
70
+ -o "$tmpfile" 2>/dev/null || { rm -f "$tmpfile"; return 1; }
71
+
72
+ local result
73
+ result=$(node -e "
74
+ const fs=require('fs');
75
+ try {
76
+ const d=fs.readFileSync('${tmpfile}','utf8');
77
+ const j=JSON.parse(d);
78
+ const wf=Array.isArray(j)?j[0]:j;
79
+ if(wf&&wf.id){ process.stdout.write(JSON.stringify(wf)); process.exit(0); }
80
+ process.exit(1);
81
+ } catch { process.exit(1); }" 2>/dev/null)
82
+ local rc=$?
83
+ rm -f "$tmpfile"
84
+ [ $rc -eq 0 ] && echo "$result" || return 1
89
85
  }
90
86
 
91
- # Get workflow ID
92
- get_workflow_id() {
93
- local wf_id
94
- wf_id=$(jq -r '.activeWorkflow.id // empty' "$INDEX_FILE" 2>/dev/null)
95
- echo "$wf_id"
87
+ create_checkpoint() {
88
+ local workflow_id="$1"
89
+ local phase="$2"
90
+ local description="$3"
91
+
92
+ local body
93
+ body=$(jq -n \
94
+ --arg phase "$phase" \
95
+ --arg desc "$description" \
96
+ '{ phase: $phase, description: $desc }')
97
+
98
+ local http_code
99
+ http_code=$(curl -sf -o /dev/null -w "%{http_code}" \
100
+ -X POST \
101
+ -H "Authorization: Bearer ${ORCHESTRATOR_PROJECT_TOKEN}" \
102
+ -H "Content-Type: application/json" \
103
+ -d "$body" \
104
+ "${ORCHESTRATOR_API_URL}/api/v1/workflows/${workflow_id}/checkpoints" 2>/dev/null) || echo ""
105
+
106
+ echo "$http_code"
96
107
  }
97
108
 
98
- # Get workflow status
99
- get_workflow_status() {
100
- local status
101
- status=$(jq -r '.activeWorkflow.status // empty' "$INDEX_FILE" 2>/dev/null)
102
- echo "$status"
109
+ # ── dedup guard ────────────────────────────────────────────────────────────────
110
+
111
+ get_saved_last_phase() {
112
+ [ -f "$LAST_PHASE_FILE" ] && cat "$LAST_PHASE_FILE" || echo ""
103
113
  }
104
114
 
105
- # Get last checkpointed phase from checkpoints array (one with commitHash)
106
- get_last_checkpointed_phase() {
107
- local phase
108
- # Find the most recent checkpoint that has a commitHash (indicating it was git-committed)
109
- phase=$(jq -r '
110
- [.checkpoints[] | select(.commitHash != null and .commitHash != "")]
111
- | sort_by(.createdAt)
112
- | last
113
- | .phase // empty
114
- ' "$INDEX_FILE" 2>/dev/null)
115
- echo "$phase"
115
+ save_last_phase() {
116
+ echo "$1" > "$LAST_PHASE_FILE"
116
117
  }
117
118
 
118
- # Determine completed phase based on current phase
119
- # When we see currentPhase = PLAN, it means SPECIFY just completed
120
- get_completed_phase() {
121
- local current_phase="$1"
122
- local workflow_status="$2"
119
+ # ── phase inference ────────────────────────────────────────────────────────────
123
120
 
124
- case "$workflow_status" in
125
- "completed")
126
- echo "IMPLEMENT"
127
- return
128
- ;;
121
+ # When workflow is at PLAN, it means SPECIFY just completed, etc.
122
+ get_completed_phase() {
123
+ local current="$1"
124
+ local status="$2"
125
+ case "$status" in
126
+ "completed") echo "implement"; return ;;
129
127
  esac
130
-
131
- case "$current_phase" in
132
- "PLAN")
133
- echo "SPECIFY"
134
- ;;
135
- "TASKS")
136
- echo "PLAN"
137
- ;;
138
- "IMPLEMENT")
139
- echo "TASKS"
140
- ;;
141
- *)
142
- echo ""
143
- ;;
128
+ case "${current,,}" in
129
+ "plan") echo "specify" ;;
130
+ "tasks") echo "plan" ;;
131
+ "implement") echo "tasks" ;;
132
+ *) echo "" ;;
144
133
  esac
145
134
  }
146
135
 
147
- # Create git checkpoint
148
- create_git_checkpoint() {
149
- local phase="$1"
150
- local commit_hash=""
151
-
152
- log_info "Creating git checkpoint for phase: $phase"
153
-
154
- # Stage all changes
155
- git -C "$PROJECT_ROOT" add -A 2>/dev/null || {
156
- log_warn "Failed to stage changes"
157
- return 1
158
- }
159
-
160
- # Check if there are staged changes
161
- if git -C "$PROJECT_ROOT" diff --cached --quiet 2>/dev/null; then
162
- log_info "No changes to commit for phase: $phase"
163
- echo ""
164
- return 0
165
- fi
166
-
167
- # Create commit with standard message
168
- local commit_msg="[orchestrator] ${phase}: Auto-checkpoint after ${phase} phase"
169
-
170
- if git -C "$PROJECT_ROOT" commit -m "$commit_msg" --no-verify 2>/dev/null; then
171
- commit_hash=$(git -C "$PROJECT_ROOT" rev-parse HEAD 2>/dev/null)
172
- log_info "Checkpoint commit created: $commit_hash"
173
- echo "$commit_hash"
174
- else
175
- log_warn "Failed to create commit"
176
- echo ""
177
- return 1
178
- fi
179
- }
180
-
181
- # Update orchestrator-index.json with checkpoint
182
- update_orchestrator_index() {
183
- local workflow_id="$1"
184
- local phase="$2"
185
- local commit_hash="$3"
186
- local created_at
187
-
188
- created_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
189
- local checkpoint_id="cp_${workflow_id}_$(date +%s)"
190
- local description="Auto-checkpoint after ${phase} phase"
191
-
192
- log_debug "Updating index with checkpoint: $checkpoint_id"
193
-
194
- # Create temp file for atomic update
195
- local temp_file="${INDEX_FILE}.tmp.$$"
196
-
197
- # Use jq to add checkpoint and update statistics
198
- if jq --arg id "$checkpoint_id" \
199
- --arg wfId "$workflow_id" \
200
- --arg phase "$phase" \
201
- --arg hash "$commit_hash" \
202
- --arg date "$created_at" \
203
- --arg desc "$description" '
204
- # Add checkpoint to array
205
- .checkpoints += [{
206
- id: $id,
207
- workflowId: $wfId,
208
- phase: $phase,
209
- commitHash: $hash,
210
- createdAt: $date,
211
- description: $desc
212
- }] |
213
- # Update statistics
214
- .statistics.totalCheckpoints = (.checkpoints | length) |
215
- .statistics.lastActivity = $date
216
- ' "$INDEX_FILE" > "$temp_file" 2>/dev/null; then
217
-
218
- # Validate JSON
219
- if jq empty "$temp_file" 2>/dev/null; then
220
- mv "$temp_file" "$INDEX_FILE"
221
- log_info "Index updated with checkpoint: $checkpoint_id"
222
- return 0
223
- else
224
- log_error "Generated invalid JSON"
225
- rm -f "$temp_file"
226
- return 1
227
- fi
228
- else
229
- log_error "Failed to update index with jq"
230
- rm -f "$temp_file"
231
- return 1
232
- fi
233
- }
234
-
235
- # Save last checkpointed phase
236
- save_last_phase() {
237
- local phase="$1"
238
- echo "$phase" > "$LAST_PHASE_FILE"
239
- }
240
-
241
- # Get saved last phase (from state file, not index)
242
- get_saved_last_phase() {
243
- if [ -f "$LAST_PHASE_FILE" ]; then
244
- cat "$LAST_PHASE_FILE"
245
- else
246
- echo ""
247
- fi
248
- }
136
+ # ── main ───────────────────────────────────────────────────────────────────────
249
137
 
250
- # Main execution
251
138
  main() {
252
139
  log_debug "Hook triggered"
253
140
 
254
- # Check prerequisites
255
141
  if ! check_prerequisites; then
256
- log_debug "Prerequisites not met, exiting"
257
- exit 0 # Don't fail the hook
142
+ exit 0
258
143
  fi
259
144
 
260
- # Get current state
261
- local current_phase
262
- current_phase=$(get_current_phase)
145
+ local workflow_json
146
+ workflow_json=$(get_active_workflow 2>/dev/null) || workflow_json=""
263
147
 
264
- if [ -z "$current_phase" ]; then
265
- log_debug "No active workflow phase found"
148
+ if [ -z "$workflow_json" ]; then
149
+ log_debug "No active workflow found via API."
266
150
  exit 0
267
151
  fi
268
152
 
269
- local workflow_id
270
- workflow_id=$(get_workflow_id)
153
+ local workflow_id current_phase workflow_status
154
+ workflow_id=$(echo "$workflow_json" | jq -r '.id // empty' 2>/dev/null || echo "")
155
+ current_phase=$(echo "$workflow_json" | jq -r '.currentPhase // empty' 2>/dev/null || echo "")
156
+ workflow_status=$(echo "$workflow_json" | jq -r '.status // empty' 2>/dev/null || echo "")
271
157
 
272
- local workflow_status
273
- workflow_status=$(get_workflow_status)
274
-
275
- log_debug "Current phase: $current_phase, Status: $workflow_status, Workflow: $workflow_id"
158
+ if [ -z "$workflow_id" ] || [ -z "$current_phase" ]; then
159
+ log_debug "Could not parse workflow ID or phase."
160
+ exit 0
161
+ fi
276
162
 
277
- # Determine which phase just completed
278
163
  local completed_phase
279
164
  completed_phase=$(get_completed_phase "$current_phase" "$workflow_status")
280
165
 
281
166
  if [ -z "$completed_phase" ]; then
282
- log_debug "No completed phase detected from current phase: $current_phase"
167
+ log_debug "No completed phase inferred from current_phase=${current_phase}."
283
168
  exit 0
284
169
  fi
285
170
 
286
- # Check if we already checkpointed this phase
287
- local last_checkpointed
288
- last_checkpointed=$(get_last_checkpointed_phase)
289
-
290
- local saved_last_phase
291
- saved_last_phase=$(get_saved_last_phase)
292
-
293
- log_debug "Completed phase: $completed_phase, Last checkpointed: $last_checkpointed, Saved: $saved_last_phase"
294
-
295
- # Skip if already checkpointed
296
- if [ "$completed_phase" = "$last_checkpointed" ] || [ "$completed_phase" = "$saved_last_phase" ]; then
297
- log_debug "Phase $completed_phase already checkpointed, skipping"
171
+ # Dedup guard — skip if we already checkpointed this phase
172
+ local saved_last
173
+ saved_last=$(get_saved_last_phase)
174
+ if [ "$completed_phase" = "$saved_last" ]; then
175
+ log_debug "Phase ${completed_phase} already checkpointed. Skipping."
298
176
  exit 0
299
177
  fi
300
178
 
301
- # Create checkpoint
302
- local commit_hash
303
- commit_hash=$(create_git_checkpoint "$completed_phase")
179
+ local description="Auto-checkpoint after ${completed_phase} phase"
180
+ local http_code
181
+ http_code=$(create_checkpoint "$workflow_id" "$completed_phase" "$description")
304
182
 
305
- # Update index (even if no commit, to track the checkpoint)
306
- if [ -n "$commit_hash" ]; then
307
- if update_orchestrator_index "$workflow_id" "$completed_phase" "$commit_hash"; then
183
+ case "$http_code" in
184
+ 201|200)
308
185
  save_last_phase "$completed_phase"
309
- log_info "Checkpoint complete for phase: $completed_phase (commit: $commit_hash)"
310
- else
311
- log_error "Failed to update index for phase: $completed_phase"
312
- fi
313
- else
314
- log_info "No changes to checkpoint for phase: $completed_phase"
315
- save_last_phase "$completed_phase"
316
- fi
186
+ log_info "Checkpoint registered: workflow=${workflow_id}, phase=${completed_phase}"
187
+ ;;
188
+ 409)
189
+ save_last_phase "$completed_phase"
190
+ log_info "Checkpoint already exists (409): phase=${completed_phase}"
191
+ ;;
192
+ "")
193
+ log_warn "API unreachable. Checkpoint skipped for phase=${completed_phase}."
194
+ ;;
195
+ *)
196
+ log_warn "Unexpected HTTP ${http_code} creating checkpoint for phase=${completed_phase}."
197
+ ;;
198
+ esac
317
199
 
318
200
  exit 0
319
201
  }
320
202
 
321
- # Run main function
322
203
  main "$@"
@@ -20,6 +20,13 @@ FILE_PATH=$(orch_json_field "$STDIN_DATA" "tool_input.file_path")
20
20
 
21
21
  orch_log "workflow-guard: file_path=$FILE_PATH"
22
22
 
23
+ # Explicit bypass via env var (for direct implementation without dogfooding)
24
+ if [ "${SKIP_WORKFLOW_GUARD:-}" = "true" ]; then
25
+ orch_log "workflow-guard: ALLOW (SKIP_WORKFLOW_GUARD=true)"
26
+ echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","additionalContext":"Workflow guard bypassed via SKIP_WORKFLOW_GUARD=true."}}'
27
+ exit 0
28
+ fi
29
+
23
30
  # Only guard src/ and tests/ paths (production code)
24
31
  case "$FILE_PATH" in
25
32
  */src/*|*/tests/*)
@@ -42,8 +49,27 @@ esac
42
49
  WORKFLOW_ID=$(orch_get_active_workflow 2>/dev/null) || WORKFLOW_ID=""
43
50
 
44
51
  if [ -n "$WORKFLOW_ID" ]; then
45
- orch_log "workflow-guard: ALLOW (active workflow: $WORKFLOW_ID)"
46
- echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"allow\",\"additionalContext\":\"Active workflow: ${WORKFLOW_ID}. Write allowed.\"}}"
52
+ # Check if running inside a sub-agent (agent_id present) or main agent (absent)
53
+ AGENT_ID=$(orch_json_field "$STDIN_DATA" "agent_id")
54
+
55
+ if [ -n "$AGENT_ID" ]; then
56
+ # Sub-agent writing — ALLOW
57
+ AGENT_TYPE=$(orch_json_field "$STDIN_DATA" "agent_type")
58
+ orch_log "workflow-guard: ALLOW (sub-agent: ${AGENT_TYPE:-unknown}, workflow: $WORKFLOW_ID)"
59
+ echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"allow\",\"additionalContext\":\"Active workflow: ${WORKFLOW_ID}. Sub-agent ${AGENT_TYPE:-unknown} write allowed.\"}}"
60
+ exit 0
61
+ fi
62
+
63
+ # Main agent writing directly — check if SKIP_SUBAGENT_GUARD allows it
64
+ if [ "${SKIP_SUBAGENT_GUARD:-}" = "true" ]; then
65
+ orch_log "workflow-guard: ALLOW (SKIP_SUBAGENT_GUARD=true, workflow: $WORKFLOW_ID)"
66
+ echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"allow\",\"additionalContext\":\"Active workflow: ${WORKFLOW_ID}. Direct write allowed via SKIP_SUBAGENT_GUARD.\"}}"
67
+ exit 0
68
+ fi
69
+
70
+ # Main agent writing directly — DENY, must use sub-agent
71
+ orch_log "workflow-guard: DENY (main agent direct write, no sub-agent invocation)"
72
+ echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"Workflow Guard: Direct code writes are blocked. You must invoke a sub-agent (e.g. implementer) to write code. The sub-agent will have write access.\",\"additionalContext\":\"Use the Agent tool to spawn an implementer sub-agent for code changes. The workflow-guard allows writes only from sub-agents (identified by agent_id in hook input).\"}}"
47
73
  exit 0
48
74
  fi
49
75
 
@@ -64,17 +64,6 @@
64
64
  }
65
65
  ],
66
66
  "PreToolUse": [
67
- {
68
- "matcher": "Task",
69
- "hooks": [
70
- {
71
- "type": "command",
72
- "command": ".claude/hooks/track-agent-invocation.sh start",
73
- "timeout": 5000,
74
- "on_failure": "ignore"
75
- }
76
- ]
77
- },
78
67
  {
79
68
  "matcher": "mcp__orchestrator-tools__advancePhase",
80
69
  "hooks": [
@@ -98,9 +87,22 @@
98
87
  ]
99
88
  }
100
89
  ],
101
- "PostToolUse": [
90
+ "SubagentStart": [
91
+ {
92
+ "matcher": "implementer|specifier|planner|task-generator|reviewer|researcher|orchestrator",
93
+ "hooks": [
94
+ {
95
+ "type": "command",
96
+ "command": ".claude/hooks/track-agent-invocation.sh start",
97
+ "timeout": 5000,
98
+ "on_failure": "ignore"
99
+ }
100
+ ]
101
+ }
102
+ ],
103
+ "SubagentStop": [
102
104
  {
103
- "matcher": "Task",
105
+ "matcher": "implementer|specifier|planner|task-generator|reviewer|researcher|orchestrator",
104
106
  "hooks": [
105
107
  {
106
108
  "type": "command",