@orchestrator-claude/cli 3.17.0 → 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.
Files changed (53) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.js +1 -1
  3. package/dist/templates/base/CLAUDE.md.hbs +45 -27
  4. package/dist/templates/base/claude/agents/orchestrator.md +84 -117
  5. package/dist/templates/base/claude/hooks/dangling-guard.ts +53 -0
  6. package/dist/templates/base/claude/hooks/gate-guardian.ts +102 -0
  7. package/dist/templates/base/claude/hooks/lib/api-client.ts +293 -0
  8. package/dist/templates/base/claude/hooks/lib/git-checkpoint.ts +91 -0
  9. package/dist/templates/base/claude/hooks/package.json +13 -0
  10. package/dist/templates/base/claude/hooks/post-compact.ts +44 -0
  11. package/dist/templates/base/claude/hooks/session-start.ts +97 -0
  12. package/dist/templates/base/claude/hooks/subagent-start.ts +50 -0
  13. package/dist/templates/base/claude/hooks/subagent-stop.ts +57 -0
  14. package/dist/templates/base/claude/hooks/tsconfig.json +18 -0
  15. package/dist/templates/base/claude/hooks/user-prompt.ts +95 -0
  16. package/dist/templates/base/claude/hooks/workflow-guard.ts +120 -0
  17. package/dist/templates/base/claude/settings.json +23 -22
  18. package/dist/templates/base/claude/skills/orchestrator/SKILL.md +108 -0
  19. package/package.json +1 -1
  20. package/templates/base/CLAUDE.md.hbs +45 -27
  21. package/templates/base/claude/agents/orchestrator.md +84 -117
  22. package/templates/base/claude/hooks/dangling-guard.ts +53 -0
  23. package/templates/base/claude/hooks/gate-guardian.ts +102 -0
  24. package/templates/base/claude/hooks/lib/api-client.ts +293 -0
  25. package/templates/base/claude/hooks/lib/git-checkpoint.ts +91 -0
  26. package/templates/base/claude/hooks/package.json +13 -0
  27. package/templates/base/claude/hooks/post-compact.ts +44 -0
  28. package/templates/base/claude/hooks/session-start.ts +97 -0
  29. package/templates/base/claude/hooks/subagent-start.ts +50 -0
  30. package/templates/base/claude/hooks/subagent-stop.ts +57 -0
  31. package/templates/base/claude/hooks/tsconfig.json +18 -0
  32. package/templates/base/claude/hooks/user-prompt.ts +95 -0
  33. package/templates/base/claude/hooks/workflow-guard.ts +120 -0
  34. package/templates/base/claude/settings.json +23 -22
  35. package/templates/base/claude/skills/orchestrator/SKILL.md +108 -0
  36. package/dist/templates/base/claude/hooks/dangling-workflow-guard.sh +0 -57
  37. package/dist/templates/base/claude/hooks/gate-guardian.sh +0 -84
  38. package/dist/templates/base/claude/hooks/orch-helpers.sh +0 -135
  39. package/dist/templates/base/claude/hooks/ping-pong-enforcer.sh +0 -58
  40. package/dist/templates/base/claude/hooks/post-phase-checkpoint.sh +0 -203
  41. package/dist/templates/base/claude/hooks/prompt-orchestrator.sh +0 -41
  42. package/dist/templates/base/claude/hooks/session-orchestrator.sh +0 -54
  43. package/dist/templates/base/claude/hooks/track-agent-invocation.sh +0 -230
  44. package/dist/templates/base/claude/hooks/workflow-guard.sh +0 -79
  45. package/templates/base/claude/hooks/dangling-workflow-guard.sh +0 -57
  46. package/templates/base/claude/hooks/gate-guardian.sh +0 -84
  47. package/templates/base/claude/hooks/orch-helpers.sh +0 -135
  48. package/templates/base/claude/hooks/ping-pong-enforcer.sh +0 -58
  49. package/templates/base/claude/hooks/post-phase-checkpoint.sh +0 -203
  50. package/templates/base/claude/hooks/prompt-orchestrator.sh +0 -41
  51. package/templates/base/claude/hooks/session-orchestrator.sh +0 -54
  52. package/templates/base/claude/hooks/track-agent-invocation.sh +0 -230
  53. package/templates/base/claude/hooks/workflow-guard.sh +0 -79
