@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.
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/templates/base/CLAUDE.md.hbs +45 -27
- package/dist/templates/base/claude/agents/orchestrator.md +84 -117
- package/dist/templates/base/claude/hooks/dangling-guard.ts +53 -0
- package/dist/templates/base/claude/hooks/gate-guardian.ts +102 -0
- package/dist/templates/base/claude/hooks/lib/api-client.ts +293 -0
- package/dist/templates/base/claude/hooks/lib/git-checkpoint.ts +91 -0
- package/dist/templates/base/claude/hooks/package.json +13 -0
- package/dist/templates/base/claude/hooks/post-compact.ts +44 -0
- package/dist/templates/base/claude/hooks/session-start.ts +97 -0
- package/dist/templates/base/claude/hooks/subagent-start.ts +50 -0
- package/dist/templates/base/claude/hooks/subagent-stop.ts +57 -0
- package/dist/templates/base/claude/hooks/tsconfig.json +18 -0
- package/dist/templates/base/claude/hooks/user-prompt.ts +95 -0
- package/dist/templates/base/claude/hooks/workflow-guard.ts +120 -0
- package/dist/templates/base/claude/settings.json +23 -22
- package/dist/templates/base/claude/skills/orchestrator/SKILL.md +108 -0
- package/package.json +1 -1
- package/templates/base/CLAUDE.md.hbs +45 -27
- package/templates/base/claude/agents/orchestrator.md +84 -117
- package/templates/base/claude/hooks/dangling-guard.ts +53 -0
- package/templates/base/claude/hooks/gate-guardian.ts +102 -0
- package/templates/base/claude/hooks/lib/api-client.ts +293 -0
- package/templates/base/claude/hooks/lib/git-checkpoint.ts +91 -0
- package/templates/base/claude/hooks/package.json +13 -0
- package/templates/base/claude/hooks/post-compact.ts +44 -0
- package/templates/base/claude/hooks/session-start.ts +97 -0
- package/templates/base/claude/hooks/subagent-start.ts +50 -0
- package/templates/base/claude/hooks/subagent-stop.ts +57 -0
- package/templates/base/claude/hooks/tsconfig.json +18 -0
- package/templates/base/claude/hooks/user-prompt.ts +95 -0
- package/templates/base/claude/hooks/workflow-guard.ts +120 -0
- package/templates/base/claude/settings.json +23 -22
- package/templates/base/claude/skills/orchestrator/SKILL.md +108 -0
- package/dist/templates/base/claude/hooks/dangling-workflow-guard.sh +0 -57
- package/dist/templates/base/claude/hooks/gate-guardian.sh +0 -84
- package/dist/templates/base/claude/hooks/orch-helpers.sh +0 -135
- package/dist/templates/base/claude/hooks/ping-pong-enforcer.sh +0 -58
- package/dist/templates/base/claude/hooks/post-phase-checkpoint.sh +0 -203
- package/dist/templates/base/claude/hooks/prompt-orchestrator.sh +0 -41
- package/dist/templates/base/claude/hooks/session-orchestrator.sh +0 -54
- package/dist/templates/base/claude/hooks/track-agent-invocation.sh +0 -230
- package/dist/templates/base/claude/hooks/workflow-guard.sh +0 -79
- package/templates/base/claude/hooks/dangling-workflow-guard.sh +0 -57
- package/templates/base/claude/hooks/gate-guardian.sh +0 -84
- package/templates/base/claude/hooks/orch-helpers.sh +0 -135
- package/templates/base/claude/hooks/ping-pong-enforcer.sh +0 -58
- package/templates/base/claude/hooks/post-phase-checkpoint.sh +0 -203
- package/templates/base/claude/hooks/prompt-orchestrator.sh +0 -41
- package/templates/base/claude/hooks/session-orchestrator.sh +0 -54
- package/templates/base/claude/hooks/track-agent-invocation.sh +0 -230
- package/templates/base/claude/hooks/workflow-guard.sh +0 -79
|
@@ -1,155 +1,122 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: orchestrator
|
|
3
|
-
description:
|
|
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
|
-
|
|
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
|
|
18
|
+
# Orchestrator — Main Conversation Agent (v3.0)
|
|
11
19
|
|
|
12
|
-
##
|
|
20
|
+
## Identity
|
|
13
21
|
|
|
14
|
-
|
|
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
|
-
##
|
|
24
|
+
## Session Start Protocol
|
|
22
25
|
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
32
|
+
This is non-negotiable. The `/orchestrator` skill is your entry point.
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
## Workflow Coordination
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
### Starting a Workflow
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
43
|
+
### Phase Dispatch Loop
|
|
74
44
|
|
|
75
|
-
|
|
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
|
-
|
|
47
|
+
1. **Dispatch** the phase sub-agent via `Agent(subagent_type: "{agent}")`:
|
|
82
48
|
|
|
83
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
94
|
-
- Auto-checkpoints after artifact approval
|
|
95
|
-
- Agent invocation metrics
|
|
96
|
-
- Gate evaluation hooks
|
|
97
|
-
- Recovery points
|
|
62
|
+
### Gate Rules
|
|
98
63
|
|
|
99
|
-
|
|
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
|
-
##
|
|
68
|
+
## Sub-Agent Dispatch
|
|
102
69
|
|
|
103
|
-
|
|
70
|
+
When dispatching a phase agent, include:
|
|
104
71
|
|
|
105
72
|
```
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
111
|
-
|
|
78
|
+
Store your output via mcp__orchestrator-extended__artifactStore.
|
|
79
|
+
")
|
|
112
80
|
```
|
|
113
81
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
## Example Flow
|
|
82
|
+
## Available Agents
|
|
117
83
|
|
|
118
|
-
|
|
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
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
-
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
##
|
|
112
|
+
## Token Efficiency
|
|
142
113
|
|
|
143
|
-
-
|
|
144
|
-
-
|
|
145
|
-
- `
|
|
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:**
|
|
153
|
-
**
|
|
154
|
-
**
|
|
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
|
+
}
|