@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 +1 -1
- package/dist/index.js +1 -1
- package/dist/templates/base/claude/hooks/post-phase-checkpoint.sh +135 -254
- package/dist/templates/base/claude/hooks/workflow-guard.sh +28 -2
- package/dist/templates/base/claude/settings.json +15 -13
- package/package.json +1 -1
- package/templates/base/claude/hooks/post-phase-checkpoint.sh +135 -254
- package/templates/base/claude/hooks/workflow-guard.sh +28 -2
- package/templates/base/claude/settings.json +15 -13
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,322 +1,203 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# Post-Phase Checkpoint Hook
|
|
3
|
-
#
|
|
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:
|
|
7
|
+
# Behavior: Infers the just-completed phase from the active workflow state
|
|
8
|
+
# and registers a checkpoint via the REST API.
|
|
7
9
|
#
|
|
8
|
-
#
|
|
9
|
-
# -
|
|
10
|
-
# -
|
|
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
|
-
#
|
|
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 -
|
|
20
|
+
set -euo pipefail
|
|
15
21
|
|
|
16
|
-
# Configuration
|
|
17
22
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
46
|
-
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] [WARN]
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
local
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
#
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
106
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
132
|
-
"
|
|
133
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
257
|
-
exit 0 # Don't fail the hook
|
|
142
|
+
exit 0
|
|
258
143
|
fi
|
|
259
144
|
|
|
260
|
-
|
|
261
|
-
|
|
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 "$
|
|
265
|
-
log_debug "No active workflow
|
|
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=$(
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
|
167
|
+
log_debug "No completed phase inferred from current_phase=${current_phase}."
|
|
283
168
|
exit 0
|
|
284
169
|
fi
|
|
285
170
|
|
|
286
|
-
#
|
|
287
|
-
local
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
302
|
-
local
|
|
303
|
-
|
|
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
|
-
|
|
306
|
-
|
|
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
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
"
|
|
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": "
|
|
105
|
+
"matcher": "implementer|specifier|planner|task-generator|reviewer|researcher|orchestrator",
|
|
104
106
|
"hooks": [
|
|
105
107
|
{
|
|
106
108
|
"type": "command",
|
package/package.json
CHANGED
|
@@ -1,322 +1,203 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# Post-Phase Checkpoint Hook
|
|
3
|
-
#
|
|
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:
|
|
7
|
+
# Behavior: Infers the just-completed phase from the active workflow state
|
|
8
|
+
# and registers a checkpoint via the REST API.
|
|
7
9
|
#
|
|
8
|
-
#
|
|
9
|
-
# -
|
|
10
|
-
# -
|
|
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
|
-
#
|
|
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 -
|
|
20
|
+
set -euo pipefail
|
|
15
21
|
|
|
16
|
-
# Configuration
|
|
17
22
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
46
|
-
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] [WARN]
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
local
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
#
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
106
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
132
|
-
"
|
|
133
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
257
|
-
exit 0 # Don't fail the hook
|
|
142
|
+
exit 0
|
|
258
143
|
fi
|
|
259
144
|
|
|
260
|
-
|
|
261
|
-
|
|
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 "$
|
|
265
|
-
log_debug "No active workflow
|
|
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=$(
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
|
167
|
+
log_debug "No completed phase inferred from current_phase=${current_phase}."
|
|
283
168
|
exit 0
|
|
284
169
|
fi
|
|
285
170
|
|
|
286
|
-
#
|
|
287
|
-
local
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
302
|
-
local
|
|
303
|
-
|
|
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
|
-
|
|
306
|
-
|
|
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
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
"
|
|
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": "
|
|
105
|
+
"matcher": "implementer|specifier|planner|task-generator|reviewer|researcher|orchestrator",
|
|
104
106
|
"hooks": [
|
|
105
107
|
{
|
|
106
108
|
"type": "command",
|