@orchestrator-claude/definitions 3.33.0 → 3.35.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/README.md +32 -0
- package/agents/adversarial-reviewer.md +2 -2
- package/agents/azure-devops-engineer.md +9 -1
- package/agents/cicd-platform-engineer.md +477 -0
- package/agents/ciclo-engineer.md +267 -0
- package/agents/debug-sidecar.md +5 -0
- package/agents/doc-sidecar.md +5 -0
- package/agents/frontend-engineer.md +173 -0
- package/agents/implementer.md +39 -23
- package/agents/methodology-researcher.md +2 -1
- package/agents/operational-artifact-builder.md +2 -1
- package/agents/orchestrator.md +54 -72
- package/agents/planner.md +2 -2
- package/agents/presentation-engineer.md +1 -1
- package/agents/python-data-engineer.md +214 -0
- package/agents/reviewer.md +16 -8
- package/agents/specifier.md +1 -1
- package/agents/task-generator.md +1 -1
- package/agents/test-sidecar.md +14 -3
- package/dist/install/DevCheckoutDetector.d.ts +30 -13
- package/dist/install/DevCheckoutDetector.d.ts.map +1 -1
- package/dist/install/DevCheckoutDetector.js +33 -16
- package/dist/install/DevCheckoutDetector.js.map +1 -1
- package/hooks/agent-git-guard.mjs +118 -0
- package/hooks/agent-git-guard.ts +166 -0
- package/hooks/hooks.json +78 -0
- package/hooks/playwright-output-guard.mjs +136 -0
- package/hooks/playwright-output-guard.ts +130 -0
- package/hooks/pre-agent-dispatch.mjs +346 -0
- package/hooks/pre-agent-dispatch.ts +287 -0
- package/hooks/session-start.mjs +8 -36
- package/hooks/session-start.ts +15 -72
- package/hooks/subagent-stop.mjs +0 -18
- package/hooks/subagent-stop.ts +29 -13
- package/hooks/verification-before-completion.mjs +105 -0
- package/hooks/verification-before-completion.ts +97 -0
- package/hooks-settings-template.json +19 -4
- package/manifest.json +125 -29
- package/package.json +1 -1
- package/personas/INDEX.md +46 -0
- package/skills/artifact-generator/SKILL.md +1 -1
- package/skills/artifact-production/SKILL.md +28 -0
- package/skills/backlog-synthesis/SKILL.md +8 -3
- package/skills/checkpoint-protocol/SKILL.md +7 -0
- package/skills/ciclo-lane-dispatch/SKILL.md +158 -0
- package/skills/infographic-engine/SKILL.md +116 -0
- package/skills/orchestrator/SKILL.md +95 -7
- package/skills/persona-library/SKILL.md +1 -1
- package/skills/project-conventions/SKILL.md +53 -30
- package/skills/project-management-enterprise/SKILL.md +1 -1
- package/skills/python-data-engineering/SKILL.md +792 -0
- package/skills/release/SKILL.md +29 -18
- package/skills/release/release.sh +65 -4
- package/skills/storybook-validated-loop/SKILL.md +110 -0
- package/skills/tdd-discipline/SKILL.md +16 -0
- package/skills/workflow-coordination/SKILL.md +117 -16
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* playwright-output-guard.ts — PreToolUse[browser_take_screenshot] hook (S269 item g)
|
|
3
|
+
*
|
|
4
|
+
* Problema: @playwright/mcp resolve `filename` contra o WORKSPACE ROOT
|
|
5
|
+
* (workspaceFile em packages/playwright-core/src/tools/backend/context.ts),
|
|
6
|
+
* não contra o outputDir. Filenames sem diretório caem na raiz do repo e o
|
|
7
|
+
* auto-checkpoint (`git add -A`) varre os PNGs para dentro de commits —
|
|
8
|
+
* recorrência observada em S266/S267/S268.
|
|
9
|
+
*
|
|
10
|
+
* Fix em camadas:
|
|
11
|
+
* 1. Este hook NEGA filenames que resolveriam para a raiz do projeto,
|
|
12
|
+
* instruindo o agente a usar `.playwright-mcp/` (gitignored) ou
|
|
13
|
+
* `.cobaia-evidence/<sessão>/` (evidência versionável).
|
|
14
|
+
* 2. `.gitignore` com `/*.png` etc. como cinto-e-suspensório.
|
|
15
|
+
*
|
|
16
|
+
* Política:
|
|
17
|
+
* - sem filename → ALLOW (default = <cwd>/.playwright-mcp/, gitignored)
|
|
18
|
+
* - filename em subdiretório → ALLOW
|
|
19
|
+
* - filename bare / ./x.png / absoluto-na-raiz / traversal → DENY
|
|
20
|
+
* - stdin não-parseável → ALLOW (fail-open: nunca bloquear por infra)
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { dirname, isAbsolute, normalize, resolve } from 'node:path';
|
|
24
|
+
import {
|
|
25
|
+
log,
|
|
26
|
+
readStdin,
|
|
27
|
+
getField,
|
|
28
|
+
outputPreToolUse,
|
|
29
|
+
resolveHookProjectRoot,
|
|
30
|
+
} from './lib/api-client.js';
|
|
31
|
+
|
|
32
|
+
const HOOK = 'PLAYWRIGHT-OUTPUT-GUARD';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Raiz do projeto contra a qual `filename` é resolvido.
|
|
36
|
+
*
|
|
37
|
+
* ⚠️ NÃO derive de `__filename` (era o bug, S396/RFC-040 §18.12): sob o modelo
|
|
38
|
+
* de distribuição por plugin (§18) este arquivo roda a partir de
|
|
39
|
+
* `~/.claude/plugins/cache/<marketplace>/orchestrator/<versão>/hooks/`, então
|
|
40
|
+
* `dirname(__filename)/../..` resolveria para o CACHE DO PLUGIN em vez do repo
|
|
41
|
+
* do usuário. O guard não quebraria (fail-open), mas `dir === PROJECT_ROOT`
|
|
42
|
+
* nunca casaria — viraria um NO-OP SILENCIOSO, a pior falha possível para um
|
|
43
|
+
* gate. Cadeia de resolução, do mais autoritativo ao último recurso:
|
|
44
|
+
* 1. `CLAUDE_PROJECT_DIR` — injetado pelo harness, correto por construção;
|
|
45
|
+
* 2. walk-up pelo sentinel `.orchestrator` (primitivo RFC-040 C1);
|
|
46
|
+
* 3. `process.cwd()` — hooks rodam com cwd = diretório do projeto.
|
|
47
|
+
*/
|
|
48
|
+
export const PROJECT_ROOT =
|
|
49
|
+
process.env.CLAUDE_PROJECT_DIR ?? resolveHookProjectRoot(process.cwd()) ?? process.cwd();
|
|
50
|
+
|
|
51
|
+
export interface GuardDecision {
|
|
52
|
+
decision: 'allow' | 'deny';
|
|
53
|
+
reason?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const GUIDANCE =
|
|
57
|
+
'Playwright Output Guard: screenshots não podem cair na raiz do repo (auto-checkpoint os varre — recorrência S266-S268). ' +
|
|
58
|
+
'Use `filename: ".playwright-mcp/<nome>.png"` (descartável, gitignored), ' +
|
|
59
|
+
'`filename: ".cobaia-evidence/<sessão>/<nome>.png"` (evidência a versionar), ' +
|
|
60
|
+
'ou omita `filename` (default já vai para .playwright-mcp/).';
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Avalia o filename pedido pelo agente.
|
|
64
|
+
* DENY quando o arquivo resolveria para a raiz do projeto (ou acima dela).
|
|
65
|
+
*/
|
|
66
|
+
export function evaluateFilename(filename: string | undefined): GuardDecision {
|
|
67
|
+
if (!filename) return { decision: 'allow' };
|
|
68
|
+
|
|
69
|
+
const resolved = isAbsolute(filename) ? normalize(filename) : resolve(PROJECT_ROOT, filename);
|
|
70
|
+
|
|
71
|
+
const dir = dirname(resolved);
|
|
72
|
+
|
|
73
|
+
// Cairia exatamente na raiz do projeto → DENY
|
|
74
|
+
if (dir === PROJECT_ROOT) {
|
|
75
|
+
return { decision: 'deny', reason: GUIDANCE };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Traversal para fora do projeto → DENY (o próprio MCP também negaria,
|
|
79
|
+
// mas aqui damos guidance acionável em vez de erro críptico)
|
|
80
|
+
if (!resolved.startsWith(PROJECT_ROOT + '/') && !isAbsolute(filename)) {
|
|
81
|
+
return { decision: 'deny', reason: GUIDANCE };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { decision: 'allow' };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ─────────────────────────────────────────────────────────────
|
|
88
|
+
// Dependency injection (testável sem IO)
|
|
89
|
+
// ─────────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
export interface HookDeps {
|
|
92
|
+
readStdin: () => Promise<Record<string, unknown>>;
|
|
93
|
+
output: (opts: { decision: 'allow' | 'deny'; reason?: string; context?: string }) => void;
|
|
94
|
+
log: (prefix: string, msg: string) => void;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const defaultDeps: HookDeps = {
|
|
98
|
+
readStdin,
|
|
99
|
+
output: outputPreToolUse,
|
|
100
|
+
log,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export async function mainWithDeps(deps: HookDeps): Promise<void> {
|
|
104
|
+
const stdin = await deps.readStdin();
|
|
105
|
+
const filename = getField(stdin, 'tool_input.filename');
|
|
106
|
+
|
|
107
|
+
const result = evaluateFilename(filename || undefined);
|
|
108
|
+
deps.log(HOOK, `filename=${filename || '(none)'} decision=${result.decision}`);
|
|
109
|
+
|
|
110
|
+
if (result.decision === 'deny') {
|
|
111
|
+
deps.output({ decision: 'deny', reason: result.reason });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
deps.output({
|
|
116
|
+
decision: 'allow',
|
|
117
|
+
...(filename
|
|
118
|
+
? {}
|
|
119
|
+
: {
|
|
120
|
+
context:
|
|
121
|
+
'[PLAYWRIGHT-OUTPUT-GUARD] sem filename: output em .playwright-mcp/ (gitignored).',
|
|
122
|
+
}),
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function main(): Promise<void> {
|
|
127
|
+
await mainWithDeps(defaultDeps);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
main().catch(() => process.exit(0));
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { createRequire as __createRequireShim } from 'node:module'; const require = __createRequireShim(import.meta.url);
|
|
2
|
+
|
|
3
|
+
// hooks/pre-agent-dispatch.ts
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
// hooks/lib/api-client.ts
|
|
7
|
+
import { readFileSync, writeFileSync, mkdirSync, statSync, unlinkSync, readdirSync } from "node:fs";
|
|
8
|
+
import { join as join2, dirname as dirname2 } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
|
|
11
|
+
// ../project-root-resolver/dist/index.js
|
|
12
|
+
import { existsSync } from "node:fs";
|
|
13
|
+
import { dirname, join } from "node:path";
|
|
14
|
+
function resolveProjectRoot(cwd, sentinel = ".orchestrator") {
|
|
15
|
+
let currentDir = cwd;
|
|
16
|
+
for (; ; ) {
|
|
17
|
+
if (existsSync(join(currentDir, sentinel))) {
|
|
18
|
+
return currentDir;
|
|
19
|
+
}
|
|
20
|
+
const parentDir = dirname(currentDir);
|
|
21
|
+
if (parentDir === currentDir) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
currentDir = parentDir;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// hooks/lib/api-client.ts
|
|
29
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
30
|
+
var __dirname = dirname2(__filename);
|
|
31
|
+
var PROJECT_ROOT = join2(__dirname, "..", "..", "..");
|
|
32
|
+
var STATE_DIR = join2(PROJECT_ROOT, ".orchestrator", ".state");
|
|
33
|
+
var JWT_CACHE = join2(STATE_DIR, "jwt-token");
|
|
34
|
+
var DEBUG_LOG = join2(STATE_DIR, "hook-debug.log");
|
|
35
|
+
mkdirSync(STATE_DIR, { recursive: true });
|
|
36
|
+
var API_URL = process.env.ORCHESTRATOR_API_URL || "http://localhost:3001";
|
|
37
|
+
var AUTH_EMAIL = process.env.ORCHESTRATOR_ADMIN_EMAIL || process.env.ORCHESTRATOR_AUTH_EMAIL || "system@orchestrator.local";
|
|
38
|
+
var AUTH_PASSWORD = process.env.ORCHESTRATOR_ADMIN_PASSWORD || process.env.ORCHESTRATOR_AUTH_PASSWORD || "system123";
|
|
39
|
+
var PROJECT_ID = process.env.ORCHESTRATOR_PROJECT_ID || "";
|
|
40
|
+
function resolveHookProjectRoot(cwd) {
|
|
41
|
+
return resolveProjectRoot(cwd);
|
|
42
|
+
}
|
|
43
|
+
var projectIdResolutionAttempted = false;
|
|
44
|
+
async function ensureProjectIdResolved(token) {
|
|
45
|
+
if (PROJECT_ID || projectIdResolutionAttempted) return;
|
|
46
|
+
projectIdResolutionAttempted = true;
|
|
47
|
+
try {
|
|
48
|
+
const rootPath = resolveHookProjectRoot(process.cwd());
|
|
49
|
+
if (rootPath === null) {
|
|
50
|
+
log(
|
|
51
|
+
"API-CLIENT",
|
|
52
|
+
"ensureProjectIdResolved: cwd not registered (.orchestrator sentinel not found) \u2014 skipping"
|
|
53
|
+
);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const resolved = await resolveProjectIdByPath(rootPath, token);
|
|
57
|
+
if (resolved) {
|
|
58
|
+
PROJECT_ID = resolved;
|
|
59
|
+
log(
|
|
60
|
+
"API-CLIENT",
|
|
61
|
+
`ensureProjectIdResolved: resolved projectId=${resolved} rootPath=${rootPath}`
|
|
62
|
+
);
|
|
63
|
+
} else {
|
|
64
|
+
log(
|
|
65
|
+
"API-CLIENT",
|
|
66
|
+
`ensureProjectIdResolved: rootPath=${rootPath} not registered to any project \u2014 skipping`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
} catch (err) {
|
|
70
|
+
log(
|
|
71
|
+
"API-CLIENT",
|
|
72
|
+
`ensureProjectIdResolved: unexpected failure \u2014 ${err instanceof Error ? err.message : String(err)}`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function log(prefix, msg) {
|
|
77
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "Z");
|
|
78
|
+
try {
|
|
79
|
+
const line = `[${ts}] ${prefix}: ${msg}
|
|
80
|
+
`;
|
|
81
|
+
writeFileSync(DEBUG_LOG, line, { flag: "a" });
|
|
82
|
+
} catch {
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function getCachedToken() {
|
|
86
|
+
try {
|
|
87
|
+
const stat = statSync(JWT_CACHE);
|
|
88
|
+
const ageMs = Date.now() - stat.mtimeMs;
|
|
89
|
+
if (ageMs < 35e5) {
|
|
90
|
+
return readFileSync(JWT_CACHE, "utf-8").trim();
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
async function login() {
|
|
97
|
+
try {
|
|
98
|
+
const resp = await fetch(`${API_URL}/api/v1/auth/login`, {
|
|
99
|
+
method: "POST",
|
|
100
|
+
headers: {
|
|
101
|
+
"Content-Type": "application/json",
|
|
102
|
+
"X-Project-ID": PROJECT_ID
|
|
103
|
+
},
|
|
104
|
+
body: JSON.stringify({ email: AUTH_EMAIL, password: AUTH_PASSWORD }),
|
|
105
|
+
signal: AbortSignal.timeout(5e3)
|
|
106
|
+
});
|
|
107
|
+
if (!resp.ok) return null;
|
|
108
|
+
const data = await resp.json();
|
|
109
|
+
const token = data.accessToken || data.data?.accessToken || "";
|
|
110
|
+
if (token) {
|
|
111
|
+
writeFileSync(JWT_CACHE, token);
|
|
112
|
+
}
|
|
113
|
+
return token || null;
|
|
114
|
+
} catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async function getToken() {
|
|
119
|
+
const token = getCachedToken() || await login();
|
|
120
|
+
if (token) {
|
|
121
|
+
await ensureProjectIdResolved(token);
|
|
122
|
+
}
|
|
123
|
+
return token;
|
|
124
|
+
}
|
|
125
|
+
async function apiGet(path, token, timeoutMs = 5e3) {
|
|
126
|
+
try {
|
|
127
|
+
const resp = await fetch(`${API_URL}${path}`, {
|
|
128
|
+
headers: {
|
|
129
|
+
Authorization: `Bearer ${token}`,
|
|
130
|
+
"X-Project-ID": PROJECT_ID
|
|
131
|
+
},
|
|
132
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
133
|
+
});
|
|
134
|
+
if (!resp.ok) return null;
|
|
135
|
+
return await resp.json();
|
|
136
|
+
} catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async function resolveProjectIdByPath(rootPath, token) {
|
|
141
|
+
const resp = await apiGet(
|
|
142
|
+
`/api/v1/projects/resolve?rootPath=${encodeURIComponent(rootPath)}`,
|
|
143
|
+
token
|
|
144
|
+
);
|
|
145
|
+
return resp?.projectId ?? null;
|
|
146
|
+
}
|
|
147
|
+
async function getActiveWorkflow() {
|
|
148
|
+
const token = await getToken();
|
|
149
|
+
if (!token) return null;
|
|
150
|
+
for (const status of ["in_progress", "awaiting_agent", "awaiting_approval"]) {
|
|
151
|
+
const resp = await apiGet(
|
|
152
|
+
`/api/v1/workflows?status=${status}&limit=1`,
|
|
153
|
+
token,
|
|
154
|
+
3e3
|
|
155
|
+
);
|
|
156
|
+
const list = resp?.data || (Array.isArray(resp) ? resp : null);
|
|
157
|
+
const wf = list?.[0];
|
|
158
|
+
if (wf?.id) return wf;
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
var LAST_COMPLETED_FILE = join2(STATE_DIR, "last-completed-workflow.json");
|
|
163
|
+
var LAST_COMPLETED_TTL_MS = 60 * 60 * 1e3;
|
|
164
|
+
var INVOCATIONS_DIR = join2(STATE_DIR, "invocations");
|
|
165
|
+
var INVOCATION_STATE_TTL_MS = 4 * 60 * 60 * 1e3;
|
|
166
|
+
var DISPATCH_INTENT_DIR = join2(STATE_DIR, "dispatch-intent");
|
|
167
|
+
function buildDispatchIntentPath(stateDir, workflowId) {
|
|
168
|
+
return join2(stateDir, "dispatch-intent", `${workflowId}.json`);
|
|
169
|
+
}
|
|
170
|
+
function writeDispatchIntent(workflowId) {
|
|
171
|
+
try {
|
|
172
|
+
mkdirSync(DISPATCH_INTENT_DIR, { recursive: true });
|
|
173
|
+
const path = buildDispatchIntentPath(STATE_DIR, workflowId);
|
|
174
|
+
const payload = { workflowId, stampedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
175
|
+
writeFileSync(path, JSON.stringify(payload), "utf-8");
|
|
176
|
+
return true;
|
|
177
|
+
} catch {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async function readStdin() {
|
|
182
|
+
const chunks = [];
|
|
183
|
+
for await (const chunk of process.stdin) {
|
|
184
|
+
chunks.push(chunk);
|
|
185
|
+
}
|
|
186
|
+
const raw = Buffer.concat(chunks).toString("utf-8").trim();
|
|
187
|
+
if (!raw) return {};
|
|
188
|
+
try {
|
|
189
|
+
return JSON.parse(raw);
|
|
190
|
+
} catch {
|
|
191
|
+
return {};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function getField(obj, path) {
|
|
195
|
+
const parts = path.split(".");
|
|
196
|
+
let current = obj;
|
|
197
|
+
for (const part of parts) {
|
|
198
|
+
if (current == null || typeof current !== "object") return "";
|
|
199
|
+
current = current[part];
|
|
200
|
+
}
|
|
201
|
+
return current != null ? String(current) : "";
|
|
202
|
+
}
|
|
203
|
+
function outputContext(hookEvent, context) {
|
|
204
|
+
console.log(
|
|
205
|
+
JSON.stringify({
|
|
206
|
+
hookSpecificOutput: {
|
|
207
|
+
hookEventName: hookEvent,
|
|
208
|
+
additionalContext: context
|
|
209
|
+
}
|
|
210
|
+
})
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// hooks/pre-agent-dispatch.ts
|
|
215
|
+
var HOOK = "PRE-AGENT-DISPATCH";
|
|
216
|
+
var KNOWN_AGENTS = [
|
|
217
|
+
"implementer",
|
|
218
|
+
"planner",
|
|
219
|
+
"specifier",
|
|
220
|
+
"reviewer",
|
|
221
|
+
"task-generator",
|
|
222
|
+
"researcher"
|
|
223
|
+
];
|
|
224
|
+
var KNOWN_AGENTS_REGEX = new RegExp(`\\b(${KNOWN_AGENTS.join("|")})\\b`, "i");
|
|
225
|
+
var COMMITTING_AGENTS = [
|
|
226
|
+
"implementer",
|
|
227
|
+
"azure-devops-engineer",
|
|
228
|
+
"presentation-engineer",
|
|
229
|
+
"operational-artifact-builder",
|
|
230
|
+
"cicd-platform-engineer",
|
|
231
|
+
"python-data-engineer",
|
|
232
|
+
"frontend-engineer"
|
|
233
|
+
];
|
|
234
|
+
var PROTECTED_BRANCHES = ["main", "master"];
|
|
235
|
+
function normalizeAgentName(subagentType) {
|
|
236
|
+
const i = subagentType.lastIndexOf(":");
|
|
237
|
+
return i === -1 ? subagentType : subagentType.slice(i + 1);
|
|
238
|
+
}
|
|
239
|
+
function isCommittingAgent(subagentType) {
|
|
240
|
+
if (!subagentType) return false;
|
|
241
|
+
const t = normalizeAgentName(subagentType).toLowerCase();
|
|
242
|
+
if (!t) return false;
|
|
243
|
+
return COMMITTING_AGENTS.some((a) => a.toLowerCase() === t);
|
|
244
|
+
}
|
|
245
|
+
function isProtectedBranch(branch) {
|
|
246
|
+
return PROTECTED_BRANCHES.includes(branch);
|
|
247
|
+
}
|
|
248
|
+
function parseBranchFromHead(headContents) {
|
|
249
|
+
const m = /^ref:\s*refs\/heads\/(.+)$/.exec(headContents.trim());
|
|
250
|
+
return m ? (m[1] ?? "").trim() : "";
|
|
251
|
+
}
|
|
252
|
+
function detectKnownAgentInPrompt(prompt) {
|
|
253
|
+
const match = KNOWN_AGENTS_REGEX.exec(prompt);
|
|
254
|
+
return match ? (match[1] ?? "").toLowerCase() : null;
|
|
255
|
+
}
|
|
256
|
+
function branchBeforeDispatchMessage(subagentType, branch) {
|
|
257
|
+
return `[BRANCH-BEFORE-DISPATCH][TD-171] About to dispatch committing agent '${subagentType}' while on protected branch '${branch}'. This agent can make LOCAL checkpoint commits, and agent-git-guard (TD-169) blocks push/merge but ALLOWS commit \u2014 so a local commit would land directly on '${branch}' (root cause of the S289 '4f1f4f37' breach). Check out a feature branch BEFORE dispatching; the orchestrator owns push after its independent assert.`;
|
|
258
|
+
}
|
|
259
|
+
function evaluateDispatch(ctx) {
|
|
260
|
+
const warnings = [];
|
|
261
|
+
const { subagentType, prompt, branch } = ctx;
|
|
262
|
+
if (!subagentType) {
|
|
263
|
+
const detected = detectKnownAgentInPrompt(prompt);
|
|
264
|
+
if (detected) {
|
|
265
|
+
warnings.push(
|
|
266
|
+
`[WARN] Agent dispatched without subagent_type. Known agent detected: "${detected}". Add subagent_type for proper frontmatter loading.`
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (isCommittingAgent(subagentType) && isProtectedBranch(branch)) {
|
|
271
|
+
warnings.push(branchBeforeDispatchMessage(subagentType, branch));
|
|
272
|
+
}
|
|
273
|
+
return warnings;
|
|
274
|
+
}
|
|
275
|
+
function readBranchFromGit() {
|
|
276
|
+
try {
|
|
277
|
+
return execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
278
|
+
encoding: "utf-8",
|
|
279
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
280
|
+
}).trim();
|
|
281
|
+
} catch {
|
|
282
|
+
return "";
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
var defaultDeps = {
|
|
286
|
+
readStdin,
|
|
287
|
+
outputContext,
|
|
288
|
+
log,
|
|
289
|
+
readBranch: readBranchFromGit,
|
|
290
|
+
getActiveWorkflow,
|
|
291
|
+
writeDispatchIntent
|
|
292
|
+
};
|
|
293
|
+
var DISPATCH_INTENT_RESOLVE_TIMEOUT_MS = 3e3;
|
|
294
|
+
async function stampDispatchIntent(deps) {
|
|
295
|
+
if (!deps.getActiveWorkflow || !deps.writeDispatchIntent) return;
|
|
296
|
+
try {
|
|
297
|
+
const workflow = await Promise.race([
|
|
298
|
+
deps.getActiveWorkflow(),
|
|
299
|
+
new Promise((resolve) => {
|
|
300
|
+
setTimeout(() => {
|
|
301
|
+
resolve(null);
|
|
302
|
+
}, DISPATCH_INTENT_RESOLVE_TIMEOUT_MS);
|
|
303
|
+
})
|
|
304
|
+
]);
|
|
305
|
+
if (workflow?.id) {
|
|
306
|
+
const stamped = deps.writeDispatchIntent(workflow.id);
|
|
307
|
+
deps.log(HOOK, `[TD-205] dispatch-intent stamped=${String(stamped)} workflow=${workflow.id}`);
|
|
308
|
+
}
|
|
309
|
+
} catch (err) {
|
|
310
|
+
deps.log(HOOK, `[TD-205] stampDispatchIntent failed (fail-open): ${String(err)}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
async function mainWithDeps(deps) {
|
|
314
|
+
const stdin = await deps.readStdin();
|
|
315
|
+
const toolInput = getField(stdin, "tool_input") || "";
|
|
316
|
+
const subagentType = getField(stdin, "tool_input.subagent_type") || getField(stdin, "tool_input.subagentType") || "";
|
|
317
|
+
const prompt = getField(stdin, "tool_input.prompt") || getField(stdin, "tool_input.description") || (typeof toolInput === "string" ? toolInput : JSON.stringify(toolInput));
|
|
318
|
+
const branch = deps.readBranch();
|
|
319
|
+
const warnings = evaluateDispatch({ subagentType, prompt, branch });
|
|
320
|
+
await stampDispatchIntent(deps);
|
|
321
|
+
if (warnings.length > 0) {
|
|
322
|
+
deps.outputContext(HOOK, warnings.join("\n"));
|
|
323
|
+
deps.log(
|
|
324
|
+
HOOK,
|
|
325
|
+
`${warnings.length} warning(s) (agent="${subagentType || "?"}" branch="${branch}")`
|
|
326
|
+
);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
deps.log(HOOK, `OK (agent="${subagentType || "?"}" branch="${branch}")`);
|
|
330
|
+
}
|
|
331
|
+
async function main() {
|
|
332
|
+
await mainWithDeps(defaultDeps);
|
|
333
|
+
}
|
|
334
|
+
main().catch(() => process.exit(0));
|
|
335
|
+
export {
|
|
336
|
+
COMMITTING_AGENTS,
|
|
337
|
+
branchBeforeDispatchMessage,
|
|
338
|
+
detectKnownAgentInPrompt,
|
|
339
|
+
evaluateDispatch,
|
|
340
|
+
isCommittingAgent,
|
|
341
|
+
isProtectedBranch,
|
|
342
|
+
mainWithDeps,
|
|
343
|
+
normalizeAgentName,
|
|
344
|
+
parseBranchFromHead,
|
|
345
|
+
stampDispatchIntent
|
|
346
|
+
};
|