@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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* git-checkpoint.ts — Git checkpoint logic for Orchestrator hooks (RFC-022 Phase 2)
|
|
3
|
+
* Replaces post-phase-checkpoint.sh git logic.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { execSync } from "node:child_process";
|
|
7
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { join, dirname } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { log, registerCheckpoint } from "./api-client.js";
|
|
11
|
+
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = dirname(__filename);
|
|
14
|
+
const PROJECT_ROOT = join(__dirname, "..", "..", "..");
|
|
15
|
+
const STATE_DIR = join(PROJECT_ROOT, ".orchestrator", ".state");
|
|
16
|
+
const LAST_PHASE_FILE = join(STATE_DIR, "last-checkpointed-phase");
|
|
17
|
+
|
|
18
|
+
const PHASE_ORDER = ["research", "specify", "plan", "tasks", "implement", "validate"];
|
|
19
|
+
|
|
20
|
+
/** Map from current phase to the phase that just completed */
|
|
21
|
+
function getCompletedPhase(currentPhase: string): string | null {
|
|
22
|
+
const lower = currentPhase.toLowerCase();
|
|
23
|
+
const idx = PHASE_ORDER.indexOf(lower);
|
|
24
|
+
return idx > 0 ? PHASE_ORDER[idx - 1] : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function exec(cmd: string): string {
|
|
28
|
+
return execSync(cmd, { cwd: PROJECT_ROOT, encoding: "utf-8" }).trim();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Create a git checkpoint if the phase changed and there are staged changes.
|
|
33
|
+
* Returns the commit hash or null.
|
|
34
|
+
*/
|
|
35
|
+
export async function createCheckpointIfNeeded(
|
|
36
|
+
workflowId: string,
|
|
37
|
+
currentPhase: string
|
|
38
|
+
): Promise<string | null> {
|
|
39
|
+
const completedPhase = getCompletedPhase(currentPhase);
|
|
40
|
+
if (!completedPhase) {
|
|
41
|
+
log("CHECKPOINT", `No completed phase from: ${currentPhase}`);
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Check if already checkpointed
|
|
46
|
+
let lastPhase = "";
|
|
47
|
+
try {
|
|
48
|
+
lastPhase = readFileSync(LAST_PHASE_FILE, "utf-8").trim();
|
|
49
|
+
} catch {
|
|
50
|
+
// no file
|
|
51
|
+
}
|
|
52
|
+
if (lastPhase === completedPhase) {
|
|
53
|
+
log("CHECKPOINT", `Already checkpointed: ${completedPhase}`);
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
// Stage all changes
|
|
59
|
+
exec("git add -A");
|
|
60
|
+
|
|
61
|
+
// Check if there are staged changes
|
|
62
|
+
try {
|
|
63
|
+
exec("git diff --cached --quiet");
|
|
64
|
+
// No changes
|
|
65
|
+
log("CHECKPOINT", `No changes for phase: ${completedPhase}`);
|
|
66
|
+
writeFileSync(LAST_PHASE_FILE, completedPhase);
|
|
67
|
+
return null;
|
|
68
|
+
} catch {
|
|
69
|
+
// Has changes — good, proceed to commit
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const commitMsg = `[orchestrator] ${completedPhase}: Auto-checkpoint after ${completedPhase} phase`;
|
|
73
|
+
exec(`git commit -m "${commitMsg}" --no-verify`);
|
|
74
|
+
const commitHash = exec("git rev-parse HEAD");
|
|
75
|
+
|
|
76
|
+
log("CHECKPOINT", `Created: phase=${completedPhase} commit=${commitHash}`);
|
|
77
|
+
|
|
78
|
+
// Register via API
|
|
79
|
+
await registerCheckpoint(
|
|
80
|
+
workflowId,
|
|
81
|
+
`Auto-checkpoint after ${completedPhase} phase`,
|
|
82
|
+
commitHash
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
writeFileSync(LAST_PHASE_FILE, completedPhase);
|
|
86
|
+
return commitHash;
|
|
87
|
+
} catch (err) {
|
|
88
|
+
log("CHECKPOINT", `Error: ${err}`);
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@orchestrator/hooks",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Orchestrator TypeScript hooks (RFC-022 Phase 2)",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=22.0.0"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"tsx": "^4.21.0"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* post-compact.ts — PostCompact hook (RFC-022 Phase 2)
|
|
3
|
+
* NEW hook: re-injects workflow state after context compaction
|
|
4
|
+
* so Claude doesn't lose track of active workflow.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
log,
|
|
9
|
+
getActiveWorkflow,
|
|
10
|
+
getNextAction,
|
|
11
|
+
outputContext,
|
|
12
|
+
} from "./lib/api-client.js";
|
|
13
|
+
|
|
14
|
+
const HOOK = "POST-COMPACT";
|
|
15
|
+
|
|
16
|
+
async function main(): Promise<void> {
|
|
17
|
+
log(HOOK, "PostCompact hook triggered");
|
|
18
|
+
|
|
19
|
+
const workflow = await getActiveWorkflow();
|
|
20
|
+
if (!workflow) {
|
|
21
|
+
log(HOOK, "No active workflow, nothing to re-inject");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const parts: string[] = [
|
|
26
|
+
`[ORCHESTRATOR] Context was compacted. Active workflow state:`,
|
|
27
|
+
`Workflow: ${workflow.id}`,
|
|
28
|
+
`Type: ${workflow.type}`,
|
|
29
|
+
`Phase: ${workflow.currentPhase}`,
|
|
30
|
+
`Status: ${workflow.status}`,
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const action = await getNextAction(workflow.id);
|
|
34
|
+
if (action?.hasAction && action.pendingAction) {
|
|
35
|
+
parts.push(`Next: invoke '${action.pendingAction.agent}' (${action.pendingAction.status})`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
parts.push("", "Remember: YOU are the orchestrator (RFC-022). Dispatch sub-agents directly via Agent tool.");
|
|
39
|
+
|
|
40
|
+
log(HOOK, `Re-injecting workflow state (wf=${workflow.id})`);
|
|
41
|
+
outputContext("PostCompact", parts.join("\n"));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
main().catch(() => process.exit(0));
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-start.ts — SessionStart hook (RFC-022 Phase 2)
|
|
3
|
+
* Replaces session-orchestrator.sh
|
|
4
|
+
* Injects workflow status context on session start.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { join, dirname } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import {
|
|
11
|
+
log,
|
|
12
|
+
getActiveWorkflow,
|
|
13
|
+
getNextAction,
|
|
14
|
+
outputContext,
|
|
15
|
+
} from "./lib/api-client.js";
|
|
16
|
+
|
|
17
|
+
const HOOK = "SESSION-START";
|
|
18
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
19
|
+
const __dirname = dirname(__filename);
|
|
20
|
+
const PROJECT_ROOT = join(__dirname, "..", "..");
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Read BACKLOG.md to extract version and next work item.
|
|
24
|
+
* Returns structured summary for the greeting.
|
|
25
|
+
*/
|
|
26
|
+
function getBacklogSummary(): { version: string; next: string; status: string } {
|
|
27
|
+
try {
|
|
28
|
+
const backlogPath = join(PROJECT_ROOT, "project-guidelines", "BACKLOG.md");
|
|
29
|
+
const content = readFileSync(backlogPath, "utf-8");
|
|
30
|
+
const lines = content.split("\n").slice(0, 40);
|
|
31
|
+
|
|
32
|
+
// Extract version from "**Release Atual:** vX.Y.Z" or "Current Release: vX.Y.Z"
|
|
33
|
+
const versionLine = lines.find((l) => /release atual|current release/i.test(l));
|
|
34
|
+
const versionMatch = versionLine?.match(/v(\d+\.\d+\.\d+)/);
|
|
35
|
+
const version = versionMatch ? `v${versionMatch[1]}` : "v?.?.?";
|
|
36
|
+
|
|
37
|
+
// Extract pending status from "**Pendente:**" line (colon may be inside or outside bold)
|
|
38
|
+
const pendenteLine = lines.find((l) => /\*\*pendente/i.test(l));
|
|
39
|
+
const status = pendenteLine
|
|
40
|
+
?.replace(/.*\*\*pendente:?\*\*:?\s*/i, "")
|
|
41
|
+
.replace(/\s*$/, "")
|
|
42
|
+
.slice(0, 80) || "";
|
|
43
|
+
|
|
44
|
+
// Extract next step from "**Proximo passo:**" or "**Next:**" line
|
|
45
|
+
const nextLine = lines.find((l) => /\*\*proximo passo/i.test(l) || /\*\*next/i.test(l));
|
|
46
|
+
const next = nextLine
|
|
47
|
+
?.replace(/.*\*\*(proximo passo|next):?\*\*:?\s*/i, "")
|
|
48
|
+
.replace(/\s*$/, "")
|
|
49
|
+
.slice(0, 120) || "see BACKLOG.md";
|
|
50
|
+
|
|
51
|
+
log(HOOK, `BACKLOG parsed: version=${version} next=${next.slice(0, 50)}`);
|
|
52
|
+
return { version, next, status };
|
|
53
|
+
} catch (err) {
|
|
54
|
+
log(HOOK, `BACKLOG read failed: ${err}`);
|
|
55
|
+
return { version: "v?.?.?", next: "see BACKLOG.md", status: "" };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function main(): Promise<void> {
|
|
60
|
+
log(HOOK, "SessionStart hook triggered");
|
|
61
|
+
|
|
62
|
+
const backlog = getBacklogSummary();
|
|
63
|
+
const workflow = await getActiveWorkflow();
|
|
64
|
+
|
|
65
|
+
if (!workflow) {
|
|
66
|
+
log(HOOK, "No active workflow, injecting greeting data");
|
|
67
|
+
outputContext(
|
|
68
|
+
"SessionStart",
|
|
69
|
+
[
|
|
70
|
+
`[ORCHESTRATOR-DATA] version=${backlog.version} | workflow=none | next=${backlog.next}`,
|
|
71
|
+
`[ORCHESTRATOR] Greet the user via AskUserQuestion using the data above (CLAUDE.md Rule #1).`,
|
|
72
|
+
"Do NOT invoke /orchestrator skill for the greeting — use AskUserQuestion directly.",
|
|
73
|
+
"Invoke /orchestrator skill only when the user chooses to start a workflow.",
|
|
74
|
+
].join("\n")
|
|
75
|
+
);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Active workflow — inject state + backlog
|
|
80
|
+
const action = await getNextAction(workflow.id);
|
|
81
|
+
const actionHint = action?.hasAction && action.pendingAction
|
|
82
|
+
? ` | pending=${action.pendingAction.agent} (${action.pendingAction.status})`
|
|
83
|
+
: "";
|
|
84
|
+
|
|
85
|
+
log(HOOK, `Injecting workflow context (wf=${workflow.id} phase=${workflow.currentPhase})`);
|
|
86
|
+
outputContext(
|
|
87
|
+
"SessionStart",
|
|
88
|
+
[
|
|
89
|
+
`[ORCHESTRATOR-DATA] version=${backlog.version} | workflow=${workflow.id} | type=${workflow.type} | phase=${workflow.currentPhase} | status=${workflow.status}${actionHint}`,
|
|
90
|
+
`[ORCHESTRATOR] Active workflow detected. Greet the user via AskUserQuestion showing workflow state (CLAUDE.md Rule #1).`,
|
|
91
|
+
"Do NOT invoke /orchestrator skill for the greeting — use AskUserQuestion directly.",
|
|
92
|
+
"Invoke /orchestrator skill when the user chooses to continue the workflow.",
|
|
93
|
+
].join("\n")
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
main().catch(() => process.exit(0));
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* subagent-start.ts — SubagentStart hook (RFC-022 Phase 2)
|
|
3
|
+
* Replaces track-agent-invocation.sh start
|
|
4
|
+
* Registers agent invocation via REST API.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
log,
|
|
9
|
+
readStdin,
|
|
10
|
+
getField,
|
|
11
|
+
getActiveWorkflow,
|
|
12
|
+
startInvocation,
|
|
13
|
+
} from "./lib/api-client.js";
|
|
14
|
+
|
|
15
|
+
const HOOK = "SUBAGENT-START";
|
|
16
|
+
|
|
17
|
+
const PHASE_MAP: Record<string, string> = {
|
|
18
|
+
specifier: "specify",
|
|
19
|
+
planner: "plan",
|
|
20
|
+
"task-generator": "tasks",
|
|
21
|
+
implementer: "implement",
|
|
22
|
+
researcher: "research",
|
|
23
|
+
reviewer: "review",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
async function main(): Promise<void> {
|
|
27
|
+
const stdin = await readStdin();
|
|
28
|
+
log(HOOK, `SubagentStart triggered`);
|
|
29
|
+
|
|
30
|
+
const agentType =
|
|
31
|
+
getField(stdin, "tool_input.subagent_type") ||
|
|
32
|
+
getField(stdin, "subagent_type") ||
|
|
33
|
+
getField(stdin, "tool_input.type") ||
|
|
34
|
+
"unknown";
|
|
35
|
+
|
|
36
|
+
const phase = PHASE_MAP[agentType] || agentType;
|
|
37
|
+
|
|
38
|
+
log(HOOK, `agent=${agentType} phase=${phase}`);
|
|
39
|
+
|
|
40
|
+
const workflow = await getActiveWorkflow();
|
|
41
|
+
if (!workflow) {
|
|
42
|
+
log(HOOK, "No active workflow, skipping");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
await startInvocation(agentType, workflow.id, phase);
|
|
47
|
+
log(HOOK, `START: agent=${agentType} phase=${phase} workflow=${workflow.id}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
main().catch(() => process.exit(0));
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* subagent-stop.ts — SubagentStop unified handler (RFC-022 Phase 2)
|
|
3
|
+
* Replaces 3 chained bash hooks:
|
|
4
|
+
* 1. track-agent-invocation.sh complete
|
|
5
|
+
* 2. ping-pong-enforcer.sh (ELIMINATED — main conversation is orchestrator)
|
|
6
|
+
* 3. post-phase-checkpoint.sh
|
|
7
|
+
*
|
|
8
|
+
* Single handler, single auth, sequential logic.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
log,
|
|
13
|
+
readStdin,
|
|
14
|
+
getField,
|
|
15
|
+
getActiveWorkflow,
|
|
16
|
+
outputContext,
|
|
17
|
+
} from "./lib/api-client.js";
|
|
18
|
+
import { createCheckpointIfNeeded } from "./lib/git-checkpoint.js";
|
|
19
|
+
|
|
20
|
+
const HOOK = "SUBAGENT-STOP";
|
|
21
|
+
|
|
22
|
+
async function main(): Promise<void> {
|
|
23
|
+
const stdin = await readStdin();
|
|
24
|
+
log(HOOK, "SubagentStop triggered");
|
|
25
|
+
|
|
26
|
+
const agentType =
|
|
27
|
+
getField(stdin, "agent_type") ||
|
|
28
|
+
getField(stdin, "tool_input.subagent_type") ||
|
|
29
|
+
"unknown";
|
|
30
|
+
|
|
31
|
+
const workflow = await getActiveWorkflow();
|
|
32
|
+
if (!workflow) {
|
|
33
|
+
log(HOOK, "No active workflow, skipping");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
log(HOOK, `agent=${agentType} workflow=${workflow.id} phase=${workflow.currentPhase}`);
|
|
38
|
+
|
|
39
|
+
// 1. Invocation tracking — agent self-reports via MCP; this is safety net
|
|
40
|
+
log(HOOK, `COMPLETE: agent=${agentType} (safety net — agent self-reports via MCP)`);
|
|
41
|
+
|
|
42
|
+
// 2. Git checkpoint if phase changed
|
|
43
|
+
const commitHash = await createCheckpointIfNeeded(workflow.id, workflow.currentPhase);
|
|
44
|
+
|
|
45
|
+
// 3. Guide main conversation — NO ping-pong, main IS the orchestrator
|
|
46
|
+
const parts: string[] = [`[ORCHESTRATOR] Agent '${agentType}' completed for workflow ${workflow.id}.`];
|
|
47
|
+
|
|
48
|
+
if (commitHash) {
|
|
49
|
+
parts.push(`Checkpoint: ${commitHash}.`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
parts.push("Proceed to gate check via AskUserQuestion before advancing phase.");
|
|
53
|
+
|
|
54
|
+
outputContext("SubagentStop", parts.join(" "));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
main().catch(() => process.exit(0));
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"outDir": "dist",
|
|
10
|
+
"rootDir": ".",
|
|
11
|
+
"declaration": false,
|
|
12
|
+
"sourceMap": false,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"isolatedModules": true,
|
|
15
|
+
"noEmit": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["*.ts", "lib/*.ts"]
|
|
18
|
+
}
|