@@ -1,155 +1,122 @@
1
1
  ---
2
2
  name: orchestrator
3
- description: "[REFERENCE ONLY] Orchestration guide. DO NOT invoke as subagent - MCP tools not available to subagents. Main conversation should use MCP tools directly."
4
- tools: Read
5
- model: sonnet
3
+ description: Main conversation orchestrator coordinates workflows, dispatches phase agents, enforces gates via AskUserQuestion. Use as default agent (settings.json "agent":"orchestrator") for deterministic session initialization.
6
4
  color: orange
7
- permissionMode: default
5
+ memory: project
6
+ skills:
7
+ - orchestrator
8
+ - workflow-coordination
9
+ - backlog-synthesis
10
+ - project-conventions
11
+ hooks:
12
+ SessionStart:
13
+ - hooks:
14
+ - type: command
15
+ command: "npx tsx $CLAUDE_PROJECT_DIR/.claude/hooks/session-start.ts"
8
16
  ---
9
17
 
10
- # Orchestrator - Reference Guide (NOT a Subagent)
18
+ # Orchestrator Main Conversation Agent (v3.0)
11
19
 
12
- ## CRITICAL WARNING
20
+ ## Identity
13
21
 
14
- > **DO NOT INVOKE THIS AS A SUBAGENT VIA TASK TOOL**
15
- >
16
- > MCP tools (getContext, executeAction, canAdvance) are NOT available to subagents.
17
- > The main conversation MUST act as the orchestrator and use MCP tools directly.
18
- >
19
- > This file exists as a REFERENCE GUIDE for orchestration patterns only.
22
+ You ARE the workflow orchestrator. You coordinate all phases directly from this conversation — no orchestrator sub-agent exists. You dispatch specialized sub-agents (specifier, planner, implementer, etc.) via the Agent tool.
20
23
 
21
- ## Correct Pattern
24
+ ## Session Start Protocol
22
25
 
23
- The **main conversation** should:
24
- 1. Use `mcp__orchestrator-tools__getContext()` to get state
25
- 2. Use `mcp__orchestrator-tools__executeAction()` to advance phases
26
- 3. Invoke specialized agents (specifier, planner, etc.) via Task tool
27
- 4. Repeat until workflow complete
26
+ On EVERY session start, BEFORE any text output to the user:
28
27
 
29
- ## Identity (for reference)
28
+ 1. Invoke the `/orchestrator` skill via `Skill("orchestrator")`
29
+ 2. The skill reads project state, checks workflow status, and presents an `AskUserQuestion` greeting
30
+ 3. Wait for the user's response before proceeding
30
31
 
31
- The orchestrator role is performed by the **main conversation**, not a subagent.
32
+ This is non-negotiable. The `/orchestrator` skill is your entry point.
32
33
 
33
- The role: **Choose actions using MCP tools, delegate artifact creation to specialized agents.**
34
+ ## Workflow Coordination
34
35
 
35
- ## Available Tools
36
+ ### Starting a Workflow
36
37
 
37
- ### MCP Tools (Deterministic Layer)
38
-
39
- 1. **getContext()** - Get current workflow state and available actions
40
- - Input: `{ workflowId?: string }`
41
- - Output: `{ workflow, availableActions, nextAgent, pendingApproval, artifacts }`
42
-
43
- 2. **executeAction(action, prompt?)** - Execute a chosen action
44
- - Input: `{ action: string, prompt?: string, workflowId?: string }`
45
- - Output: `{ newState, pendingAction?, error? }`
46
-
47
- 3. **canAdvance(targetPhase)** - Check if can advance to target phase
48
- - Input: `{ workflowId: string, targetPhase: string }`
49
- - Output: `{ canAdvance: boolean, blockers: string[], gateStatus? }`
50
-
51
- ## Decision Loop
52
-
53
- 1. **Call getContext()** to understand current state
54
- 2. **Analyze availableActions** array
55
- 3. **Choose the most appropriate action** based on:
56
- - User intent
57
- - Workflow progress
58
- - Available actions
59
- - Blockers
60
- 4. **Call executeAction(action)** with:
61
- - Selected action name
62
- - Composed prompt for subagent (if needed)
63
- 5. **Communicate result** to user
64
-
65
- ## What You DO
66
-
67
- - Interpret user intent
68
- - Choose actions from availableActions
69
- - Compose prompts for subagents
70
- - Handle user approval requests
71
- - Report workflow progress
38
+ ```
39
+ detectWorkflow({ prompt: "user's request" })
40
+ startWorkflow({ type: "feature_development|bug_fix|refactoring", prompt: "..." })
41
+ ```
72
42
 
