@orchestrator-claude/definitions 3.17.1 → 3.18.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/agents/orchestrator.md +79 -165
- package/hooks/dangling-guard.ts +53 -0
- package/hooks/gate-guardian.ts +102 -0
- package/hooks/lib/api-client.ts +293 -0
- package/hooks/lib/git-checkpoint.ts +91 -0
- package/hooks/package.json +13 -0
- package/hooks/post-compact.ts +44 -0
- package/hooks/session-start.ts +97 -0
- package/hooks/subagent-start.ts +50 -0
- package/hooks/subagent-stop.ts +57 -0
- package/hooks/tsconfig.json +18 -0
- package/hooks/user-prompt.ts +95 -0
- package/hooks/workflow-guard.ts +120 -0
- package/manifest.json +4 -85
- package/package.json +1 -1
- package/hooks/approval-guardian.sh +0 -62
- package/hooks/dangling-workflow-guard.sh +0 -57
- package/hooks/gate-guardian.sh +0 -84
- package/hooks/orch-helpers.sh +0 -135
- package/hooks/ping-pong-enforcer.sh +0 -58
- package/hooks/post-phase-checkpoint.sh +0 -203
- package/hooks/prompt-orchestrator.sh +0 -41
- package/hooks/session-orchestrator.sh +0 -54
- package/hooks/track-agent-invocation.sh +0 -230
- package/hooks/workflow-guard.sh +0 -79
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* user-prompt.ts — UserPromptSubmit hook (RFC-022 Phase 2)
|
|
3
|
+
* Replaces prompt-orchestrator.sh
|
|
4
|
+
* Detects workflow type and injects hints by confidence tiers.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
log,
|
|
9
|
+
readStdin,
|
|
10
|
+
getField,
|
|
11
|
+
getActiveWorkflow,
|
|
12
|
+
getNextAction,
|
|
13
|
+
detectWorkflow,
|
|
14
|
+
outputContext,
|
|
15
|
+
} from "./lib/api-client.js";
|
|
16
|
+
|
|
17
|
+
const HOOK = "USER-PROMPT";
|
|
18
|
+
|
|
19
|
+
async function main(): Promise<void> {
|
|
20
|
+
log(HOOK, "UserPromptSubmit triggered");
|
|
21
|
+
|
|
22
|
+
const stdin = await readStdin();
|
|
23
|
+
const workflow = await getActiveWorkflow();
|
|
24
|
+
|
|
25
|
+
if (!workflow) {
|
|
26
|
+
// No active workflow — detect and suggest
|
|
27
|
+
const prompt = getField(stdin, "prompt");
|
|
28
|
+
if (!prompt) {
|
|
29
|
+
log(HOOK, "empty prompt, silence");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const detection = await detectWorkflow(prompt);
|
|
34
|
+
if (!detection) {
|
|
35
|
+
log(HOOK, "detect API failed/timeout, silence (fail-open)");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const { workflowType, confidence, suggestedMode } = detection;
|
|
40
|
+
log(HOOK, `detected type=${workflowType} confidence=${confidence} mode=${suggestedMode}`);
|
|
41
|
+
|
|
42
|
+
// Tier 1: interactive_analysis
|
|
43
|
+
if (workflowType === "interactive_analysis") {
|
|
44
|
+
outputContext(
|
|
45
|
+
"UserPromptSubmit",
|
|
46
|
+
[
|
|
47
|
+
"[ORCHESTRATOR] This looks like an analytical/design request. How would you like to proceed?",
|
|
48
|
+
'(a) Continue as a free conversation (no workflow)',
|
|
49
|
+
'(b) Start an interactive_analysis workflow for structured exploration (cyclic phases: research → discuss → analyze)',
|
|
50
|
+
"",
|
|
51
|
+
'If you choose (b), I will call: mcp__orchestrator-tools__startWorkflow({ type: "interactive_analysis", prompt: "..." })',
|
|
52
|
+
].join("\n")
|
|
53
|
+
);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Tier 2: high confidence (>= 0.7)
|
|
58
|
+
if (confidence >= 0.7) {
|
|
59
|
+
const mode = suggestedMode || "standard";
|
|
60
|
+
outputContext(
|
|
61
|
+
"UserPromptSubmit",
|
|
62
|
+
`[ORCHESTRATOR] Detected: ${workflowType} workflow (confidence: ${confidence}, suggested mode: ${mode}).\nTo start: mcp__orchestrator-tools__startWorkflow({ type: "${workflowType}", prompt: "...", mode: "${mode}" })\nOr continue without a workflow if this is exploratory.`
|
|
63
|
+
);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Tier 3: medium confidence (0.5-0.69)
|
|
68
|
+
if (confidence >= 0.5) {
|
|
69
|
+
outputContext(
|
|
70
|
+
"UserPromptSubmit",
|
|
71
|
+
`[ORCHESTRATOR] Hint: this might be a ${workflowType} task. Consider starting a workflow if you plan to write code.`
|
|
72
|
+
);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Tier 4: low confidence — silence
|
|
77
|
+
log(HOOK, `Tier 4 silence (confidence=${confidence} < 0.5)`);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Active workflow — inject state
|
|
82
|
+
const action = await getNextAction(workflow.id);
|
|
83
|
+
let context: string;
|
|
84
|
+
|
|
85
|
+
if (action?.hasAction && action.pendingAction) {
|
|
86
|
+
context = `[ORCHESTRATOR] Active workflow: ${workflow.id}. Next: invoke '${action.pendingAction.agent}' (${action.pendingAction.status}).`;
|
|
87
|
+
} else {
|
|
88
|
+
context = `[ORCHESTRATOR] Active workflow: ${workflow.id}. No pending actions.`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
log(HOOK, `workflow=${workflow.id} hasAction=${action?.hasAction}`);
|
|
92
|
+
outputContext("UserPromptSubmit", context);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
main().catch(() => process.exit(0));
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* workflow-guard.ts — PreToolUse[Write|Edit] hook (RFC-022 Phase 2)
|
|
3
|
+
* Replaces workflow-guard.sh
|
|
4
|
+
* Blocks writes to src/ and tests/ without active workflow.
|
|
5
|
+
* Respects SKIP_WORKFLOW_GUARD, quick/interactive modes, sub-agent writes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
log,
|
|
10
|
+
readStdin,
|
|
11
|
+
getField,
|
|
12
|
+
getActiveWorkflow,
|
|
13
|
+
getWorkflowMode,
|
|
14
|
+
outputPreToolUse,
|
|
15
|
+
} from "./lib/api-client.js";
|
|
16
|
+
|
|
17
|
+
const HOOK = "WORKFLOW-GUARD";
|
|
18
|
+
|
|
19
|
+
async function main(): Promise<void> {
|
|
20
|
+
const stdin = await readStdin();
|
|
21
|
+
const filePath = getField(stdin, "tool_input.file_path") || getField(stdin, "file_path") || getField(stdin, "input.file_path");
|
|
22
|
+
|
|
23
|
+
log(HOOK, `file_path=${filePath}`);
|
|
24
|
+
|
|
25
|
+
// Explicit bypass
|
|
26
|
+
if (process.env.SKIP_WORKFLOW_GUARD === "true") {
|
|
27
|
+
log(HOOK, "ALLOW (SKIP_WORKFLOW_GUARD=true)");
|
|
28
|
+
outputPreToolUse({
|
|
29
|
+
decision: "allow",
|
|
30
|
+
context: "Workflow guard bypassed via SKIP_WORKFLOW_GUARD=true.",
|
|
31
|
+
});
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Only guard src/ and tests/ paths
|
|
36
|
+
if (!filePath.includes("/src/") && !filePath.includes("/tests/")) {
|
|
37
|
+
log(HOOK, "ALLOW (non-guarded path)");
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Allow config/doc files
|
|
42
|
+
if (/\.(json|ya?ml|md|env(\..*)?)$/.test(filePath)) {
|
|
43
|
+
log(HOOK, "ALLOW (config/doc file)");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Check for active workflow
|
|
48
|
+
const workflow = await getActiveWorkflow();
|
|
49
|
+
|
|
50
|
+
if (workflow) {
|
|
51
|
+
const mode = await getWorkflowMode(workflow.id);
|
|
52
|
+
log(HOOK, `workflow=${workflow.id} mode=${mode || "legacy"}`);
|
|
53
|
+
|
|
54
|
+
if (mode === "quick") {
|
|
55
|
+
outputPreToolUse({
|
|
56
|
+
decision: "allow",
|
|
57
|
+
context: `Active workflow: ${workflow.id} (mode: quick). Direct writes allowed in quick mode.`,
|
|
58
|
+
});
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (mode === "interactive") {
|
|
63
|
+
outputPreToolUse({
|
|
64
|
+
decision: "allow",
|
|
65
|
+
context: `Active workflow: ${workflow.id} (mode: interactive). Note: code writes are unexpected in interactive/analysis workflows.`,
|
|
66
|
+
});
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Check if running inside a sub-agent
|
|
71
|
+
const agentId = getField(stdin, "agent_id");
|
|
72
|
+
if (agentId) {
|
|
73
|
+
const agentType = getField(stdin, "agent_type");
|
|
74
|
+
log(HOOK, `ALLOW (sub-agent: ${agentType || "unknown"}, workflow: ${workflow.id})`);
|
|
75
|
+
outputPreToolUse({
|
|
76
|
+
decision: "allow",
|
|
77
|
+
context: `Active workflow: ${workflow.id}. Sub-agent ${agentType || "unknown"} write allowed.`,
|
|
78
|
+
});
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Main agent writing directly — check SKIP_SUBAGENT_GUARD
|
|
83
|
+
if (process.env.SKIP_SUBAGENT_GUARD === "true") {
|
|
84
|
+
log(HOOK, `ALLOW (SKIP_SUBAGENT_GUARD=true, workflow: ${workflow.id})`);
|
|
85
|
+
outputPreToolUse({
|
|
86
|
+
decision: "allow",
|
|
87
|
+
context: `Active workflow: ${workflow.id}. Direct write allowed via SKIP_SUBAGENT_GUARD.`,
|
|
88
|
+
});
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Main agent direct write — DENY
|
|
93
|
+
log(HOOK, `DENY (main agent direct write, mode=${mode || "legacy"})`);
|
|
94
|
+
outputPreToolUse({
|
|
95
|
+
decision: "deny",
|
|
96
|
+
reason:
|
|
97
|
+
"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.",
|
|
98
|
+
context:
|
|
99
|
+
"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).",
|
|
100
|
+
});
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// No active workflow — DENY
|
|
105
|
+
log(HOOK, "DENY (no active workflow)");
|
|
106
|
+
outputPreToolUse({
|
|
107
|
+
decision: "deny",
|
|
108
|
+
reason: "Workflow Guard: No active workflow. Direct implementation is not allowed.",
|
|
109
|
+
context: [
|
|
110
|
+
"You MUST start a workflow before writing code:",
|
|
111
|
+
'1. mcp__orchestrator-tools__detectWorkflow({ prompt: "..." })',
|
|
112
|
+
'2. mcp__orchestrator-tools__startWorkflow({ workflowType: "...", prompt: "..." })',
|
|
113
|
+
"3. Follow the nextStep returned by startWorkflow",
|
|
114
|
+
"",
|
|
115
|
+
"The workflow-guard hook blocks all writes to src/ and tests/ without an active workflow.",
|
|
116
|
+
].join("\n"),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
main().catch(() => process.exit(0));
|
package/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": "orchestrator-definitions/v1",
|
|
3
3
|
"namespace": "orchestrator",
|
|
4
|
-
"version": "3.
|
|
5
|
-
"generatedAt": "2026-03-
|
|
4
|
+
"version": "3.18.0",
|
|
5
|
+
"generatedAt": "2026-03-24T00:26:43.809Z",
|
|
6
6
|
"definitions": {
|
|
7
7
|
"agents": [
|
|
8
8
|
{
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"slug": "orchestrator",
|
|
66
66
|
"file": "agents/orchestrator.md",
|
|
67
67
|
"version": "1.0.0",
|
|
68
|
-
"checksum": "sha256:
|
|
68
|
+
"checksum": "sha256:026d1bee4f40979b281f9372972e54b8538b8c4bde032fbd142cc15cc3f80cdc",
|
|
69
69
|
"supersedes": null,
|
|
70
70
|
"metadata": {}
|
|
71
71
|
},
|
|
@@ -232,88 +232,7 @@
|
|
|
232
232
|
"metadata": {}
|
|
233
233
|
}
|
|
234
234
|
],
|
|
235
|
-
"hooks": [
|
|
236
|
-
{
|
|
237
|
-
"slug": "approval-guardian",
|
|
238
|
-
"file": "hooks/approval-guardian.sh",
|
|
239
|
-
"version": "1.0.0",
|
|
240
|
-
"checksum": "sha256:5d9c1080926610cd3dd1babc3815ec82c93f46177758a5a921f65a717196bd83",
|
|
241
|
-
"supersedes": null,
|
|
242
|
-
"metadata": {}
|
|
243
|
-
},
|
|
244
|
-
{
|
|
245
|
-
"slug": "dangling-workflow-guard",
|
|
246
|
-
"file": "hooks/dangling-workflow-guard.sh",
|
|
247
|
-
"version": "1.0.0",
|
|
248
|
-
"checksum": "sha256:34b195e03357e1d264328d7345832845beae11d2846c8e22810e427b20c75a0c",
|
|
249
|
-
"supersedes": null,
|
|
250
|
-
"metadata": {}
|
|
251
|
-
},
|
|
252
|
-
{
|
|
253
|
-
"slug": "gate-guardian",
|
|
254
|
-
"file": "hooks/gate-guardian.sh",
|
|
255
|
-
"version": "1.0.0",
|
|
256
|
-
"checksum": "sha256:1631a7be326e1099698f6492caead2f05423582869bba0f69a50754055e1be2d",
|
|
257
|
-
"supersedes": null,
|
|
258
|
-
"metadata": {}
|
|
259
|
-
},
|
|
260
|
-
{
|
|
261
|
-
"slug": "orch-helpers",
|
|
262
|
-
"file": "hooks/orch-helpers.sh",
|
|
263
|
-
"version": "1.0.0",
|
|
264
|
-
"checksum": "sha256:906d72e990592bcac675d01c8e8f7ea2df42794b59492992fd8a15975b50608d",
|
|
265
|
-
"supersedes": null,
|
|
266
|
-
"metadata": {}
|
|
267
|
-
},
|
|
268
|
-
{
|
|
269
|
-
"slug": "ping-pong-enforcer",
|
|
270
|
-
"file": "hooks/ping-pong-enforcer.sh",
|
|
271
|
-
"version": "1.0.0",
|
|
272
|
-
"checksum": "sha256:d2a5a10ea253da2ead5e203d64690129a56f377ecabe7ea4d43cd9c99d4cad95",
|
|
273
|
-
"supersedes": null,
|
|
274
|
-
"metadata": {}
|
|
275
|
-
},
|
|
276
|
-
{
|
|
277
|
-
"slug": "post-phase-checkpoint",
|
|
278
|
-
"file": "hooks/post-phase-checkpoint.sh",
|
|
279
|
-
"version": "1.0.0",
|
|
280
|
-
"checksum": "sha256:f75c815334e371bf508642b9591d94a548e1221c65ff7f34af9e430ec6e88722",
|
|
281
|
-
"supersedes": null,
|
|
282
|
-
"metadata": {}
|
|
283
|
-
},
|
|
284
|
-
{
|
|
285
|
-
"slug": "prompt-orchestrator",
|
|
286
|
-
"file": "hooks/prompt-orchestrator.sh",
|
|
287
|
-
"version": "1.0.0",
|
|
288
|
-
"checksum": "sha256:93835c7798185a9c8f8bf84a029c660b2bbe34801cd34c5be6b2b7a1f07a0b8b",
|
|
289
|
-
"supersedes": null,
|
|
290
|
-
"metadata": {}
|
|
291
|
-
},
|
|
292
|
-
{
|
|
293
|
-
"slug": "session-orchestrator",
|
|
294
|
-
"file": "hooks/session-orchestrator.sh",
|
|
295
|
-
"version": "1.0.0",
|
|
296
|
-
"checksum": "sha256:143024a68b8438a0724fd93dd9b6eb1688bcc89c1a846ae7f6c389361b469f65",
|
|
297
|
-
"supersedes": null,
|
|
298
|
-
"metadata": {}
|
|
299
|
-
},
|
|
300
|
-
{
|
|
301
|
-
"slug": "track-agent-invocation",
|
|
302
|
-
"file": "hooks/track-agent-invocation.sh",
|
|
303
|
-
"version": "1.0.0",
|
|
304
|
-
"checksum": "sha256:6a9343b032dd72e391fc4c476af480ee3055a5a25ec60d3524b0b0d804a80c6d",
|
|
305
|
-
"supersedes": null,
|
|
306
|
-
"metadata": {}
|
|
307
|
-
},
|
|
308
|
-
{
|
|
309
|
-
"slug": "workflow-guard",
|
|
310
|
-
"file": "hooks/workflow-guard.sh",
|
|
311
|
-
"version": "1.0.0",
|
|
312
|
-
"checksum": "sha256:170a15396a61c537df4c8adc093d7ac36faab6e7de761ff8d283828c53e4a332",
|
|
313
|
-
"supersedes": null,
|
|
314
|
-
"metadata": {}
|
|
315
|
-
}
|
|
316
|
-
],
|
|
235
|
+
"hooks": [],
|
|
317
236
|
"workflows": [
|
|
318
237
|
{
|
|
319
238
|
"slug": "bug-fix",
|
package/package.json
CHANGED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# approval-guardian.sh — TD-128 F-09 Hook
|
|
3
|
-
# Trigger: PreToolUse on mcp__orchestrator-tools__approveAction and mcp__orchestrator-extended__completeWorkflow
|
|
4
|
-
# Purpose: Block auto-approve and auto-complete when workflow is awaiting_approval.
|
|
5
|
-
# Requires explicit human confirmation before proceeding.
|
|
6
|
-
#
|
|
7
|
-
# Output: JSON with permissionDecision (deny/allow)
|
|
8
|
-
|
|
9
|
-
set -euo pipefail
|
|
10
|
-
|
|
11
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
12
|
-
source "$SCRIPT_DIR/orch-helpers.sh"
|
|
13
|
-
|
|
14
|
-
STDIN_DATA=$(orch_read_stdin)
|
|
15
|
-
TOOL_NAME=$(orch_json_field "$STDIN_DATA" "tool_name")
|
|
16
|
-
orch_log "APPROVAL-GUARDIAN: PreToolUse $TOOL_NAME triggered"
|
|
17
|
-
|
|
18
|
-
# Extract workflow ID from tool_input
|
|
19
|
-
WORKFLOW_ID=$(orch_json_field "$STDIN_DATA" "tool_input.workflowId")
|
|
20
|
-
[ -z "$WORKFLOW_ID" ] && WORKFLOW_ID=$(orch_json_field "$STDIN_DATA" "workflowId")
|
|
21
|
-
|
|
22
|
-
if [ -z "$WORKFLOW_ID" ]; then
|
|
23
|
-
# No workflow ID — allow (fail-open for non-workflow calls)
|
|
24
|
-
orch_log "APPROVAL-GUARDIAN: ALLOW (no workflowId)"
|
|
25
|
-
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","additionalContext":"No workflowId found, allowing."}}'
|
|
26
|
-
exit 0
|
|
27
|
-
fi
|
|
28
|
-
|
|
29
|
-
# Get auth token
|
|
30
|
-
TOKEN=$(orch_get_token 2>/dev/null) || TOKEN=""
|
|
31
|
-
if [ -z "$TOKEN" ]; then
|
|
32
|
-
# Can't check status — fail-open (auth issues shouldn't block workflow completion)
|
|
33
|
-
orch_log "APPROVAL-GUARDIAN: ALLOW (auth failed, fail-open)"
|
|
34
|
-
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","additionalContext":"Auth failed, allowing."}}'
|
|
35
|
-
exit 0
|
|
36
|
-
fi
|
|
37
|
-
|
|
38
|
-
# Get workflow status
|
|
39
|
-
STATUS=$(curl -sf --max-time 5 "${API_URL}/api/v1/workflows/${WORKFLOW_ID}" \
|
|
40
|
-
-H "Authorization: Bearer $TOKEN" \
|
|
41
|
-
-H "X-Project-ID: $PROJECT_ID" 2>/dev/null) || STATUS=""
|
|
42
|
-
|
|
43
|
-
if [ -z "$STATUS" ]; then
|
|
44
|
-
orch_log "APPROVAL-GUARDIAN: ALLOW (could not fetch workflow status)"
|
|
45
|
-
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","additionalContext":"Could not fetch workflow status, allowing."}}'
|
|
46
|
-
exit 0
|
|
47
|
-
fi
|
|
48
|
-
|
|
49
|
-
WORKFLOW_STATUS=$(orch_json_field "$STATUS" "status")
|
|
50
|
-
orch_log "APPROVAL-GUARDIAN: workflow=$WORKFLOW_ID status=$WORKFLOW_STATUS tool=$TOOL_NAME"
|
|
51
|
-
|
|
52
|
-
# Block approveAction AND completeWorkflow when awaiting_approval
|
|
53
|
-
if [ "$WORKFLOW_STATUS" = "awaiting_approval" ]; then
|
|
54
|
-
orch_log "APPROVAL-GUARDIAN: DENY (workflow awaiting_approval — human confirmation required for $TOOL_NAME)"
|
|
55
|
-
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"Approval Guardian: Workflow is awaiting human approval. You MUST ask the user for explicit confirmation before calling ${TOOL_NAME}.\",\"additionalContext\":\"Present the workflow summary to the user and ask: 'Do you approve advancing to IMPLEMENT?' Wait for their response. Do NOT auto-approve or auto-complete.\"}}"
|
|
56
|
-
exit 0
|
|
57
|
-
fi
|
|
58
|
-
|
|
59
|
-
# All other statuses: ALLOW
|
|
60
|
-
orch_log "APPROVAL-GUARDIAN: ALLOW ($TOOL_NAME, status=$WORKFLOW_STATUS)"
|
|
61
|
-
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"allow\",\"additionalContext\":\"${TOOL_NAME} allowed (status=${WORKFLOW_STATUS}).\"}}"
|
|
62
|
-
exit 0
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# dangling-workflow-guard.sh — ADR-013 Phase 5 Hook (JSON Structured Output)
|
|
3
|
-
# Trigger: Stop
|
|
4
|
-
# Purpose: Before conversation ends, check for workflows in in_progress state.
|
|
5
|
-
# Blocks stop and instructs LLM to call completeWorkflow.
|
|
6
|
-
#
|
|
7
|
-
# Output: JSON with decision: "block" + reason (forces LLM to continue)
|
|
8
|
-
# Exit 0 with block JSON = prevent stop
|
|
9
|
-
# Check stop_hook_active to prevent infinite loops
|
|
10
|
-
|
|
11
|
-
set -euo pipefail
|
|
12
|
-
|
|
13
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
14
|
-
source "$SCRIPT_DIR/orch-helpers.sh"
|
|
15
|
-
|
|
16
|
-
STDIN_DATA=$(orch_read_stdin)
|
|
17
|
-
orch_log "DANGLING-GUARD: Stop hook triggered"
|
|
18
|
-
|
|
19
|
-
# Prevent infinite loops
|
|
20
|
-
STOP_ACTIVE=$(orch_json_field "$STDIN_DATA" "stop_hook_active")
|
|
21
|
-
if [ "$STOP_ACTIVE" = "true" ]; then
|
|
22
|
-
orch_log "DANGLING-GUARD: stop_hook_active=true, allowing stop to prevent loop"
|
|
23
|
-
exit 0
|
|
24
|
-
fi
|
|
25
|
-
|
|
26
|
-
# Find active workflow
|
|
27
|
-
WORKFLOW_ID=$(orch_get_active_workflow 2>/dev/null) || WORKFLOW_ID=""
|
|
28
|
-
|
|
29
|
-
if [ -z "$WORKFLOW_ID" ]; then
|
|
30
|
-
orch_log "DANGLING-GUARD: No active workflow, clean exit"
|
|
31
|
-
exit 0
|
|
32
|
-
fi
|
|
33
|
-
|
|
34
|
-
# Get workflow status
|
|
35
|
-
TOKEN=$(orch_get_token 2>/dev/null) || TOKEN=""
|
|
36
|
-
if [ -z "$TOKEN" ]; then
|
|
37
|
-
orch_log "DANGLING-GUARD: Auth failed, allowing stop"
|
|
38
|
-
exit 0
|
|
39
|
-
fi
|
|
40
|
-
|
|
41
|
-
STATUS_RESP=$(curl -sf --max-time 5 "${API_URL}/api/v1/workflows/${WORKFLOW_ID}/status" \
|
|
42
|
-
-H "Authorization: Bearer $TOKEN" \
|
|
43
|
-
-H "X-Project-ID: $PROJECT_ID" 2>/dev/null) || STATUS_RESP=""
|
|
44
|
-
|
|
45
|
-
PHASE=$(orch_json_field "$STATUS_RESP" "currentPhase")
|
|
46
|
-
STATUS=$(orch_json_field "$STATUS_RESP" "status")
|
|
47
|
-
|
|
48
|
-
orch_log "DANGLING-GUARD: workflow=$WORKFLOW_ID status=$STATUS phase=$PHASE"
|
|
49
|
-
|
|
50
|
-
if [ "$STATUS" = "in_progress" ] || [ "$STATUS" = "awaiting_agent" ] || [ "$STATUS" = "awaiting_approval" ]; then
|
|
51
|
-
orch_log "DANGLING-GUARD: BLOCK stop (workflow still active)"
|
|
52
|
-
echo "{\"decision\":\"block\",\"reason\":\"[DANGLING-GUARD] Workflow ${WORKFLOW_ID} is still ${STATUS} (phase: ${PHASE}). You must call mcp__orchestrator-extended__completeWorkflow({ workflowId: '${WORKFLOW_ID}' }) before ending the session.\"}"
|
|
53
|
-
exit 0
|
|
54
|
-
fi
|
|
55
|
-
|
|
56
|
-
orch_log "DANGLING-GUARD: Workflow completed or not active, allowing stop"
|
|
57
|
-
exit 0
|
package/hooks/gate-guardian.sh
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# gate-guardian.sh — ADR-013 Phase 5 Hook (JSON Structured Output)
|
|
3
|
-
# Trigger: PreToolUse on mcp__orchestrator-tools__advancePhase
|
|
4
|
-
# Purpose: Guard IMPLEMENT phase advance (requires human approval). ALLOWs all other phases.
|
|
5
|
-
# Note: POST /gate/evaluate endpoint removed (TD-127). Phase transitions are now handled
|
|
6
|
-
# atomically by setPendingAction() via the ping-pong-enforcer hook.
|
|
7
|
-
#
|
|
8
|
-
# Output: JSON with permissionDecision (deny/allow) + additionalContext
|
|
9
|
-
# Exit 0 with JSON = structured decision
|
|
10
|
-
|
|
11
|
-
set -euo pipefail
|
|
12
|
-
|
|
13
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
14
|
-
source "$SCRIPT_DIR/orch-helpers.sh"
|
|
15
|
-
|
|
16
|
-
STDIN_DATA=$(orch_read_stdin)
|
|
17
|
-
orch_log "GATE-GUARDIAN: PreToolUse advancePhase triggered"
|
|
18
|
-
|
|
19
|
-
# Extract parameters
|
|
20
|
-
WORKFLOW_ID=$(orch_json_field "$STDIN_DATA" "tool_input.workflowId")
|
|
21
|
-
TARGET_PHASE=$(orch_json_field "$STDIN_DATA" "tool_input.targetPhase")
|
|
22
|
-
[ -z "$WORKFLOW_ID" ] && WORKFLOW_ID=$(orch_json_field "$STDIN_DATA" "workflowId")
|
|
23
|
-
[ -z "$TARGET_PHASE" ] && TARGET_PHASE=$(orch_json_field "$STDIN_DATA" "targetPhase")
|
|
24
|
-
|
|
25
|
-
orch_log "GATE-GUARDIAN: workflow=$WORKFLOW_ID targetPhase=$TARGET_PHASE"
|
|
26
|
-
|
|
27
|
-
# ADR-013 Phase 6: Check workflow mode before gate evaluation
|
|
28
|
-
# quick and interactive modes skip gate evaluation entirely
|
|
29
|
-
if [ -n "$WORKFLOW_ID" ]; then
|
|
30
|
-
MODE=$(orch_get_workflow_mode "$WORKFLOW_ID" 2>/dev/null) || MODE=""
|
|
31
|
-
orch_log "GATE-GUARDIAN: workflow=$WORKFLOW_ID mode=${MODE:-legacy}"
|
|
32
|
-
|
|
33
|
-
if [ "$MODE" = "quick" ]; then
|
|
34
|
-
orch_log "GATE-GUARDIAN: ALLOW (quick mode skips gate evaluation)"
|
|
35
|
-
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"allow\",\"additionalContext\":\"Gate to '${TARGET_PHASE}' allowed: quick mode skips gate evaluation.\"}}"
|
|
36
|
-
exit 0
|
|
37
|
-
fi
|
|
38
|
-
|
|
39
|
-
if [ "$MODE" = "interactive" ]; then
|
|
40
|
-
orch_log "GATE-GUARDIAN: ALLOW (interactive mode skips gate evaluation, artifact gates not required)"
|
|
41
|
-
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"allow\",\"additionalContext\":\"Gate to '${TARGET_PHASE}' allowed: interactive mode skips artifact gate evaluation.\"}}"
|
|
42
|
-
exit 0
|
|
43
|
-
fi
|
|
44
|
-
fi
|
|
45
|
-
|
|
46
|
-
# FAIL-CLOSED: if we can't parse input, DENY
|
|
47
|
-
if [ -z "$WORKFLOW_ID" ] || [ -z "$TARGET_PHASE" ]; then
|
|
48
|
-
orch_log "GATE-GUARDIAN: DENY (could not parse input)"
|
|
49
|
-
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Gate Guardian: Could not parse workflowId or targetPhase.","additionalContext":"Ensure you pass both workflowId and targetPhase to advancePhase."}}'
|
|
50
|
-
exit 0
|
|
51
|
-
fi
|
|
52
|
-
|
|
53
|
-
# Get auth token — FAIL-CLOSED on auth failure
|
|
54
|
-
TOKEN=$(orch_get_token 2>/dev/null) || TOKEN=""
|
|
55
|
-
if [ -z "$TOKEN" ]; then
|
|
56
|
-
orch_log "GATE-GUARDIAN: DENY (auth failed)"
|
|
57
|
-
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Gate Guardian: Authentication failed.","additionalContext":"Check ORCHESTRATOR_ADMIN_EMAIL, ORCHESTRATOR_ADMIN_PASSWORD, ORCHESTRATOR_PROJECT_ID env vars."}}'
|
|
58
|
-
exit 0
|
|
59
|
-
fi
|
|
60
|
-
|
|
61
|
-
# Special handling for IMPLEMENT phase: require approval
|
|
62
|
-
TARGET_LOWER=$(echo "$TARGET_PHASE" | tr '[:upper:]' '[:lower:]')
|
|
63
|
-
if [ "$TARGET_LOWER" = "implement" ]; then
|
|
64
|
-
orch_log "GATE-GUARDIAN: Checking approval for IMPLEMENT phase"
|
|
65
|
-
|
|
66
|
-
PENDING=$(curl -sf --max-time 5 "${API_URL}/api/v1/workflows/${WORKFLOW_ID}/pending-action" \
|
|
67
|
-
-H "Authorization: Bearer $TOKEN" \
|
|
68
|
-
-H "X-Project-ID: $PROJECT_ID" 2>/dev/null) || PENDING=""
|
|
69
|
-
|
|
70
|
-
if [ -n "$PENDING" ]; then
|
|
71
|
-
PENDING_STATUS=$(orch_json_field "$PENDING" "status")
|
|
72
|
-
if [ "$PENDING_STATUS" != "approved" ] && [ "$PENDING_STATUS" != "awaiting_agent" ]; then
|
|
73
|
-
orch_log "GATE-GUARDIAN: DENY (IMPLEMENT requires approval, status=$PENDING_STATUS)"
|
|
74
|
-
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"Gate Guardian: IMPLEMENT requires human approval. Status: ${PENDING_STATUS}.\",\"additionalContext\":\"Ask the user for approval first. Then call mcp__orchestrator-tools__approveAction.\"}}"
|
|
75
|
-
exit 0
|
|
76
|
-
fi
|
|
77
|
-
fi
|
|
78
|
-
fi
|
|
79
|
-
|
|
80
|
-
# Non-IMPLEMENT phases: ALLOW directly.
|
|
81
|
-
# Phase transitions are handled atomically by setPendingAction() via ping-pong-enforcer.
|
|
82
|
-
orch_log "GATE-GUARDIAN: ALLOW (non-IMPLEMENT phase, no gate evaluation required)"
|
|
83
|
-
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"allow\",\"additionalContext\":\"Gate to '${TARGET_PHASE}' allowed.\"}}"
|
|
84
|
-
exit 0
|
package/hooks/orch-helpers.sh
DELETED
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# orch-helpers.sh — Shared helper functions for Orchestrator hooks
|
|
3
|
-
# ADR-013: Deterministic Orchestration
|
|
4
|
-
# Sourced by all hooks. NOT called directly.
|
|
5
|
-
|
|
6
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
-
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
8
|
-
STATE_DIR="$PROJECT_ROOT/.orchestrator/.state"
|
|
9
|
-
DEBUG_LOG="$STATE_DIR/hook-debug.log"
|
|
10
|
-
|
|
11
|
-
# API Configuration (from .env or defaults)
|
|
12
|
-
API_URL="${ORCHESTRATOR_API_URL:-http://localhost:3001}"
|
|
13
|
-
AUTH_EMAIL="${ORCHESTRATOR_ADMIN_EMAIL:-${ORCHESTRATOR_AUTH_EMAIL:-admin@orchestrator.local}}"
|
|
14
|
-
AUTH_PASSWORD="${ORCHESTRATOR_ADMIN_PASSWORD:-${ORCHESTRATOR_AUTH_PASSWORD:-admin123}}"
|
|
15
|
-
PROJECT_ID="${ORCHESTRATOR_PROJECT_ID}"
|
|
16
|
-
|
|
17
|
-
mkdir -p "$STATE_DIR"
|
|
18
|
-
|
|
19
|
-
orch_log() {
|
|
20
|
-
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
# Read stdin (Claude Code passes JSON via stdin)
|
|
24
|
-
orch_read_stdin() {
|
|
25
|
-
if [ ! -t 0 ]; then
|
|
26
|
-
cat
|
|
27
|
-
else
|
|
28
|
-
echo ""
|
|
29
|
-
fi
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
# Get cached JWT token (login if expired)
|
|
33
|
-
orch_get_token() {
|
|
34
|
-
local cached="$STATE_DIR/jwt-token"
|
|
35
|
-
if [ -f "$cached" ]; then
|
|
36
|
-
local age
|
|
37
|
-
age=$(( $(date +%s) - $(stat -c%Y "$cached" 2>/dev/null || stat -f%m "$cached" 2>/dev/null || echo 0) ))
|
|
38
|
-
if [ "$age" -lt 3500 ]; then
|
|
39
|
-
cat "$cached"
|
|
40
|
-
return
|
|
41
|
-
fi
|
|
42
|
-
fi
|
|
43
|
-
|
|
44
|
-
[ -z "$PROJECT_ID" ] && return 1
|
|
45
|
-
|
|
46
|
-
local resp
|
|
47
|
-
resp=$(curl -sf --max-time 5 -X POST "${API_URL}/api/v1/auth/login" \
|
|
48
|
-
-H "Content-Type: application/json" \
|
|
49
|
-
-H "X-Project-ID: $PROJECT_ID" \
|
|
50
|
-
-d "{\"email\":\"${AUTH_EMAIL}\",\"password\":\"${AUTH_PASSWORD}\"}" 2>/dev/null) || return 1
|
|
51
|
-
|
|
52
|
-
local token
|
|
53
|
-
token=$(echo "$resp" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const j=JSON.parse(d);console.log(j.accessToken||j.data?.accessToken||'')}catch{console.log('')}})" 2>/dev/null)
|
|
54
|
-
|
|
55
|
-
if [ -n "$token" ] && [ "$token" != "" ]; then
|
|
56
|
-
echo "$token" > "$cached"
|
|
57
|
-
echo "$token"
|
|
58
|
-
else
|
|
59
|
-
return 1
|
|
60
|
-
fi
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
# Get the active (non-terminal) workflow ID
|
|
64
|
-
# Checks in_progress, awaiting_agent, and awaiting_approval statuses
|
|
65
|
-
orch_get_active_workflow() {
|
|
66
|
-
local token
|
|
67
|
-
token=$(orch_get_token) || return 1
|
|
68
|
-
|
|
69
|
-
local status id
|
|
70
|
-
for status in in_progress awaiting_agent awaiting_approval; do
|
|
71
|
-
local resp
|
|
72
|
-
resp=$(curl -sf --max-time 3 "${API_URL}/api/v1/workflows?status=${status}&limit=1" \
|
|
73
|
-
-H "Authorization: Bearer $token" \
|
|
74
|
-
-H "X-Project-ID: $PROJECT_ID" 2>/dev/null) || continue
|
|
75
|
-
|
|
76
|
-
id=$(echo "$resp" | node -e "
|
|
77
|
-
let d='';
|
|
78
|
-
process.stdin.on('data',c=>d+=c);
|
|
79
|
-
process.stdin.on('end',()=>{
|
|
80
|
-
try {
|
|
81
|
-
const j=JSON.parse(d);
|
|
82
|
-
const wfs=j.data||j;
|
|
83
|
-
const wf=Array.isArray(wfs)?wfs[0]:wfs;
|
|
84
|
-
const id=wf?.id||'';
|
|
85
|
-
if(id) console.log(id); else process.exit(1);
|
|
86
|
-
} catch { process.exit(1); }
|
|
87
|
-
});" 2>/dev/null) && [ -n "$id" ] && echo "$id" && return 0
|
|
88
|
-
done
|
|
89
|
-
return 1
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
# Call getNextAction for a workflow
|
|
93
|
-
orch_get_next_action() {
|
|
94
|
-
local workflow_id="$1"
|
|
95
|
-
local token
|
|
96
|
-
token=$(orch_get_token) || return 1
|
|
97
|
-
|
|
98
|
-
curl -sf --max-time 5 "${API_URL}/api/v1/workflows/${workflow_id}/pending-action" \
|
|
99
|
-
-H "Authorization: Bearer $token" \
|
|
100
|
-
-H "X-Project-ID: $PROJECT_ID" 2>/dev/null
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
# ADR-013 Phase 6: Get the mode of a workflow (quick/standard/full/interactive)
|
|
104
|
-
# Returns the mode string, or empty string for legacy workflows (mode=null)
|
|
105
|
-
# Returns exit code 1 on network/auth error
|
|
106
|
-
orch_get_workflow_mode() {
|
|
107
|
-
local workflow_id="$1"
|
|
108
|
-
local token
|
|
109
|
-
token=$(orch_get_token) || return 1
|
|
110
|
-
|
|
111
|
-
local resp
|
|
112
|
-
resp=$(curl -sf --max-time 3 "${API_URL}/api/v1/workflows/${workflow_id}/status" \
|
|
113
|
-
-H "Authorization: Bearer $token" \
|
|
114
|
-
-H "X-Project-ID: $PROJECT_ID" 2>/dev/null) || return 1
|
|
115
|
-
|
|
116
|
-
orch_json_field "$resp" "mode"
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
# Extract a field from JSON string using node
|
|
120
|
-
orch_json_field() {
|
|
121
|
-
local json="$1"
|
|
122
|
-
local field="$2"
|
|
123
|
-
echo "$json" | node -e "
|
|
124
|
-
let d='';
|
|
125
|
-
process.stdin.on('data',c=>d+=c);
|
|
126
|
-
process.stdin.on('end',()=>{
|
|
127
|
-
try {
|
|
128
|
-
const j=JSON.parse(d);
|
|
129
|
-
const parts='${field}'.split('.');
|
|
130
|
-
let v=j;
|
|
131
|
-
for(const p of parts) v=v?.[p];
|
|
132
|
-
console.log(v||'');
|
|
133
|
-
} catch { console.log(''); }
|
|
134
|
-
});" 2>/dev/null
|
|
135
|
-
}
|