@orchestrator-claude/cli 3.17.1 → 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 -28
- 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 -28
- 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/approval-guardian.sh +0 -62
- 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/approval-guardian.sh +0 -62
- 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,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));
|