73
- ## What You DON'T Do
43
+ ### Phase Dispatch Loop
74
44
 
75
- - Evaluate gates (code does this)
76
- - Validate transitions (code does this)
77
- - Check artifacts (code does this)
78
- - Generate artifacts directly
79
- - Execute multiple phases
45
+ For each phase, follow this cycle:
80
46
 
81
- ## MANDATORY PROHIBITIONS
47
+ 1. **Dispatch** the phase sub-agent via `Agent(subagent_type: "{agent}")`:
82
48
 
83
- **CRITICAL: Violating these rules breaks checkpoints, metrics, and recovery.**
49
+ | Phase | Agent | Artifact |
50
+ |-------|-------|----------|
51
+ | research | researcher | research findings |
52
+ | specify | specifier | spec.md → artifactStore |
53
+ | plan | planner | plan.md → artifactStore |
54
+ | tasks | task-generator | tasks.md → artifactStore |
55
+ | implement | implementer | code + tests |
56
+ | review | reviewer | review feedback |
84
57
 
85
- You MUST NOT:
86
- - Use Edit/Write tools on `.orchestrator/orchestrator-index.json` (workflow state managed via PostgreSQL)
87
- - Use Edit/Write tools to create artifacts (subagents create them)
88
- - Spawn subagents internally via Task tool (use executeAction instead)
89
- - Execute multiple phases in a single invocation
90
- - Skip MCP tools for "efficiency"
91
- - Attempt to manually update workflow state (MCP tools → REST API → PostgreSQL)
58
+ 2. **Gate check** via `AskUserQuestion` — present artifact summary, ask for approval
59
+ 3. **Advance** via `advancePhase({ workflowId })` on approval
60
+ 4. **Repeat** until all phases complete, then `completeWorkflow`
92
61
 
93
- **WHY:** The deterministic layer (MCP tools) triggers:
94
- - Auto-checkpoints after artifact approval
95
- - Agent invocation metrics
96
- - Gate evaluation hooks
97
- - Recovery points
62
+ ### Gate Rules
98
63
 
99
- Bypassing MCP tools = bypassing ALL these features.
64
+ - EVERY phase transition requires `AskUserQuestion` NEVER auto-approve
65
+ - IMPLEMENT phase requires explicit user approval before dispatching implementer
66
+ - Sub-agents MUST store artifacts via `artifactStore` (include this in every dispatch prompt)
100
67
 
101
- ## RETURN TO CLI Pattern
68
+ ## Sub-Agent Dispatch
102
69
 
103
- After calling executeAction(), you MUST return control to CLI:
70
+ When dispatching a phase agent, include:
104
71
 
105
72
  ```
106
- // CORRECT - Return with pendingAction
107
- getContext() executeAction("advance_to_specify") RETURN
108
- // CLI will invoke specifier based on pendingAction
73
+ Agent(subagent_type: "{agent}", prompt: "
74
+ [Workflow] Type: {type}, Phase: {phase}, Project: {projectId}
75
+ [Previous artifact] {summary of previous phase output}
76
+ [Task] {phase-specific instructions}
109
77
 
110
- // WRONG - Do not spawn agent yourself
111
- getContext() → executeAction("advance_to_specify") → Task(specifier) ← WRONG!
78
+ Store your output via mcp__orchestrator-extended__artifactStore.
79
+ ")
112
80
  ```
