@orchestrator-claude/cli 3.17.1 → 3.19.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 (55) 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 -28
  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 -28
  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/approval-guardian.sh +0 -62
  37. package/dist/templates/base/claude/hooks/dangling-workflow-guard.sh +0 -57
  38. package/dist/templates/base/claude/hooks/gate-guardian.sh +0 -84
  39. package/dist/templates/base/claude/hooks/orch-helpers.sh +0 -135
  40. package/dist/templates/base/claude/hooks/ping-pong-enforcer.sh +0 -58
  41. package/dist/templates/base/claude/hooks/post-phase-checkpoint.sh +0 -203
  42. package/dist/templates/base/claude/hooks/prompt-orchestrator.sh +0 -41
  43. package/dist/templates/base/claude/hooks/session-orchestrator.sh +0 -54
  44. package/dist/templates/base/claude/hooks/track-agent-invocation.sh +0 -230
  45. package/dist/templates/base/claude/hooks/workflow-guard.sh +0 -79
  46. package/templates/base/claude/hooks/approval-guardian.sh +0 -62
  47. package/templates/base/claude/hooks/dangling-workflow-guard.sh +0 -57
  48. package/templates/base/claude/hooks/gate-guardian.sh +0 -84
  49. package/templates/base/claude/hooks/orch-helpers.sh +0 -135
  50. package/templates/base/claude/hooks/ping-pong-enforcer.sh +0 -58
  51. package/templates/base/claude/hooks/post-phase-checkpoint.sh +0 -203
  52. package/templates/base/claude/hooks/prompt-orchestrator.sh +0 -41
  53. package/templates/base/claude/hooks/session-orchestrator.sh +0 -54
  54. package/templates/base/claude/hooks/track-agent-invocation.sh +0 -230
  55. package/templates/base/claude/hooks/workflow-guard.sh +0 -79
