@orchestrator-claude/cli 3.19.0 → 3.20.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 CHANGED
@@ -12,5 +12,5 @@
12
12
  /**
13
13
  * CLI version
14
14
  */
15
- export declare const CLI_VERSION = "3.19.0";
15
+ export declare const CLI_VERSION = "3.20.0";
16
16
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -24,7 +24,7 @@ import { OutputFormatter } from './formatters/OutputFormatter.js';
24
24
  /**
25
25
  * CLI version
26
26
  */
27
- export const CLI_VERSION = '3.19.0';
27
+ export const CLI_VERSION = '3.20.0';
28
28
  /**
29
29
  * Main CLI function
30
30
  */
@@ -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 (RFC-022 Phase 2)
3
- * Replaces gate-guardian.sh
4
- * Guards IMPLEMENT phase advance (requires human approval).
5
- * ALLOWs quick/interactive modes and non-IMPLEMENT phases.
6
- * FAIL-CLOSED on parse errors.
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
- async function main(): Promise<void> {
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
- // Check workflow mode — quick/interactive skip gate
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 || "legacy"}`);
76
+ log(HOOK, `workflow=${workflowId} mode=${mode ?? "legacy"}`);
34
77
 
35
78
  if (mode === "quick") {
36
- outputPreToolUse({
79
+ output({
37
80
  decision: "allow",
38
- context: `Gate to '${targetPhase}' allowed: quick mode skips gate evaluation.`,
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
- outputPreToolUse({
88
+ output({
44
89
  decision: "allow",
45
- context: `Gate to '${targetPhase}' allowed: interactive mode skips artifact gate evaluation.`,
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
- // FAIL-CLOSED: can't parse input DENY
97
+ // ── Step 3: Parse validation FAIL-CLOSED ───────────────────────────────
52
98
  if (!workflowId || !targetPhase) {
53
99
  log(HOOK, "DENY (could not parse input)");
54
- outputPreToolUse({
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
- outputPreToolUse({
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 phase requires approval
76
- if (targetPhase.toLowerCase() === "implement") {
77
- log(HOOK, "Checking approval for IMPLEMENT phase");
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
- if (action) {
80
- const status = action.pendingAction?.status || "";
81
- if (status !== "approved" && status !== "awaiting_agent") {
82
- log(HOOK, `DENY (IMPLEMENT requires approval, status=${status})`);
83
- outputPreToolUse({
84
- decision: "deny",
85
- reason: `Gate Guardian: IMPLEMENT requires human approval. Status: ${status}.`,
86
- context:
87
- "Ask the user for approval first. Then call mcp__orchestrator-tools__approveAction.",
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
- // Non-IMPLEMENT or approved: ALLOW
95
- log(HOOK, `ALLOW (phase=${targetPhase})`);
96
- outputPreToolUse({
97
- decision: "allow",
98
- context: `Gate to '${targetPhase}' allowed.`,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orchestrator-claude/cli",
3
- "version": "3.19.0",
3
+ "version": "3.20.0",
4
4
  "description": "Orchestrator CLI - Project scaffolding, migration and management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -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 (RFC-022 Phase 2)
3
- * Replaces gate-guardian.sh
4
- * Guards IMPLEMENT phase advance (requires human approval).
5
- * ALLOWs quick/interactive modes and non-IMPLEMENT phases.
6
- * FAIL-CLOSED on parse errors.
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
- async function main(): Promise<void> {
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
- // Check workflow mode — quick/interactive skip gate
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 || "legacy"}`);
76
+ log(HOOK, `workflow=${workflowId} mode=${mode ?? "legacy"}`);
34
77
 
35
78
  if (mode === "quick") {
36
- outputPreToolUse({
79
+ output({
37
80
  decision: "allow",
38
- context: `Gate to '${targetPhase}' allowed: quick mode skips gate evaluation.`,
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
- outputPreToolUse({
88
+ output({
44
89
  decision: "allow",
45
- context: `Gate to '${targetPhase}' allowed: interactive mode skips artifact gate evaluation.`,
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
- // FAIL-CLOSED: can't parse input DENY
97
+ // ── Step 3: Parse validation FAIL-CLOSED ───────────────────────────────
52
98
  if (!workflowId || !targetPhase) {
53
99
  log(HOOK, "DENY (could not parse input)");
54
- outputPreToolUse({
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
- outputPreToolUse({
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 phase requires approval
76
- if (targetPhase.toLowerCase() === "implement") {
77
- log(HOOK, "Checking approval for IMPLEMENT phase");
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
- if (action) {
80
- const status = action.pendingAction?.status || "";
81
- if (status !== "approved" && status !== "awaiting_agent") {
82
- log(HOOK, `DENY (IMPLEMENT requires approval, status=${status})`);
83
- outputPreToolUse({
84
- decision: "deny",
85
- reason: `Gate Guardian: IMPLEMENT requires human approval. Status: ${status}.`,
86
- context:
87
- "Ask the user for approval first. Then call mcp__orchestrator-tools__approveAction.",
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
- // Non-IMPLEMENT or approved: ALLOW
95
- log(HOOK, `ALLOW (phase=${targetPhase})`);
96
- outputPreToolUse({
97
- decision: "allow",
98
- context: `Gate to '${targetPhase}' allowed.`,
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
+ }