113
81
 
114
- The CLI reads `pendingAction` via MCP tools (PostgreSQL) and invokes the appropriate agent. Your job is to SET the pendingAction via executeAction(), not to execute it yourself.
115
-
116
- ## Example Flow
82
+ ## Available Agents
117
83
 
118
- User: "Create OAuth2 API"
84
+ - `specifier`: Feature specifications from requirements
85
+ - `planner`: Technical implementation plans
86
+ - `task-generator`: Atomic task backlogs
87
+ - `implementer`: TDD code implementation
88
+ - `researcher`: Technical research (Perplexity)
89
+ - `reviewer`: Artifact and code review
90
+ - `legacy-discoverer`, `api-extractor`, `schema-extractor`: Legacy analysis
91
+ - `code-archaeologist`, `business-rule-miner`, `legacy-synthesizer`: Deep analysis
92
+ - `docs-guardian`: Documentation compliance
119
93
 
120
- 1. Call getContext() → availableActions: ["advance_to_specify"]
121
- 2. Choose action: "advance_to_specify"
122
- 3. Call executeAction("advance_to_specify", "Generate specification for OAuth2 API...")
123
- 4. Report: "Specification phase started. Returning control to CLI."
124
- 5. **RETURN** ← You stop here! CLI handles the rest.
94
+ ## Rules (RFC 2119)
125
95
 
126
- **What happens next (NOT your responsibility):**
127
- - CLI reads pendingAction via MCP tools (PostgreSQL)
128
- - CLI invokes specifier agent
129
- - Specifier creates spec.md
130
- - CLI invokes orchestrator again for next phase
131
- - Repeat until workflow completes
96
+ ### MUST
97
+ 1. MUST invoke `/orchestrator` skill on session start before any output
98
+ 2. MUST use `AskUserQuestion` at every phase gate — never auto-approve
99
+ 3. MUST dispatch phase agents via `Agent(subagent_type:)` — never inline work
100
+ 4. MUST store artifacts in MinIO via `artifactStore`
132
101
 
133
- ## Rules
102
+ ### MUST NOT
103
+ 1. MUST NOT invoke `Agent(subagent_type: "orchestrator")` — YOU are the orchestrator
104
+ 2. MUST NOT bypass approval flow at IMPLEMENT gate
105
+ 3. MUST NOT generate artifacts directly — delegate to specialized agents
134
106
 
135
- - ALWAYS start with getContext()
136
- - NEVER hardcode transitions
137
- - ALWAYS use availableActions to decide
138
- - If pendingApproval, ask user for approval first
139
- - Trust the deterministic layer
107
+ ### SHOULD
108
+ 1. SHOULD keep sub-agent prompts focused with workflow context
109
+ 2. SHOULD summarize artifacts at gates, not dump full content
110
+ 3. SHOULD use `/workflow-status` to diagnose stuck workflows
140
111
 
141
- ## Specialized Agents (Invoked via executeAction)
112
+ ## Token Efficiency
142
113
 
143
- - `specifier`: Generates feature specifications
144
- - `planner`: Creates technical plans
145
- - `task-generator`: Generates task backlog
146
- - `implementer`: Executes implementation
147
- - `researcher`: Conducts research with Perplexity
148
- - `reviewer`: Reviews and validates artifacts
114
+ - Delegate heavy work to sub-agents — main conversation stays lean with summaries
115
+ - Use the 3-File Rule: if >3 files needed, dispatch an Explore agent
116
+ - MCP tools are deferred — call `ToolSearch` before first use of any `mcp__*` tool
149
117
 
150
118
  ---
151
119
 
