@orchestrator-claude/cli 3.19.0 → 3.21.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/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/templates/base/CLAUDE.md.hbs +1 -0
- package/dist/templates/base/claude/hooks/gate-guardian.ts +137 -39
- package/dist/templates/base/claude/hooks/lib/gate-logic.ts +58 -0
- package/package.json +1 -1
- package/templates/base/CLAUDE.md.hbs +1 -0
- package/templates/base/claude/hooks/gate-guardian.ts +137 -39
- package/templates/base/claude/hooks/lib/gate-logic.ts +58 -0
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -22,6 +22,7 @@ The main conversation IS the orchestrator — it coordinates all phases directly
|
|
|
22
22
|
5. **NEVER edit `.orchestrator/orchestrator-index.json`** — state is in PostgreSQL
|
|
23
23
|
6. **Access artifacts via MCP tools** (`artifactStore`, `artifactRetrieve`), not filesystem paths
|
|
24
24
|
7. **NEVER invoke `Agent(subagent_type: "orchestrator")`** — YOU are the orchestrator (RFC-022)
|
|
25
|
+
8. **Before reading/editing files: if more than a single file or single concern, dispatch a sub-agent** — direct file operations consume 2-5k tokens per file; sub-agents return focused results in ~2k tokens total
|
|
25
26
|
|
|
26
27
|
## How Workflows Work
|
|
27
28
|
|
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* gate-guardian.ts — PreToolUse[advancePhase] hook (
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* gate-guardian.ts — PreToolUse[advancePhase] hook (ADR-017 Phase 4)
|
|
3
|
+
* Adaptive Gate Engine: replaces binary IMPLEMENT-only gate with risk-based engine.
|
|
4
|
+
*
|
|
5
|
+
* Flow:
|
|
6
|
+
* 1. readStdin() → workflowId, targetPhase
|
|
7
|
+
* 2. getWorkflowMode() → quick/interactive: ALLOW immediately
|
|
8
|
+
* 3. validate parse → fail: DENY fail-closed
|
|
9
|
+
* 4. getToken() → fail: DENY fail-closed
|
|
10
|
+
* 5. if targetPhase matches /implement/i → getNextAction() pending-action check:
|
|
11
|
+
* - approved/awaiting_agent → ALLOW (short-circuit, skip classifyRisk)
|
|
12
|
+
* - not approved → DENY
|
|
13
|
+
* 6. classifyRisk(targetPhase) → riskLevel
|
|
14
|
+
* 7. buildGateContext() + deriveGateStrategy() → GateDecision
|
|
15
|
+
* 8. switch strategy:
|
|
16
|
+
* AUTO_APPROVE → allow + telemetry context
|
|
17
|
+
* ASK_HUMAN → deny + approval instructions
|
|
18
|
+
* BLOCK_AND_REQUIRE → deny + requiredAction
|
|
19
|
+
* 9. log(HOOK, JSON summary)
|
|
7
20
|
*/
|
|
8
21
|
|
|
9
22
|
import {
|
|
@@ -14,11 +27,41 @@ import {
|
|
|
14
27
|
getWorkflowMode,
|
|
15
28
|
getNextAction,
|
|
16
29
|
outputPreToolUse,
|
|
30
|
+
type PendingAction,
|
|
17
31
|
} from "./lib/api-client.js";
|
|
32
|
+
import { classifyRisk, deriveGateStrategy, type GateContext } from "./lib/gate-logic.js";
|
|
18
33
|
|
|
19
34
|
const HOOK = "GATE-GUARDIAN";
|
|
20
35
|
|
|
21
|
-
|
|
36
|
+
// ─────────────────────────────────────────────────────────────
|
|
37
|
+
// Dependency injection interface (enables testing without IO)
|
|
38
|
+
// ─────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
export interface HookDeps {
|
|
41
|
+
readStdin: () => Promise<Record<string, unknown>>;
|
|
42
|
+
getWorkflowMode: (id: string) => Promise<string | null>;
|
|
43
|
+
getToken: () => Promise<string | null>;
|
|
44
|
+
getNextAction: (id: string) => Promise<PendingAction | null>;
|
|
45
|
+
output: (opts: { decision: "allow" | "deny"; reason?: string; context?: string }) => void;
|
|
46
|
+
log: (prefix: string, msg: string) => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const defaultDeps: HookDeps = {
|
|
50
|
+
readStdin,
|
|
51
|
+
getWorkflowMode,
|
|
52
|
+
getToken,
|
|
53
|
+
getNextAction,
|
|
54
|
+
output: outputPreToolUse,
|
|
55
|
+
log,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// ─────────────────────────────────────────────────────────────
|
|
59
|
+
// Core logic (exported for testing)
|
|
60
|
+
// ─────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
export async function mainWithDeps(deps: HookDeps): Promise<void> {
|
|
63
|
+
const { readStdin, getWorkflowMode, getToken, getNextAction, output, log } = deps;
|
|
64
|
+
|
|
22
65
|
const stdin = await readStdin();
|
|
23
66
|
log(HOOK, "PreToolUse advancePhase triggered");
|
|
24
67
|
|
|
@@ -27,31 +70,34 @@ async function main(): Promise<void> {
|
|
|
27
70
|
|
|
28
71
|
log(HOOK, `workflow=${workflowId} targetPhase=${targetPhase}`);
|
|
29
72
|
|
|
30
|
-
//
|
|
73
|
+
// ── Step 2: Mode check — quick/interactive bypass gate entirely ──────────
|
|
31
74
|
if (workflowId) {
|
|
32
75
|
const mode = await getWorkflowMode(workflowId);
|
|
33
|
-
log(HOOK, `workflow=${workflowId} mode=${mode
|
|
76
|
+
log(HOOK, `workflow=${workflowId} mode=${mode ?? "legacy"}`);
|
|
34
77
|
|
|
35
78
|
if (mode === "quick") {
|
|
36
|
-
|
|
79
|
+
output({
|
|
37
80
|
decision: "allow",
|
|
38
|
-
context: `
|
|
81
|
+
context: `[GATE] AUTO_APPROVE: quick mode skips gate evaluation for '${targetPhase}'.`,
|
|
39
82
|
});
|
|
83
|
+
log(HOOK, `ALLOW quick mode phase=${targetPhase}`);
|
|
40
84
|
return;
|
|
41
85
|
}
|
|
86
|
+
|
|
42
87
|
if (mode === "interactive") {
|
|
43
|
-
|
|
88
|
+
output({
|
|
44
89
|
decision: "allow",
|
|
45
|
-
context: `
|
|
90
|
+
context: `[GATE] AUTO_APPROVE: interactive mode skips artifact gate evaluation for '${targetPhase}'.`,
|
|
46
91
|
});
|
|
92
|
+
log(HOOK, `ALLOW interactive mode phase=${targetPhase}`);
|
|
47
93
|
return;
|
|
48
94
|
}
|
|
49
95
|
}
|
|
50
96
|
|
|
51
|
-
//
|
|
97
|
+
// ── Step 3: Parse validation — FAIL-CLOSED ───────────────────────────────
|
|
52
98
|
if (!workflowId || !targetPhase) {
|
|
53
99
|
log(HOOK, "DENY (could not parse input)");
|
|
54
|
-
|
|
100
|
+
output({
|
|
55
101
|
decision: "deny",
|
|
56
102
|
reason: "Gate Guardian: Could not parse workflowId or targetPhase.",
|
|
57
103
|
context: "Ensure you pass both workflowId and targetPhase to advancePhase.",
|
|
@@ -59,44 +105,96 @@ async function main(): Promise<void> {
|
|
|
59
105
|
return;
|
|
60
106
|
}
|
|
61
107
|
|
|
62
|
-
// Auth check — FAIL-CLOSED
|
|
108
|
+
// ── Step 4: Auth check — FAIL-CLOSED ────────────────────────────────────
|
|
63
109
|
const token = await getToken();
|
|
64
110
|
if (!token) {
|
|
65
111
|
log(HOOK, "DENY (auth failed)");
|
|
66
|
-
|
|
112
|
+
output({
|
|
67
113
|
decision: "deny",
|
|
68
114
|
reason: "Gate Guardian: Authentication failed.",
|
|
69
|
-
context:
|
|
70
|
-
"Check ORCHESTRATOR_ADMIN_EMAIL, ORCHESTRATOR_ADMIN_PASSWORD, ORCHESTRATOR_PROJECT_ID env vars.",
|
|
115
|
+
context: "Check ORCHESTRATOR_ADMIN_EMAIL, ORCHESTRATOR_ADMIN_PASSWORD, ORCHESTRATOR_PROJECT_ID env vars.",
|
|
71
116
|
});
|
|
72
117
|
return;
|
|
73
118
|
}
|
|
74
119
|
|
|
75
|
-
// IMPLEMENT
|
|
76
|
-
|
|
77
|
-
|
|
120
|
+
// ── Step 5: IMPLEMENT short-circuit (RF-008) ─────────────────────────────
|
|
121
|
+
// If targetPhase contains "implement", check pending-action BEFORE classifyRisk.
|
|
122
|
+
// This prevents AUTO_APPROVE from being applied to implement phases.
|
|
123
|
+
//
|
|
124
|
+
// Phase 4: Only 'implement' phases have the approval path (via pending-action).
|
|
125
|
+
// Other HIGH-risk phases (deploy, migrate, delete) will always ASK_HUMAN → DENY.
|
|
126
|
+
// An approval path for these will be added in Phase 5 (Adaptive Gate V2).
|
|
127
|
+
if (/implement/i.test(targetPhase)) {
|
|
128
|
+
log(HOOK, "Checking approval for IMPLEMENT-type phase");
|
|
78
129
|
const action = await getNextAction(workflowId);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
});
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
130
|
+
const status = action?.pendingAction?.status ?? "";
|
|
131
|
+
|
|
132
|
+
if (status === "approved" || status === "awaiting_agent") {
|
|
133
|
+
output({
|
|
134
|
+
decision: "allow",
|
|
135
|
+
context: `[GATE] IMPLEMENT approved (status=${status}): proceeding to phase '${targetPhase}'.`,
|
|
136
|
+
});
|
|
137
|
+
log(HOOK, JSON.stringify({ hook: "gate-guardian", strategy: "IMPLEMENT_APPROVED", targetPhase, workflowId }));
|
|
138
|
+
return;
|
|
91
139
|
}
|
|
140
|
+
|
|
141
|
+
output({
|
|
142
|
+
decision: "deny",
|
|
143
|
+
reason: `Gate Guardian: IMPLEMENT requires human approval. Status: ${status || "not-set"}.`,
|
|
144
|
+
context: "Ask the user for approval first. Then call mcp__orchestrator-tools__approveAction.",
|
|
145
|
+
});
|
|
146
|
+
log(HOOK, JSON.stringify({ hook: "gate-guardian", strategy: "IMPLEMENT_DENIED", targetPhase, workflowId, reason: "pending-action not approved" }));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── Steps 6–8: Adaptive engine for non-implement phases ──────────────────
|
|
151
|
+
const riskLevel = classifyRisk(targetPhase);
|
|
152
|
+
|
|
153
|
+
const ctx: GateContext = {
|
|
154
|
+
workflowId,
|
|
155
|
+
targetPhase,
|
|
156
|
+
// Reserved for Phase 5+6 — will be populated when OLAP integration provides
|
|
157
|
+
// workflow context. Currently unused by deriveGateStrategy().
|
|
158
|
+
currentPhase: "",
|
|
159
|
+
executionMode: null,
|
|
160
|
+
workflowMode: null,
|
|
161
|
+
riskLevel,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const decision = deriveGateStrategy(ctx);
|
|
165
|
+
|
|
166
|
+
log(HOOK, JSON.stringify({ strategy: decision.strategy, targetPhase, riskLevel, reason: decision.reason }));
|
|
167
|
+
|
|
168
|
+
switch (decision.strategy) {
|
|
169
|
+
case "AUTO_APPROVE":
|
|
170
|
+
output({
|
|
171
|
+
decision: "allow",
|
|
172
|
+
context: `[GATE] AUTO_APPROVE: ${decision.reason}`,
|
|
173
|
+
});
|
|
174
|
+
break;
|
|
175
|
+
|
|
176
|
+
case "ASK_HUMAN":
|
|
177
|
+
output({
|
|
178
|
+
decision: "deny",
|
|
179
|
+
reason: `Gate Guardian: ${decision.reason} Ask user for approval.`,
|
|
180
|
+
});
|
|
181
|
+
break;
|
|
182
|
+
|
|
183
|
+
case "BLOCK_AND_REQUIRE":
|
|
184
|
+
output({
|
|
185
|
+
decision: "deny",
|
|
186
|
+
reason: `Gate Guardian BLOCKED: ${decision.reason}. Required: ${decision.requiredAction ?? "unspecified"}.`,
|
|
187
|
+
});
|
|
188
|
+
break;
|
|
92
189
|
}
|
|
190
|
+
}
|
|
93
191
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
192
|
+
// ─────────────────────────────────────────────────────────────
|
|
193
|
+
// Entry point (production use)
|
|
194
|
+
// ─────────────────────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
async function main(): Promise<void> {
|
|
197
|
+
await mainWithDeps(defaultDeps);
|
|
100
198
|
}
|
|
101
199
|
|
|
102
200
|
main().catch(() => process.exit(0));
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gate-logic.ts — Pure gate logic functions for Adaptive Gate Engine (ADR-017 Phase 4)
|
|
3
|
+
* Zero IO — no imports from node:fs, fetch, or api-client.
|
|
4
|
+
* Fully testable without any mocks.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type RiskLevel = "HIGH" | "LOW";
|
|
8
|
+
export type GateStrategy = "AUTO_APPROVE" | "ASK_HUMAN" | "BLOCK_AND_REQUIRE";
|
|
9
|
+
|
|
10
|
+
export interface GateContext {
|
|
11
|
+
workflowId: string;
|
|
12
|
+
targetPhase: string;
|
|
13
|
+
currentPhase: string;
|
|
14
|
+
executionMode: string | null;
|
|
15
|
+
workflowMode: string | null;
|
|
16
|
+
riskLevel: RiskLevel;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface GateDecision {
|
|
20
|
+
strategy: GateStrategy;
|
|
21
|
+
reason: string;
|
|
22
|
+
requiredAction?: string; // only for BLOCK_AND_REQUIRE
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Classify the risk level of a target phase.
|
|
27
|
+
* HIGH risk: implement, deploy, migrate, delete (case-insensitive, substring match).
|
|
28
|
+
* LOW risk: everything else.
|
|
29
|
+
*/
|
|
30
|
+
export function classifyRisk(targetPhase: string): RiskLevel {
|
|
31
|
+
if (/implement|deploy|migrate|delete/i.test(targetPhase)) {
|
|
32
|
+
return "HIGH";
|
|
33
|
+
}
|
|
34
|
+
return "LOW";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Derive the gate strategy based on context.
|
|
39
|
+
* Rules (evaluated in order):
|
|
40
|
+
* 1. HIGH risk → ASK_HUMAN
|
|
41
|
+
* 2. default → AUTO_APPROVE
|
|
42
|
+
*
|
|
43
|
+
* BLOCK_AND_REQUIRE exists as a type but has no trigger rule yet.
|
|
44
|
+
*/
|
|
45
|
+
export function deriveGateStrategy(ctx: GateContext): GateDecision {
|
|
46
|
+
if (ctx.riskLevel === "HIGH") {
|
|
47
|
+
return {
|
|
48
|
+
strategy: "ASK_HUMAN",
|
|
49
|
+
reason: `Phase '${ctx.targetPhase}' is high-risk and requires human approval before proceeding.`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Default: AUTO_APPROVE
|
|
54
|
+
return {
|
|
55
|
+
strategy: "AUTO_APPROVE",
|
|
56
|
+
reason: `Phase '${ctx.targetPhase}' is low-risk — auto-approved.`,
|
|
57
|
+
};
|
|
58
|
+
}
|
package/package.json
CHANGED
|
@@ -22,6 +22,7 @@ The main conversation IS the orchestrator — it coordinates all phases directly
|
|
|
22
22
|
5. **NEVER edit `.orchestrator/orchestrator-index.json`** — state is in PostgreSQL
|
|
23
23
|
6. **Access artifacts via MCP tools** (`artifactStore`, `artifactRetrieve`), not filesystem paths
|
|
24
24
|
7. **NEVER invoke `Agent(subagent_type: "orchestrator")`** — YOU are the orchestrator (RFC-022)
|
|
25
|
+
8. **Before reading/editing files: if more than a single file or single concern, dispatch a sub-agent** — direct file operations consume 2-5k tokens per file; sub-agents return focused results in ~2k tokens total
|
|
25
26
|
|
|
26
27
|
## How Workflows Work
|
|
27
28
|
|
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* gate-guardian.ts — PreToolUse[advancePhase] hook (
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* gate-guardian.ts — PreToolUse[advancePhase] hook (ADR-017 Phase 4)
|
|
3
|
+
* Adaptive Gate Engine: replaces binary IMPLEMENT-only gate with risk-based engine.
|
|
4
|
+
*
|
|
5
|
+
* Flow:
|
|
6
|
+
* 1. readStdin() → workflowId, targetPhase
|
|
7
|
+
* 2. getWorkflowMode() → quick/interactive: ALLOW immediately
|
|
8
|
+
* 3. validate parse → fail: DENY fail-closed
|
|
9
|
+
* 4. getToken() → fail: DENY fail-closed
|
|
10
|
+
* 5. if targetPhase matches /implement/i → getNextAction() pending-action check:
|
|
11
|
+
* - approved/awaiting_agent → ALLOW (short-circuit, skip classifyRisk)
|
|
12
|
+
* - not approved → DENY
|
|
13
|
+
* 6. classifyRisk(targetPhase) → riskLevel
|
|
14
|
+
* 7. buildGateContext() + deriveGateStrategy() → GateDecision
|
|
15
|
+
* 8. switch strategy:
|
|
16
|
+
* AUTO_APPROVE → allow + telemetry context
|
|
17
|
+
* ASK_HUMAN → deny + approval instructions
|
|
18
|
+
* BLOCK_AND_REQUIRE → deny + requiredAction
|
|
19
|
+
* 9. log(HOOK, JSON summary)
|
|
7
20
|
*/
|
|
8
21
|
|
|
9
22
|
import {
|
|
@@ -14,11 +27,41 @@ import {
|
|
|
14
27
|
getWorkflowMode,
|
|
15
28
|
getNextAction,
|
|
16
29
|
outputPreToolUse,
|
|
30
|
+
type PendingAction,
|
|
17
31
|
} from "./lib/api-client.js";
|
|
32
|
+
import { classifyRisk, deriveGateStrategy, type GateContext } from "./lib/gate-logic.js";
|
|
18
33
|
|
|
19
34
|
const HOOK = "GATE-GUARDIAN";
|
|
20
35
|
|
|
21
|
-
|
|
36
|
+
// ─────────────────────────────────────────────────────────────
|
|
37
|
+
// Dependency injection interface (enables testing without IO)
|
|
38
|
+
// ─────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
export interface HookDeps {
|
|
41
|
+
readStdin: () => Promise<Record<string, unknown>>;
|
|
42
|
+
getWorkflowMode: (id: string) => Promise<string | null>;
|
|
43
|
+
getToken: () => Promise<string | null>;
|
|
44
|
+
getNextAction: (id: string) => Promise<PendingAction | null>;
|
|
45
|
+
output: (opts: { decision: "allow" | "deny"; reason?: string; context?: string }) => void;
|
|
46
|
+
log: (prefix: string, msg: string) => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const defaultDeps: HookDeps = {
|
|
50
|
+
readStdin,
|
|
51
|
+
getWorkflowMode,
|
|
52
|
+
getToken,
|
|
53
|
+
getNextAction,
|
|
54
|
+
output: outputPreToolUse,
|
|
55
|
+
log,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// ─────────────────────────────────────────────────────────────
|
|
59
|
+
// Core logic (exported for testing)
|
|
60
|
+
// ─────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
export async function mainWithDeps(deps: HookDeps): Promise<void> {
|
|
63
|
+
const { readStdin, getWorkflowMode, getToken, getNextAction, output, log } = deps;
|
|
64
|
+
|
|
22
65
|
const stdin = await readStdin();
|
|
23
66
|
log(HOOK, "PreToolUse advancePhase triggered");
|
|
24
67
|
|
|
@@ -27,31 +70,34 @@ async function main(): Promise<void> {
|
|
|
27
70
|
|
|
28
71
|
log(HOOK, `workflow=${workflowId} targetPhase=${targetPhase}`);
|
|
29
72
|
|
|
30
|
-
//
|
|
73
|
+
// ── Step 2: Mode check — quick/interactive bypass gate entirely ──────────
|
|
31
74
|
if (workflowId) {
|
|
32
75
|
const mode = await getWorkflowMode(workflowId);
|
|
33
|
-
log(HOOK, `workflow=${workflowId} mode=${mode
|
|
76
|
+
log(HOOK, `workflow=${workflowId} mode=${mode ?? "legacy"}`);
|
|
34
77
|
|
|
35
78
|
if (mode === "quick") {
|
|
36
|
-
|
|
79
|
+
output({
|
|
37
80
|
decision: "allow",
|
|
38
|
-
context: `
|
|
81
|
+
context: `[GATE] AUTO_APPROVE: quick mode skips gate evaluation for '${targetPhase}'.`,
|
|
39
82
|
});
|
|
83
|
+
log(HOOK, `ALLOW quick mode phase=${targetPhase}`);
|
|
40
84
|
return;
|
|
41
85
|
}
|
|
86
|
+
|
|
42
87
|
if (mode === "interactive") {
|
|
43
|
-
|
|
88
|
+
output({
|
|
44
89
|
decision: "allow",
|
|
45
|
-
context: `
|
|
90
|
+
context: `[GATE] AUTO_APPROVE: interactive mode skips artifact gate evaluation for '${targetPhase}'.`,
|
|
46
91
|
});
|
|
92
|
+
log(HOOK, `ALLOW interactive mode phase=${targetPhase}`);
|
|
47
93
|
return;
|
|
48
94
|
}
|
|
49
95
|
}
|
|
50
96
|
|
|
51
|
-
//
|
|
97
|
+
// ── Step 3: Parse validation — FAIL-CLOSED ───────────────────────────────
|
|
52
98
|
if (!workflowId || !targetPhase) {
|
|
53
99
|
log(HOOK, "DENY (could not parse input)");
|
|
54
|
-
|
|
100
|
+
output({
|
|
55
101
|
decision: "deny",
|
|
56
102
|
reason: "Gate Guardian: Could not parse workflowId or targetPhase.",
|
|
57
103
|
context: "Ensure you pass both workflowId and targetPhase to advancePhase.",
|
|
@@ -59,44 +105,96 @@ async function main(): Promise<void> {
|
|
|
59
105
|
return;
|
|
60
106
|
}
|
|
61
107
|
|
|
62
|
-
// Auth check — FAIL-CLOSED
|
|
108
|
+
// ── Step 4: Auth check — FAIL-CLOSED ────────────────────────────────────
|
|
63
109
|
const token = await getToken();
|
|
64
110
|
if (!token) {
|
|
65
111
|
log(HOOK, "DENY (auth failed)");
|
|
66
|
-
|
|
112
|
+
output({
|
|
67
113
|
decision: "deny",
|
|
68
114
|
reason: "Gate Guardian: Authentication failed.",
|
|
69
|
-
context:
|
|
70
|
-
"Check ORCHESTRATOR_ADMIN_EMAIL, ORCHESTRATOR_ADMIN_PASSWORD, ORCHESTRATOR_PROJECT_ID env vars.",
|
|
115
|
+
context: "Check ORCHESTRATOR_ADMIN_EMAIL, ORCHESTRATOR_ADMIN_PASSWORD, ORCHESTRATOR_PROJECT_ID env vars.",
|
|
71
116
|
});
|
|
72
117
|
return;
|
|
73
118
|
}
|
|
74
119
|
|
|
75
|
-
// IMPLEMENT
|
|
76
|
-
|
|
77
|
-
|
|
120
|
+
// ── Step 5: IMPLEMENT short-circuit (RF-008) ─────────────────────────────
|
|
121
|
+
// If targetPhase contains "implement", check pending-action BEFORE classifyRisk.
|
|
122
|
+
// This prevents AUTO_APPROVE from being applied to implement phases.
|
|
123
|
+
//
|
|
124
|
+
// Phase 4: Only 'implement' phases have the approval path (via pending-action).
|
|
125
|
+
// Other HIGH-risk phases (deploy, migrate, delete) will always ASK_HUMAN → DENY.
|
|
126
|
+
// An approval path for these will be added in Phase 5 (Adaptive Gate V2).
|
|
127
|
+
if (/implement/i.test(targetPhase)) {
|
|
128
|
+
log(HOOK, "Checking approval for IMPLEMENT-type phase");
|
|
78
129
|
const action = await getNextAction(workflowId);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
});
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
130
|
+
const status = action?.pendingAction?.status ?? "";
|
|
131
|
+
|
|
132
|
+
if (status === "approved" || status === "awaiting_agent") {
|
|
133
|
+
output({
|
|
134
|
+
decision: "allow",
|
|
135
|
+
context: `[GATE] IMPLEMENT approved (status=${status}): proceeding to phase '${targetPhase}'.`,
|
|
136
|
+
});
|
|
137
|
+
log(HOOK, JSON.stringify({ hook: "gate-guardian", strategy: "IMPLEMENT_APPROVED", targetPhase, workflowId }));
|
|
138
|
+
return;
|
|
91
139
|
}
|
|
140
|
+
|
|
141
|
+
output({
|
|
142
|
+
decision: "deny",
|
|
143
|
+
reason: `Gate Guardian: IMPLEMENT requires human approval. Status: ${status || "not-set"}.`,
|
|
144
|
+
context: "Ask the user for approval first. Then call mcp__orchestrator-tools__approveAction.",
|
|
145
|
+
});
|
|
146
|
+
log(HOOK, JSON.stringify({ hook: "gate-guardian", strategy: "IMPLEMENT_DENIED", targetPhase, workflowId, reason: "pending-action not approved" }));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── Steps 6–8: Adaptive engine for non-implement phases ──────────────────
|
|
151
|
+
const riskLevel = classifyRisk(targetPhase);
|
|
152
|
+
|
|
153
|
+
const ctx: GateContext = {
|
|
154
|
+
workflowId,
|
|
155
|
+
targetPhase,
|
|
156
|
+
// Reserved for Phase 5+6 — will be populated when OLAP integration provides
|
|
157
|
+
// workflow context. Currently unused by deriveGateStrategy().
|
|
158
|
+
currentPhase: "",
|
|
159
|
+
executionMode: null,
|
|
160
|
+
workflowMode: null,
|
|
161
|
+
riskLevel,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const decision = deriveGateStrategy(ctx);
|
|
165
|
+
|
|
166
|
+
log(HOOK, JSON.stringify({ strategy: decision.strategy, targetPhase, riskLevel, reason: decision.reason }));
|
|
167
|
+
|
|
168
|
+
switch (decision.strategy) {
|
|
169
|
+
case "AUTO_APPROVE":
|
|
170
|
+
output({
|
|
171
|
+
decision: "allow",
|
|
172
|
+
context: `[GATE] AUTO_APPROVE: ${decision.reason}`,
|
|
173
|
+
});
|
|
174
|
+
break;
|
|
175
|
+
|
|
176
|
+
case "ASK_HUMAN":
|
|
177
|
+
output({
|
|
178
|
+
decision: "deny",
|
|
179
|
+
reason: `Gate Guardian: ${decision.reason} Ask user for approval.`,
|
|
180
|
+
});
|
|
181
|
+
break;
|
|
182
|
+
|
|
183
|
+
case "BLOCK_AND_REQUIRE":
|
|
184
|
+
output({
|
|
185
|
+
decision: "deny",
|
|
186
|
+
reason: `Gate Guardian BLOCKED: ${decision.reason}. Required: ${decision.requiredAction ?? "unspecified"}.`,
|
|
187
|
+
});
|
|
188
|
+
break;
|
|
92
189
|
}
|
|
190
|
+
}
|
|
93
191
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
192
|
+
// ─────────────────────────────────────────────────────────────
|
|
193
|
+
// Entry point (production use)
|
|
194
|
+
// ─────────────────────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
async function main(): Promise<void> {
|
|
197
|
+
await mainWithDeps(defaultDeps);
|
|
100
198
|
}
|
|
101
199
|
|
|
102
200
|
main().catch(() => process.exit(0));
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gate-logic.ts — Pure gate logic functions for Adaptive Gate Engine (ADR-017 Phase 4)
|
|
3
|
+
* Zero IO — no imports from node:fs, fetch, or api-client.
|
|
4
|
+
* Fully testable without any mocks.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type RiskLevel = "HIGH" | "LOW";
|
|
8
|
+
export type GateStrategy = "AUTO_APPROVE" | "ASK_HUMAN" | "BLOCK_AND_REQUIRE";
|
|
9
|
+
|
|
10
|
+
export interface GateContext {
|
|
11
|
+
workflowId: string;
|
|
12
|
+
targetPhase: string;
|
|
13
|
+
currentPhase: string;
|
|
14
|
+
executionMode: string | null;
|
|
15
|
+
workflowMode: string | null;
|
|
16
|
+
riskLevel: RiskLevel;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface GateDecision {
|
|
20
|
+
strategy: GateStrategy;
|
|
21
|
+
reason: string;
|
|
22
|
+
requiredAction?: string; // only for BLOCK_AND_REQUIRE
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Classify the risk level of a target phase.
|
|
27
|
+
* HIGH risk: implement, deploy, migrate, delete (case-insensitive, substring match).
|
|
28
|
+
* LOW risk: everything else.
|
|
29
|
+
*/
|
|
30
|
+
export function classifyRisk(targetPhase: string): RiskLevel {
|
|
31
|
+
if (/implement|deploy|migrate|delete/i.test(targetPhase)) {
|
|
32
|
+
return "HIGH";
|
|
33
|
+
}
|
|
34
|
+
return "LOW";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Derive the gate strategy based on context.
|
|
39
|
+
* Rules (evaluated in order):
|
|
40
|
+
* 1. HIGH risk → ASK_HUMAN
|
|
41
|
+
* 2. default → AUTO_APPROVE
|
|
42
|
+
*
|
|
43
|
+
* BLOCK_AND_REQUIRE exists as a type but has no trigger rule yet.
|
|
44
|
+
*/
|
|
45
|
+
export function deriveGateStrategy(ctx: GateContext): GateDecision {
|
|
46
|
+
if (ctx.riskLevel === "HIGH") {
|
|
47
|
+
return {
|
|
48
|
+
strategy: "ASK_HUMAN",
|
|
49
|
+
reason: `Phase '${ctx.targetPhase}' is high-risk and requires human approval before proceeding.`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Default: AUTO_APPROVE
|
|
54
|
+
return {
|
|
55
|
+
strategy: "AUTO_APPROVE",
|
|
56
|
+
reason: `Phase '${ctx.targetPhase}' is low-risk — auto-approved.`,
|
|
57
|
+
};
|
|
58
|
+
}
|