@@ -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
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * user-prompt.ts — UserPromptSubmit hook (RFC-022 Phase 2)
3
+ * Replaces prompt-orchestrator.sh
4
+ * Detects workflow type and injects hints by confidence tiers.
5
+ */
6
+
7
+ import {
8
+ log,
9
+ readStdin,
10
+ getField,
11
+ getActiveWorkflow,
12
+ getNextAction,
13
+ detectWorkflow,
14
+ outputContext,
15
+ } from "./lib/api-client.js";
16
+
17
+ const HOOK = "USER-PROMPT";
18
+
19
+ async function main(): Promise<void> {
20
+ log(HOOK, "UserPromptSubmit triggered");
21
+
22
+ const stdin = await readStdin();
23
+ const workflow = await getActiveWorkflow();
24
+
25
+ if (!workflow) {
26
+ // No active workflow — detect and suggest
27
+ const prompt = getField(stdin, "prompt");
28
+ if (!prompt) {
29
+ log(HOOK, "empty prompt, silence");
30
+ return;
31
+ }
32
+
33
+ const detection = await detectWorkflow(prompt);
34
+ if (!detection) {
35
+ log(HOOK, "detect API failed/timeout, silence (fail-open)");
36
+ return;
37
+ }
38
+
39
+ const { workflowType, confidence, suggestedMode } = detection;
40
+ log(HOOK, `detected type=${workflowType} confidence=${confidence} mode=${suggestedMode}`);
41
+
42
+ // Tier 1: interactive_analysis
43
+ if (workflowType === "interactive_analysis") {
44
+ outputContext(
45
+ "UserPromptSubmit",
46
+ [
47
+ "[ORCHESTRATOR] This looks like an analytical/design request. How would you like to proceed?",
48
+ '(a) Continue as a free conversation (no workflow)',
49
+ '(b) Start an interactive_analysis workflow for structured exploration (cyclic phases: research → discuss → analyze)',
50
+ "",
51
+ 'If you choose (b), I will call: mcp__orchestrator-tools__startWorkflow({ type: "interactive_analysis", prompt: "..." })',
52
+ ].join("\n")
53
+ );
54
+ return;
55
+ }
56
+
57
+ // Tier 2: high confidence (>= 0.7)
58
+ if (confidence >= 0.7) {
59
+ const mode = suggestedMode || "standard";
60
+ outputContext(
61
+ "UserPromptSubmit",
62
+ `[ORCHESTRATOR] Detected: ${workflowType} workflow (confidence: ${confidence}, suggested mode: ${mode}).\nTo start: mcp__orchestrator-tools__startWorkflow({ type: "${workflowType}", prompt: "...", mode: "${mode}" })\nOr continue without a workflow if this is exploratory.`
63
+ );
64
+ return;
65
+ }
66
+
67
+ // Tier 3: medium confidence (0.5-0.69)
68
+ if (confidence >= 0.5) {
69
+ outputContext(
70
+ "UserPromptSubmit",
71
+ `[ORCHESTRATOR] Hint: this might be a ${workflowType} task. Consider starting a workflow if you plan to write code.`
72
+ );
73
+ return;
74
+ }
75
+
76
+ // Tier 4: low confidence — silence
77
+ log(HOOK, `Tier 4 silence (confidence=${confidence} < 0.5)`);
78
+ return;
79
+ }
80
+
81
+ // Active workflow — inject state
82
+ const action = await getNextAction(workflow.id);
83
+ let context: string;
84
+
85
+ if (action?.hasAction && action.pendingAction) {
86
+ context = `[ORCHESTRATOR] Active workflow: ${workflow.id}. Next: invoke '${action.pendingAction.agent}' (${action.pendingAction.status}).`;
87
+ } else {
88
+ context = `[ORCHESTRATOR] Active workflow: ${workflow.id}. No pending actions.`;
89
+ }
90
+
91
+ log(HOOK, `workflow=${workflow.id} hasAction=${action?.hasAction}`);
92
+ outputContext("UserPromptSubmit", context);
93
+ }
94
+
95
+ main().catch(() => process.exit(0));
@@ -0,0 +1,120 @@
1
+ /**
2
+ * workflow-guard.ts — PreToolUse[Write|Edit] hook (RFC-022 Phase 2)
3
+ * Replaces workflow-guard.sh
4
+ * Blocks writes to src/ and tests/ without active workflow.
5
+ * Respects SKIP_WORKFLOW_GUARD, quick/interactive modes, sub-agent writes.
6
+ */
7
+
8
+ import {
9
+ log,
10
+ readStdin,
11
+ getField,
12
+ getActiveWorkflow,
13
+ getWorkflowMode,
14
+ outputPreToolUse,
15
+ } from "./lib/api-client.js";
16
+
17
+ const HOOK = "WORKFLOW-GUARD";
18
+
19
+ async function main(): Promise<void> {
20
+ const stdin = await readStdin();
21
+ const filePath = getField(stdin, "tool_input.file_path") || getField(stdin, "file_path") || getField(stdin, "input.file_path");
22
+
23
+ log(HOOK, `file_path=${filePath}`);
24
+
25
+ // Explicit bypass
26
+ if (process.env.SKIP_WORKFLOW_GUARD === "true") {
27
+ log(HOOK, "ALLOW (SKIP_WORKFLOW_GUARD=true)");
28
+ outputPreToolUse({
29
+ decision: "allow",
30
+ context: "Workflow guard bypassed via SKIP_WORKFLOW_GUARD=true.",
31
+ });
32
+ return;
33
+ }
34
+
35
+ // Only guard src/ and tests/ paths
36
+ if (!filePath.includes("/src/") && !filePath.includes("/tests/")) {
37
+ log(HOOK, "ALLOW (non-guarded path)");
38
+ return;
39
+ }
40
+
41
+ // Allow config/doc files
42
+ if (/\.(json|ya?ml|md|env(\..*)?)$/.test(filePath)) {
43
+ log(HOOK, "ALLOW (config/doc file)");
44
+ return;
45
+ }
46
+
47
+ // Check for active workflow
48
+ const workflow = await getActiveWorkflow();
49
+
50
+ if (workflow) {
51
+ const mode = await getWorkflowMode(workflow.id);
52
+ log(HOOK, `workflow=${workflow.id} mode=${mode || "legacy"}`);
53
+
54
+ if (mode === "quick") {
55
+ outputPreToolUse({
56
+ decision: "allow",
57
+ context: `Active workflow: ${workflow.id} (mode: quick). Direct writes allowed in quick mode.`,
58
+ });
59
+ return;
60
+ }
61
+
62
+ if (mode === "interactive") {
63
+ outputPreToolUse({
64
+ decision: "allow",
65
+ context: `Active workflow: ${workflow.id} (mode: interactive). Note: code writes are unexpected in interactive/analysis workflows.`,
66
+ });
67
+ return;
68
+ }
69
+
70
+ // Check if running inside a sub-agent
71
+ const agentId = getField(stdin, "agent_id");
72
+ if (agentId) {
73
+ const agentType = getField(stdin, "agent_type");
74
+ log(HOOK, `ALLOW (sub-agent: ${agentType || "unknown"}, workflow: ${workflow.id})`);
75
+ outputPreToolUse({
76
+ decision: "allow",
77
+ context: `Active workflow: ${workflow.id}. Sub-agent ${agentType || "unknown"} write allowed.`,
78
+ });
79
+ return;
80
+ }
81
+
82
+ // Main agent writing directly — check SKIP_SUBAGENT_GUARD
83
+ if (process.env.SKIP_SUBAGENT_GUARD === "true") {
84
+ log(HOOK, `ALLOW (SKIP_SUBAGENT_GUARD=true, workflow: ${workflow.id})`);
85
+ outputPreToolUse({
86
+ decision: "allow",
87
+ context: `Active workflow: ${workflow.id}. Direct write allowed via SKIP_SUBAGENT_GUARD.`,
88
+ });
89
+ return;
90
+ }
91
+
92
+ // Main agent direct write — DENY
93
+ log(HOOK, `DENY (main agent direct write, mode=${mode || "legacy"})`);
94
+ outputPreToolUse({
95
+ decision: "deny",
96
+ reason:
97
+ "Workflow Guard: Direct code writes are blocked. You must invoke a sub-agent (e.g. implementer) to write code. The sub-agent will have write access.",
98
+ context:
99
+ "Use the Agent tool to spawn an implementer sub-agent for code changes. The workflow-guard allows writes only from sub-agents (identified by agent_id in hook input).",
100
+ });
101
+ return;
102
+ }
103
+
104
+ // No active workflow — DENY
105
+ log(HOOK, "DENY (no active workflow)");
106
+ outputPreToolUse({
107
+ decision: "deny",
108
+ reason: "Workflow Guard: No active workflow. Direct implementation is not allowed.",
109
+ context: [
110
+ "You MUST start a workflow before writing code:",
111
+ '1. mcp__orchestrator-tools__detectWorkflow({ prompt: "..." })',
112
+ '2. mcp__orchestrator-tools__startWorkflow({ workflowType: "...", prompt: "..." })',
113
+ "3. Follow the nextStep returned by startWorkflow",
114
+ "",
115
+ "The workflow-guard hook blocks all writes to src/ and tests/ without an active workflow.",
116
+ ].join("\n"),
117
+ });
118
+ }
119
+
120
+ main().catch(() => process.exit(0));