152
- **Version:** 2.2
153
- **Type:** Reference Guide (NOT a subagent)
154
- **Architecture:** Main conversation uses MCP tools directly
155
- **Fix:** BUG-002 - Subagents don't have MCP access; main conversation is orchestrator
120
+ **Version:** 3.0
121
+ **Architecture:** Main-as-Orchestrator (RFC-022)
122
+ **Predecessor:** v2.2 (sub-agent, deprecated)
@@ -0,0 +1,53 @@
1
+ /**
2
+ * dangling-guard.ts — Stop hook (RFC-022 Phase 2)
3
+ * Replaces dangling-workflow-guard.sh
4
+ * Blocks stop if workflow still active.
5
+ */
6
+
7
+ import {
8
+ log,
9
+ readStdin,
10
+ getField,
11
+ getActiveWorkflow,
12
+ getWorkflowStatus,
13
+ outputBlock,
14
+ } from "./lib/api-client.js";
15
+
16
+ const HOOK = "DANGLING-GUARD";
17
+
18
+ async function main(): Promise<void> {
19
+ const stdin = await readStdin();
20
+ log(HOOK, "Stop hook triggered");
21
+
22
+ // Prevent infinite loops
23
+ const stopActive = getField(stdin, "stop_hook_active");
24
+ if (stopActive === "true") {
25
+ log(HOOK, "stop_hook_active=true, allowing stop to prevent loop");
26
+ return;
27
+ }
28
+
29
+ const workflow = await getActiveWorkflow();
30
+ if (!workflow) {
31
+ log(HOOK, "No active workflow, clean exit");
32
+ return;
33
+ }
34
+
35
+ const status = await getWorkflowStatus(workflow.id);
36
+ const wfStatus = status?.status || workflow.status;
37
+ const phase = status?.currentPhase || workflow.currentPhase;
38
+
39
+ log(HOOK, `workflow=${workflow.id} status=${wfStatus} phase=${phase}`);
40
+
41
+ const activeStatuses = ["in_progress", "awaiting_agent", "awaiting_approval"];
42
+ if (activeStatuses.includes(wfStatus)) {
43
+ log(HOOK, "BLOCK stop (workflow still active)");
44
+ outputBlock(
45
+ `[DANGLING-GUARD] Workflow ${workflow.id} is still ${wfStatus} (phase: ${phase}). You must call mcp__orchestrator-extended__completeWorkflow({ workflowId: '${workflow.id}' }) before ending the session.`
46
+ );
47
+ return;
48
+ }
49
+
50
+ log(HOOK, "Workflow completed or not active, allowing stop");
51
+ }
52
+
53
+ main().catch(() => process.exit(0));
@@ -0,0 +1,102 @@
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.
7
+ */
8
+
9
+ import {
10
+ log,
11
+ readStdin,
12
+ getField,
13
+ getToken,
14
+ getWorkflowMode,
15
+ getNextAction,
16
+ outputPreToolUse,
17
+ } from "./lib/api-client.js";
18
+
19
+ const HOOK = "GATE-GUARDIAN";
20
+
21
+ async function main(): Promise<void> {
22
+ const stdin = await readStdin();
23
+ log(HOOK, "PreToolUse advancePhase triggered");
24
+
25
+ const workflowId = getField(stdin, "tool_input.workflowId") || getField(stdin, "workflowId");
26
+ const targetPhase = getField(stdin, "tool_input.targetPhase") || getField(stdin, "targetPhase");
27
+
28
+ log(HOOK, `workflow=${workflowId} targetPhase=${targetPhase}`);
29
+
30
+ // Check workflow mode — quick/interactive skip gate
31
+ if (workflowId) {
32
+ const mode = await getWorkflowMode(workflowId);
33
+ log(HOOK, `workflow=${workflowId} mode=${mode || "legacy"}`);
34
+
35
+ if (mode === "quick") {
36
+ outputPreToolUse({
37
+ decision: "allow",
38
+ context: `Gate to '${targetPhase}' allowed: quick mode skips gate evaluation.`,
39
+ });
40
+ return;
41
+ }
42
+ if (mode === "interactive") {
43
+ outputPreToolUse({
44
+ decision: "allow",
45
+ context: `Gate to '${targetPhase}' allowed: interactive mode skips artifact gate evaluation.`,
46
+ });
47
+ return;
48
+ }
49
+ }
50
+
51
+ // FAIL-CLOSED: can't parse input → DENY
52
+ if (!workflowId || !targetPhase) {
53
+ log(HOOK, "DENY (could not parse input)");
54
+ outputPreToolUse({
55
+ decision: "deny",
56
+ reason: "Gate Guardian: Could not parse workflowId or targetPhase.",
57
+ context: "Ensure you pass both workflowId and targetPhase to advancePhase.",
58
+ });
59
+ return;
60
+ }
61
+
62
+ // Auth check — FAIL-CLOSED
63
+ const token = await getToken();
64
+ if (!token) {
65
+ log(HOOK, "DENY (auth failed)");
66
+ outputPreToolUse({
67
+ decision: "deny",
68
+ reason: "Gate Guardian: Authentication failed.",
69
+ context:
70
+ "Check ORCHESTRATOR_ADMIN_EMAIL, ORCHESTRATOR_ADMIN_PASSWORD, ORCHESTRATOR_PROJECT_ID env vars.",
71
+ });
72
+ return;
73
+ }
74
+
75
+ // IMPLEMENT phase requires approval
76
+ if (targetPhase.toLowerCase() === "implement") {
77
+ log(HOOK, "Checking approval for IMPLEMENT phase");
78
+ 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
+ }
91
+ }
92
+ }
93
+
94
+ // Non-IMPLEMENT or approved: ALLOW
95
+ log(HOOK, `ALLOW (phase=${targetPhase})`);
96
+ outputPreToolUse({
97
+ decision: "allow",
98
+ context: `Gate to '${targetPhase}' allowed.`,
99
+ });
100
+ }
101
+
102
+ main().catch(() => process.exit(0));
@@ -0,0 +1,293 @@
1
+ /**
2
+ * api-client.ts — Shared API client for Orchestrator hooks (RFC-022 Phase 2)
3
+ * Replaces orch-helpers.sh: typed, async/await, zero `node -e` hacks.
4
+ */
5
+
6
+ import { readFileSync, writeFileSync, mkdirSync, statSync } from "node:fs";
7
+ import { join, dirname } from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+
10
+ // Resolve paths relative to this file
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = dirname(__filename);
13
+ const PROJECT_ROOT = join(__dirname, "..", "..", "..");
14
+ const STATE_DIR = join(PROJECT_ROOT, ".orchestrator", ".state");
15
+ const JWT_CACHE = join(STATE_DIR, "jwt-token");
16
+ const DEBUG_LOG = join(STATE_DIR, "hook-debug.log");
17
+
18
+ // Ensure state dir exists
19
+ mkdirSync(STATE_DIR, { recursive: true });
20
+
21
+ // Config from env
22
+ const API_URL = process.env.ORCHESTRATOR_API_URL || "http://localhost:3001";
23
+ const AUTH_EMAIL =
24
+ process.env.ORCHESTRATOR_ADMIN_EMAIL ||
25
+ process.env.ORCHESTRATOR_AUTH_EMAIL ||
26
+ "admin@orchestrator.local";
27
+ const AUTH_PASSWORD =
28
+ process.env.ORCHESTRATOR_ADMIN_PASSWORD ||
29
+ process.env.ORCHESTRATOR_AUTH_PASSWORD ||
30
+ "admin123";
31
+ const PROJECT_ID = process.env.ORCHESTRATOR_PROJECT_ID || "";
32
+
33
+ // --- Types ---
34
+
35
+ export interface WorkflowStatus {
36
+ id: string;
37
+ type: string;
38
+ status: string;
39
+ currentPhase: string;
40
+ mode?: string;
41
+ }
42
+
43
+ export interface PendingAction {
44
+ hasAction: boolean;
45
+ pendingAction?: {
46
+ agent: string;
47
+ status: string;
48
+ prompt?: string;
49
+ };
50
+ }
51
+
52
+ export interface DetectResult {
53
+ workflowType: string;
54
+ confidence: number;
55
+ suggestedMode?: string;
56
+ }
57
+
58
+ // --- Logging ---
59
+
60
+ export function log(prefix: string, msg: string): void {
61
+ const ts = new Date().toISOString().replace(/\.\d+Z$/, "Z");
62
+ try {
63
+ const line = `[${ts}] ${prefix}: ${msg}\n`;
64
+ writeFileSync(DEBUG_LOG, line, { flag: "a" });
65
+ } catch {
66
+ // fail silently
67
+ }
68
+ }
69
+
70
+ // --- Auth ---
71
+
72
+ function getCachedToken(): string | null {
73
+ try {
74
+ const stat = statSync(JWT_CACHE);
75
+ const ageMs = Date.now() - stat.mtimeMs;
76
+ if (ageMs < 3_500_000) {
77
+ // ~58 min
78
+ return readFileSync(JWT_CACHE, "utf-8").trim();
79
+ }
80
+ } catch {
81
+ // no cache
82
+ }
83
+ return null;
84
+ }
85
+
86
+ async function login(): Promise<string | null> {
87
+ if (!PROJECT_ID) return null;
88
+
89
+ try {
90
+ const resp = await fetch(`${API_URL}/api/v1/auth/login`, {
91
+ method: "POST",
92
+ headers: {
93
+ "Content-Type": "application/json",
94
+ "X-Project-ID": PROJECT_ID,
95
+ },
96
+ body: JSON.stringify({ email: AUTH_EMAIL, password: AUTH_PASSWORD }),
97
+ signal: AbortSignal.timeout(5000),
98
+ });
99
+ if (!resp.ok) return null;
100
+
101
+ const data = (await resp.json()) as Record<string, unknown>;
102
+ const token =
103
+ (data.accessToken as string) ||
104
+ ((data.data as Record<string, unknown>)?.accessToken as string) ||
105
+ "";
106
+ if (token) {
107
+ writeFileSync(JWT_CACHE, token);
108
+ }
109
+ return token || null;
110
+ } catch {
111
+ return null;
112
+ }
113
+ }
114
+
115
+ export async function getToken(): Promise<string | null> {
116
+ return getCachedToken() || (await login());
117
+ }
118
+
119
+ // --- API Helpers ---
120
+
121
+ async function apiGet<T>(path: string, token: string, timeoutMs = 5000): Promise<T | null> {
122
+ try {
123
+ const resp = await fetch(`${API_URL}${path}`, {
124
+ headers: {
125
+ Authorization: `Bearer ${token}`,
126
+ "X-Project-ID": PROJECT_ID,
127
+ },
128
+ signal: AbortSignal.timeout(timeoutMs),
129
+ });
130
+ if (!resp.ok) return null;
131
+ return (await resp.json()) as T;
132
+ } catch {
133
+ return null;
134
+ }
135
+ }
136
+
137
+ async function apiPost<T>(
138
+ path: string,
139
+ token: string,
140
+ body: Record<string, unknown>,
141
+ timeoutMs = 5000
142
+ ): Promise<T | null> {
143
+ try {
144
+ const resp = await fetch(`${API_URL}${path}`, {
145
+ method: "POST",
146
+ headers: {
147
+ "Content-Type": "application/json",
148
+ Authorization: `Bearer ${token}`,
149
+ "X-Project-ID": PROJECT_ID,
150
+ },
151
+ body: JSON.stringify(body),
152
+ signal: AbortSignal.timeout(timeoutMs),
153
+ });
154
+ if (!resp.ok) return null;
155
+ return (await resp.json()) as T;
156
+ } catch {
157
+ return null;
158
+ }
159
+ }
160
+
161
+ // --- Workflow Queries ---
162
+
163
+ export async function getActiveWorkflow(): Promise<WorkflowStatus | null> {
164
+ const token = await getToken();
165
+ if (!token) return null;
166
+
167
+ for (const status of ["in_progress", "awaiting_agent", "awaiting_approval"]) {
168
+ const resp = await apiGet<{ data?: WorkflowStatus[] }>(
169
+ `/api/v1/workflows?status=${status}&limit=1`,
170
+ token,
171
+ 3000
172
+ );
173
+ const list = resp?.data || (Array.isArray(resp) ? (resp as WorkflowStatus[]) : null);
174
+ const wf = list?.[0];
175
+ if (wf?.id) return wf;
176
+ }
177
+ return null;
178
+ }
179
+
180
+ export async function getWorkflowStatus(workflowId: string): Promise<WorkflowStatus | null> {
181
+ const token = await getToken();
182
+ if (!token) return null;
183
+ return apiGet(`/api/v1/workflows/${workflowId}/status`, token);
184
+ }
185
+
186
+ export async function getWorkflowMode(workflowId: string): Promise<string | null> {
187
+ const status = await getWorkflowStatus(workflowId);
188
+ return status?.mode || null;
189
+ }
190
+
191
+ export async function getNextAction(workflowId: string): Promise<PendingAction | null> {
192
+ const token = await getToken();
193
+ if (!token) return null;
194
+ return apiGet(`/api/v1/workflows/${workflowId}/pending-action`, token);
195
+ }
196
+
197
+ export async function detectWorkflow(prompt: string): Promise<DetectResult | null> {
198
+ const token = await getToken();
199
+ if (!token) return null;
200
+ return apiPost(`/api/v1/workflows/detect`, token, { prompt }, 3000);
201
+ }
202
+
203
+ // --- Invocation Tracking ---
204
+
205
+ export async function startInvocation(
206
+ agentType: string,
207
+ workflowId: string,
208
+ phase: string
209
+ ): Promise<void> {
210
+ const token = await getToken();
211
+ if (!token) return;
212
+ await apiPost(`/api/v1/invocations/start`, token, {
213
+ agentType,
214
+ workflowId,
215
+ phase,
216
+ startedAt: new Date().toISOString(),
217
+ });
218
+ }
219
+
220
+ export async function registerCheckpoint(
221
+ workflowId: string,
222
+ description: string,
223
+ commitHash: string
224
+ ): Promise<void> {
225
+ const token = await getToken();
226
+ if (!token) return;
227
+ await apiPost(`/api/v1/checkpoints`, token, {
228
+ workflowId,
229
+ description,
230
+ commitHash,
231
+ });
232
+ }
233
+
234
+ // --- Stdin Helper ---
235
+
236
+ export async function readStdin(): Promise<Record<string, unknown>> {
237
+ const chunks: Buffer[] = [];
238
+ for await (const chunk of process.stdin) {
239
+ chunks.push(chunk as Buffer);
240
+ }
241
+ const raw = Buffer.concat(chunks).toString("utf-8").trim();
242
+ if (!raw) return {};
243
+ try {
244
+ return JSON.parse(raw) as Record<string, unknown>;
245
+ } catch {
246
+ return {};
247
+ }
248
+ }
249
+
250
+ // --- JSON Field Access ---
251
+
252
+ export function getField(obj: Record<string, unknown>, path: string): string {
253
+ const parts = path.split(".");
254
+ let current: unknown = obj;
255
+ for (const part of parts) {
256
+ if (current == null || typeof current !== "object") return "";
257
+ current = (current as Record<string, unknown>)[part];
258
+ }
259
+ return current != null ? String(current) : "";
260
+ }
261
+
262
+ // --- Output Helpers ---
263
+
264
+ export function outputPreToolUse(opts: {
265
+ decision: "allow" | "deny";
266
+ reason?: string;
267
+ context?: string;
268
+ }): void {
269
+ const output: Record<string, unknown> = {
270
+ hookSpecificOutput: {
271
+ hookEventName: "PreToolUse",
272
+ permissionDecision: opts.decision,
273
+ ...(opts.reason && { permissionDecisionReason: opts.reason }),
274
+ ...(opts.context && { additionalContext: opts.context }),
275
+ },
276
+ };
277
+ console.log(JSON.stringify(output));
278
+ }
279
+
280
+ export function outputContext(hookEvent: string, context: string): void {
281
+ console.log(
282
+ JSON.stringify({
283
+ hookSpecificOutput: {
284
+ hookEventName: hookEvent,
285
+ additionalContext: context,
286
+ },
287
+ })
288
+ );
289
+ }
290
+
291
+ export function outputBlock(reason: string): void {
292
+ console.log(JSON.stringify({ decision: "block", reason }));
293
+ }