@orchestrator-claude/definitions 3.34.0 → 3.36.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/agents/ai-architect.md +136 -0
- package/agents/ciclo-engineer.md +267 -0
- package/agents/orchestrator.md +14 -3
- package/agents/software-architect.md +140 -0
- 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 +30 -15
- 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 +70 -6
- package/package.json +1 -1
- package/personas/INDEX.md +46 -0
- package/skills/ciclo-lane-dispatch/SKILL.md +158 -0
- package/skills/orchestrator/SKILL.md +3 -1
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pre-agent-dispatch.ts — PreToolUse hook for the Agent tool
|
|
3
|
+
*
|
|
4
|
+
* NON-BLOCKING (always exits 0; only injects advisory context). Two responsibilities:
|
|
5
|
+
*
|
|
6
|
+
* 1. (RFC-027 Sprint 4) Warn when a known agent is dispatched WITHOUT subagent_type,
|
|
7
|
+
* so frontmatter (model/tools) loads correctly.
|
|
8
|
+
*
|
|
9
|
+
* 2. (TD-171) Branch-before-dispatch ALERT. When the orchestrator dispatches a
|
|
10
|
+
* COMMITTING agent — one whose tool list holds `mcp__orchestrator-tools__createCheckpoint`,
|
|
11
|
+
* so it may make local checkpoint commits — while sitting on a PROTECTED branch
|
|
12
|
+
* (main/master), warn it to check out a feature branch FIRST. Root cause of the
|
|
13
|
+
* S289 breach `4f1f4f37`: an implementer was dispatched from `main`, so its local
|
|
14
|
+
* commit landed directly on the protected branch. agent-git-guard (TD-169) blocks
|
|
15
|
+
* push/merge but intentionally ALLOWS local commit, so a local commit on `main`
|
|
16
|
+
* slips through. The real fix is branch-before-dispatch (orchestrator owns the
|
|
17
|
+
* branch); this hook is the non-blocking backstop. Per the user directive it ALERTS
|
|
18
|
+
* (not blocks) — the hard stop on the unrecoverable vector stays in agent-git-guard.
|
|
19
|
+
*
|
|
20
|
+
* 3. (TD-205) Dispatch-intent stamp. This hook fires SYNCHRONOUSLY at
|
|
21
|
+
* PreToolUse[Agent] — before the sub-agent spawns / before the turn yields — so it
|
|
22
|
+
* is the guaranteed-present signal for the bg-dispatch race: when the orchestrator
|
|
23
|
+
* dispatches a sub-agent in the BACKGROUND and yields immediately, dangling-guard's
|
|
24
|
+
* Stop hook can fire before subagent-start.ts's ASYNC startInvocation()+
|
|
25
|
+
* writeInvocationState() has landed, producing a false-positive BLOCK on a
|
|
26
|
+
* genuinely active workflow. Stamping a dispatch-intent marker for the active
|
|
27
|
+
* workflow here gives dangling-guard a short (seconds), self-expiring grace-window
|
|
28
|
+
* signal to cover that race without opening a hole in the rule-9 zombie safety net.
|
|
29
|
+
* Best-effort / fail-open in every dimension (missing deps, no active workflow,
|
|
30
|
+
* resolution timeout, filesystem error) — never blocks the dispatch.
|
|
31
|
+
*
|
|
32
|
+
* Design note (TD-171): the committing-agent set mirrors the `createCheckpoint` tool
|
|
33
|
+
* holders in `.claude/agents/*.md`. Keep it in sync with that ground truth and with the
|
|
34
|
+
* orchestrator SKILL §2c.1 Commit-Policy scope.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import { execFileSync } from 'node:child_process';
|
|
38
|
+
import {
|
|
39
|
+
log,
|
|
40
|
+
readStdin,
|
|
41
|
+
getField,
|
|
42
|
+
outputContext,
|
|
43
|
+
getActiveWorkflow,
|
|
44
|
+
writeDispatchIntent,
|
|
45
|
+
} from './lib/api-client.js';
|
|
46
|
+
|
|
47
|
+
const HOOK = 'PRE-AGENT-DISPATCH';
|
|
48
|
+
|
|
49
|
+
// Known workflow agents (RFC-027 Sprint 4 — subagent_type presence check).
|
|
50
|
+
const KNOWN_AGENTS = [
|
|
51
|
+
'implementer',
|
|
52
|
+
'planner',
|
|
53
|
+
'specifier',
|
|
54
|
+
'reviewer',
|
|
55
|
+
'task-generator',
|
|
56
|
+
'researcher',
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
const KNOWN_AGENTS_REGEX = new RegExp(`\\b(${KNOWN_AGENTS.join('|')})\\b`, 'i');
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Agents whose tool list includes `mcp__orchestrator-tools__createCheckpoint` (ground
|
|
63
|
+
* truth: grep `createCheckpoint` over `.claude/agents/*.md`). These may make local
|
|
64
|
+
* checkpoint commits during their phase, so dispatching them on a protected branch is
|
|
65
|
+
* the branch-before-dispatch hazard. Keep in sync with orchestrator SKILL §2c.1.
|
|
66
|
+
*/
|
|
67
|
+
export const COMMITTING_AGENTS: readonly string[] = [
|
|
68
|
+
'implementer',
|
|
69
|
+
'azure-devops-engineer',
|
|
70
|
+
'presentation-engineer',
|
|
71
|
+
'operational-artifact-builder',
|
|
72
|
+
'cicd-platform-engineer',
|
|
73
|
+
'python-data-engineer',
|
|
74
|
+
'frontend-engineer',
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
const PROTECTED_BRANCHES: readonly string[] = ['main', 'master'];
|
|
78
|
+
|
|
79
|
+
// ─────────────────────────────────────────────────────────────
|
|
80
|
+
// Pure helpers (exported for testing)
|
|
81
|
+
// ─────────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Strip the plugin namespace from a dispatch name.
|
|
85
|
+
*
|
|
86
|
+
* Under plugin scope the harness namespaces every agent (`orchestrator:implementer`
|
|
87
|
+
* — measured in the S398 sandbox, RFC-040 §18.14); in project scope the same agent
|
|
88
|
+
* arrives bare (`implementer`). A guard that compares against the bare name only
|
|
89
|
+
* answers `false` for the namespaced form — and since hooks fail open, it disables
|
|
90
|
+
* itself in silence. Both forms MUST resolve to the same decision.
|
|
91
|
+
*/
|
|
92
|
+
export function normalizeAgentName(subagentType: string): string {
|
|
93
|
+
const i = subagentType.lastIndexOf(':');
|
|
94
|
+
return i === -1 ? subagentType : subagentType.slice(i + 1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function isCommittingAgent(subagentType: string): boolean {
|
|
98
|
+
if (!subagentType) return false;
|
|
99
|
+
const t = normalizeAgentName(subagentType).toLowerCase();
|
|
100
|
+
if (!t) return false;
|
|
101
|
+
return COMMITTING_AGENTS.some((a) => a.toLowerCase() === t);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function isProtectedBranch(branch: string): boolean {
|
|
105
|
+
return PROTECTED_BRANCHES.includes(branch);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Parse `.git/HEAD` contents → branch name, or '' for detached HEAD / junk. */
|
|
109
|
+
export function parseBranchFromHead(headContents: string): string {
|
|
110
|
+
const m = /^ref:\s*refs\/heads\/(.+)$/.exec(headContents.trim());
|
|
111
|
+
return m ? (m[1] ?? '').trim() : '';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function detectKnownAgentInPrompt(prompt: string): string | null {
|
|
115
|
+
const match = KNOWN_AGENTS_REGEX.exec(prompt);
|
|
116
|
+
return match ? (match[1] ?? '').toLowerCase() : null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function branchBeforeDispatchMessage(subagentType: string, branch: string): string {
|
|
120
|
+
return (
|
|
121
|
+
`[BRANCH-BEFORE-DISPATCH][TD-171] About to dispatch committing agent '${subagentType}' while on ` +
|
|
122
|
+
`protected branch '${branch}'. This agent can make LOCAL checkpoint commits, and agent-git-guard ` +
|
|
123
|
+
`(TD-169) blocks push/merge but ALLOWS commit — so a local commit would land directly on '${branch}' ` +
|
|
124
|
+
`(root cause of the S289 '4f1f4f37' breach). Check out a feature branch BEFORE dispatching; the ` +
|
|
125
|
+
`orchestrator owns push after its independent assert.`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface DispatchContext {
|
|
130
|
+
subagentType: string;
|
|
131
|
+
prompt: string;
|
|
132
|
+
branch: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Pure decision. Returns the list of advisory warning lines (possibly empty).
|
|
137
|
+
* - missing subagent_type + a known agent named in the prompt → RFC-027 warn
|
|
138
|
+
* - committing agent dispatched on a protected branch → TD-171 branch-before-dispatch warn
|
|
139
|
+
*/
|
|
140
|
+
export function evaluateDispatch(ctx: DispatchContext): string[] {
|
|
141
|
+
const warnings: string[] = [];
|
|
142
|
+
const { subagentType, prompt, branch } = ctx;
|
|
143
|
+
|
|
144
|
+
if (!subagentType) {
|
|
145
|
+
const detected = detectKnownAgentInPrompt(prompt);
|
|
146
|
+
if (detected) {
|
|
147
|
+
warnings.push(
|
|
148
|
+
`[WARN] Agent dispatched without subagent_type. Known agent detected: "${detected}". ` +
|
|
149
|
+
`Add subagent_type for proper frontmatter loading.`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (isCommittingAgent(subagentType) && isProtectedBranch(branch)) {
|
|
155
|
+
warnings.push(branchBeforeDispatchMessage(subagentType, branch));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return warnings;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ─────────────────────────────────────────────────────────────
|
|
162
|
+
// Dependency injection interface (enables testing without IO)
|
|
163
|
+
// ─────────────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
export interface HookDeps {
|
|
166
|
+
readStdin: () => Promise<Record<string, unknown>>;
|
|
167
|
+
outputContext: (hookEvent: string, context: string) => void;
|
|
168
|
+
log: (prefix: string, msg: string) => void;
|
|
169
|
+
readBranch: () => string;
|
|
170
|
+
/**
|
|
171
|
+
* TD-205: resolves the active workflow so the dispatch-intent marker can be
|
|
172
|
+
* workflow-scoped. Optional so pre-TD-205 test doubles keep working —
|
|
173
|
+
* stampDispatchIntent() no-ops (fail-open) when this is absent. Production
|
|
174
|
+
* defaultDeps below always provides it.
|
|
175
|
+
*/
|
|
176
|
+
getActiveWorkflow?: () => Promise<{ id: string } | null>;
|
|
177
|
+
/** TD-205: stamps the dispatch-intent marker for workflowId. See getActiveWorkflow doc. */
|
|
178
|
+
writeDispatchIntent?: (workflowId: string) => boolean;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function readBranchFromGit(): string {
|
|
182
|
+
try {
|
|
183
|
+
// Fixed args, no shell (execFile, not exec) — no interpolation, no injection surface.
|
|
184
|
+
return execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
185
|
+
encoding: 'utf-8',
|
|
186
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
187
|
+
}).trim();
|
|
188
|
+
} catch {
|
|
189
|
+
return '';
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const defaultDeps: HookDeps = {
|
|
194
|
+
readStdin,
|
|
195
|
+
outputContext,
|
|
196
|
+
log,
|
|
197
|
+
readBranch: readBranchFromGit,
|
|
198
|
+
getActiveWorkflow,
|
|
199
|
+
writeDispatchIntent,
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* TD-205: internal resolve timeout for stampDispatchIntent's getActiveWorkflow()
|
|
204
|
+
* call — seconds, bounded well within this hook's own 5000ms PreToolUse
|
|
205
|
+
* timeout (settings.json) so a slow/unavailable API degrades gracefully
|
|
206
|
+
* (marker simply isn't stamped for that dispatch) rather than ever blocking
|
|
207
|
+
* the dispatch itself.
|
|
208
|
+
*/
|
|
209
|
+
const DISPATCH_INTENT_RESOLVE_TIMEOUT_MS = 3000;
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* TD-205: stamps a dispatch-intent marker for the currently active workflow,
|
|
213
|
+
* if one is resolvable within DISPATCH_INTENT_RESOLVE_TIMEOUT_MS. Fail-open
|
|
214
|
+
* in every dimension — missing deps (pre-TD-205 test doubles), no active
|
|
215
|
+
* workflow, a rejected/never-resolving getActiveWorkflow(), or a filesystem
|
|
216
|
+
* failure inside writeDispatchIntent() all silently skip the stamp. This is
|
|
217
|
+
* a best-effort advisory signal, never a blocking step (matches this hook's
|
|
218
|
+
* existing non-blocking posture).
|
|
219
|
+
*/
|
|
220
|
+
export async function stampDispatchIntent(
|
|
221
|
+
deps: Pick<HookDeps, 'getActiveWorkflow' | 'writeDispatchIntent' | 'log'>
|
|
222
|
+
): Promise<void> {
|
|
223
|
+
if (!deps.getActiveWorkflow || !deps.writeDispatchIntent) return;
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
const workflow = await Promise.race([
|
|
227
|
+
deps.getActiveWorkflow(),
|
|
228
|
+
new Promise<null>((resolve) => {
|
|
229
|
+
setTimeout(() => {
|
|
230
|
+
resolve(null);
|
|
231
|
+
}, DISPATCH_INTENT_RESOLVE_TIMEOUT_MS);
|
|
232
|
+
}),
|
|
233
|
+
]);
|
|
234
|
+
if (workflow?.id) {
|
|
235
|
+
const stamped = deps.writeDispatchIntent(workflow.id);
|
|
236
|
+
deps.log(HOOK, `[TD-205] dispatch-intent stamped=${String(stamped)} workflow=${workflow.id}`);
|
|
237
|
+
}
|
|
238
|
+
} catch (err) {
|
|
239
|
+
deps.log(HOOK, `[TD-205] stampDispatchIntent failed (fail-open): ${String(err)}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ─────────────────────────────────────────────────────────────
|
|
244
|
+
// Core logic (exported for testing)
|
|
245
|
+
// ─────────────────────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
export async function mainWithDeps(deps: HookDeps): Promise<void> {
|
|
248
|
+
const stdin = await deps.readStdin();
|
|
249
|
+
|
|
250
|
+
const toolInput = getField(stdin, 'tool_input') || '';
|
|
251
|
+
const subagentType =
|
|
252
|
+
getField(stdin, 'tool_input.subagent_type') || getField(stdin, 'tool_input.subagentType') || '';
|
|
253
|
+
const prompt =
|
|
254
|
+
getField(stdin, 'tool_input.prompt') ||
|
|
255
|
+
getField(stdin, 'tool_input.description') ||
|
|
256
|
+
(typeof toolInput === 'string' ? toolInput : JSON.stringify(toolInput));
|
|
257
|
+
|
|
258
|
+
const branch = deps.readBranch();
|
|
259
|
+
|
|
260
|
+
const warnings = evaluateDispatch({ subagentType, prompt, branch });
|
|
261
|
+
|
|
262
|
+
// TD-205: stamp the dispatch-intent marker regardless of whether a warning
|
|
263
|
+
// fires — a dispatch is happening either way, and the marker exists purely
|
|
264
|
+
// to cover the bg-dispatch race for dangling-guard's Stop hook.
|
|
265
|
+
await stampDispatchIntent(deps);
|
|
266
|
+
|
|
267
|
+
if (warnings.length > 0) {
|
|
268
|
+
deps.outputContext(HOOK, warnings.join('\n'));
|
|
269
|
+
deps.log(
|
|
270
|
+
HOOK,
|
|
271
|
+
`${warnings.length} warning(s) (agent="${subagentType || '?'}" branch="${branch}")`
|
|
272
|
+
);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
deps.log(HOOK, `OK (agent="${subagentType || '?'}" branch="${branch}")`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ─────────────────────────────────────────────────────────────
|
|
280
|
+
// Entry point (production use)
|
|
281
|
+
// ─────────────────────────────────────────────────────────────
|
|
282
|
+
|
|
283
|
+
async function main(): Promise<void> {
|
|
284
|
+
await mainWithDeps(defaultDeps);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
main().catch(() => process.exit(0));
|
package/hooks/session-start.mjs
CHANGED
|
@@ -7881,7 +7881,6 @@ function readSessionSelection(stateDir, sessionId) {
|
|
|
7881
7881
|
var HOOK = "SESSION-START";
|
|
7882
7882
|
var __filename2 = fileURLToPath2(import.meta.url);
|
|
7883
7883
|
var __dirname2 = dirname3(__filename2);
|
|
7884
|
-
var GRAPHRAG_TIMEOUT_MS = 2e3;
|
|
7885
7884
|
var PROJECT_ROOT2 = resolveHookProjectRoot(process.cwd());
|
|
7886
7885
|
var PROJECT_ID2 = process.env.ORCHESTRATOR_PROJECT_ID ?? "";
|
|
7887
7886
|
var HOME = process.env.HOME ?? process.env.USERPROFILE ?? "/home/user";
|
|
@@ -7960,7 +7959,7 @@ function readBacklogFallback() {
|
|
|
7960
7959
|
const latestSession = sessionMatches.sort((a, b) => b.num - a.num || a.idx - b.idx)[0];
|
|
7961
7960
|
const lastSession = latestSession?.line.replace(/\*\*/g, "").slice(0, 120) || "";
|
|
7962
7961
|
const nextLine = allLines.find((l) => /^\*\*proximo passo/i.test(l) || /^\*\*next/i.test(l));
|
|
7963
|
-
const next = nextLine?.replace(/.*\*\*(proximo passo|next):?\*\*:?\s*/i, "").replace(/\s*$/, "").slice(0, 120) || "see
|
|
7962
|
+
const next = nextLine?.replace(/.*\*\*(proximo passo|next):?\*\*:?\s*/i, "").replace(/\s*$/, "").slice(0, 120) || "BACKLOG.md has no next-step line (legacy parser; see project-guidelines/project-state.yaml)";
|
|
7964
7963
|
log(
|
|
7965
7964
|
HOOK,
|
|
7966
7965
|
`BACKLOG.md fallback parsed: version=${version} next=${next.slice(0, 50)} lastSession=${lastSession.slice(0, 50)}`
|
|
@@ -7968,7 +7967,12 @@ function readBacklogFallback() {
|
|
|
7968
7967
|
return { version, next, status, lastSession };
|
|
7969
7968
|
} catch (err) {
|
|
7970
7969
|
log(HOOK, `BACKLOG read failed: ${err}`);
|
|
7971
|
-
return {
|
|
7970
|
+
return {
|
|
7971
|
+
version: "v?.?.?",
|
|
7972
|
+
next: "no session state (project-guidelines/project-state.yaml missing or invalid, BACKLOG.md unreadable)",
|
|
7973
|
+
status: "",
|
|
7974
|
+
lastSession: ""
|
|
7975
|
+
};
|
|
7972
7976
|
}
|
|
7973
7977
|
}
|
|
7974
7978
|
function getBacklogSummary() {
|
|
@@ -8128,34 +8132,6 @@ function checkSprintInbox() {
|
|
|
8128
8132
|
return "";
|
|
8129
8133
|
}
|
|
8130
8134
|
}
|
|
8131
|
-
async function getGraphContext(token, fetcher) {
|
|
8132
|
-
if (token === null || token === "") return "";
|
|
8133
|
-
const defaultFetcher = async (path, tok, timeoutMs = GRAPHRAG_TIMEOUT_MS) => {
|
|
8134
|
-
const resp = await fetch(`http://localhost:3000${path}`, {
|
|
8135
|
-
headers: { Authorization: `Bearer ${tok}` },
|
|
8136
|
-
signal: AbortSignal.timeout(timeoutMs)
|
|
8137
|
-
});
|
|
8138
|
-
if (!resp.ok) return null;
|
|
8139
|
-
return resp.json();
|
|
8140
|
-
};
|
|
8141
|
-
const fetch_ = fetcher ?? defaultFetcher;
|
|
8142
|
-
try {
|
|
8143
|
-
const [statsRaw, atRiskRaw] = await Promise.all([
|
|
8144
|
-
fetch_("/api/v1/graphrag/stats", token, GRAPHRAG_TIMEOUT_MS),
|
|
8145
|
-
fetch_("/api/v1/graphrag/at-risk", token, GRAPHRAG_TIMEOUT_MS)
|
|
8146
|
-
]);
|
|
8147
|
-
const stats = statsRaw;
|
|
8148
|
-
if (stats?.nodeCount === void 0) return "";
|
|
8149
|
-
const atRiskData = atRiskRaw;
|
|
8150
|
-
const components = String(stats.nodeCount);
|
|
8151
|
-
const edges = String(stats.edgeCount ?? 0);
|
|
8152
|
-
const hotPaths = (stats.hotPaths ?? []).join(",");
|
|
8153
|
-
const atRisk = (atRiskData?.atRisk ?? []).join(",");
|
|
8154
|
-
return `[GRAPH-CONTEXT] components=${components} edges=${edges} hot_paths=[${hotPaths}] at_risk=[${atRisk}]`;
|
|
8155
|
-
} catch {
|
|
8156
|
-
return "";
|
|
8157
|
-
}
|
|
8158
|
-
}
|
|
8159
8135
|
async function main() {
|
|
8160
8136
|
log(HOOK, "SessionStart hook triggered");
|
|
8161
8137
|
if (PROJECT_ROOT2 === null) {
|
|
@@ -8172,8 +8148,7 @@ async function main() {
|
|
|
8172
8148
|
const tierSegment = formatTierSegment(tierInfo);
|
|
8173
8149
|
const capabilityContext = buildCapabilityMatrixContext();
|
|
8174
8150
|
const sprintInboxContext = checkSprintInbox();
|
|
8175
|
-
|
|
8176
|
-
const graphContext = await getGraphContext(token);
|
|
8151
|
+
await getToken();
|
|
8177
8152
|
const selection = PROJECT_SELECTION_ENABLED && sessionId ? readSessionSelection(SESSION_SELECTION_STATE_DIR, sessionId) : void 0;
|
|
8178
8153
|
const projectBinding = resolveSessionProjectBinding({
|
|
8179
8154
|
envOverride: PROJECT_ID2,
|
|
@@ -8198,7 +8173,6 @@ async function main() {
|
|
|
8198
8173
|
"Do NOT invoke /orchestrator skill for the greeting \u2014 use AskUserQuestion directly.",
|
|
8199
8174
|
"Invoke /orchestrator skill only when the user chooses to start a workflow.",
|
|
8200
8175
|
capabilityContext,
|
|
8201
|
-
graphContext,
|
|
8202
8176
|
sprintInboxContext,
|
|
8203
8177
|
projectSelectionPrompt
|
|
8204
8178
|
].filter(Boolean).join("\n")
|
|
@@ -8216,7 +8190,6 @@ async function main() {
|
|
|
8216
8190
|
"Do NOT invoke /orchestrator skill for the greeting \u2014 use AskUserQuestion directly.",
|
|
8217
8191
|
"Invoke /orchestrator skill when the user chooses to continue the workflow.",
|
|
8218
8192
|
capabilityContext,
|
|
8219
|
-
graphContext,
|
|
8220
8193
|
projectSelectionPrompt
|
|
8221
8194
|
].filter(Boolean).join("\n")
|
|
8222
8195
|
);
|
|
@@ -8227,7 +8200,6 @@ export {
|
|
|
8227
8200
|
formatProjectBindingSegment,
|
|
8228
8201
|
formatSessionNumberSegment,
|
|
8229
8202
|
formatTierSegment,
|
|
8230
|
-
getGraphContext,
|
|
8231
8203
|
readProjectState,
|
|
8232
8204
|
resolveSessionProjectBinding,
|
|
8233
8205
|
synthesizeStatus
|
package/hooks/session-start.ts
CHANGED
|
@@ -37,7 +37,6 @@ import { readSessionSelection } from './lib/session-selection.js';
|
|
|
37
37
|
const HOOK = 'SESSION-START';
|
|
38
38
|
const __filename = fileURLToPath(import.meta.url);
|
|
39
39
|
const __dirname = dirname(__filename);
|
|
40
|
-
const GRAPHRAG_TIMEOUT_MS = 2000;
|
|
41
40
|
// RFC-040 Operator Multi-Project — Slice 5 (C5), S358/S361: cwd-aware
|
|
42
41
|
// resolution (walks up for the .orchestrator sentinel) replaces the
|
|
43
42
|
// __dirname-hardcoded path. `null` means an unregistered cwd — deliberately
|
|
@@ -208,7 +207,8 @@ function readBacklogFallback(): BacklogSummary {
|
|
|
208
207
|
nextLine
|
|
209
208
|
?.replace(/.*\*\*(proximo passo|next):?\*\*:?\s*/i, '')
|
|
210
209
|
.replace(/\s*$/, '')
|
|
211
|
-
.slice(0, 120) ||
|
|
210
|
+
.slice(0, 120) ||
|
|
211
|
+
'BACKLOG.md has no next-step line (legacy parser; see project-guidelines/project-state.yaml)';
|
|
212
212
|
|
|
213
213
|
log(
|
|
214
214
|
HOOK,
|
|
@@ -217,7 +217,12 @@ function readBacklogFallback(): BacklogSummary {
|
|
|
217
217
|
return { version, next, status, lastSession };
|
|
218
218
|
} catch (err) {
|
|
219
219
|
log(HOOK, `BACKLOG read failed: ${err}`);
|
|
220
|
-
return {
|
|
220
|
+
return {
|
|
221
|
+
version: 'v?.?.?',
|
|
222
|
+
next: 'no session state (project-guidelines/project-state.yaml missing or invalid, BACKLOG.md unreadable)',
|
|
223
|
+
status: '',
|
|
224
|
+
lastSession: '',
|
|
225
|
+
};
|
|
221
226
|
}
|
|
222
227
|
}
|
|
223
228
|
|
|
@@ -371,9 +376,9 @@ export function formatProjectBindingSegment(binding: SessionProjectBinding): str
|
|
|
371
376
|
* registered project this session belongs to, listing every registered
|
|
372
377
|
* project by name/id/rootPath.
|
|
373
378
|
*
|
|
374
|
-
* Fail-open (Class C benign degrade, mirrors buildCapabilityMatrixContext
|
|
375
|
-
*
|
|
376
|
-
*
|
|
379
|
+
* Fail-open (Class C benign degrade, mirrors buildCapabilityMatrixContext):
|
|
380
|
+
* returns "" on any error — a failed project-list fetch must never block
|
|
381
|
+
* or crash the hook.
|
|
377
382
|
*/
|
|
378
383
|
export async function buildProjectSelectionPrompt(): Promise<string> {
|
|
379
384
|
try {
|
|
@@ -504,66 +509,6 @@ function checkSprintInbox(): string {
|
|
|
504
509
|
}
|
|
505
510
|
}
|
|
506
511
|
|
|
507
|
-
// ─── Graph Context (RFC-021 T-G4-06) ──────────────────────────────────────────
|
|
508
|
-
|
|
509
|
-
type GraphFetcher = (path: string, token: string, timeoutMs?: number) => Promise<unknown>;
|
|
510
|
-
|
|
511
|
-
interface GraphStatsResponse {
|
|
512
|
-
nodeCount?: number;
|
|
513
|
-
edgeCount?: number;
|
|
514
|
-
hotPaths?: string[];
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
interface AtRiskResponse {
|
|
518
|
-
atRisk?: string[];
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
/**
|
|
522
|
-
* Fetch graph stats and at-risk components from the GraphRAG API.
|
|
523
|
-
* Uses Promise.all for parallel calls with a 2s AbortController timeout.
|
|
524
|
-
* Fail-open: returns "" on any error so session-start never blocks.
|
|
525
|
-
*
|
|
526
|
-
* @param token - JWT token (null → skip API calls, return "")
|
|
527
|
-
* @param fetcher - injectable fetch function (default: real HTTP via api-client pattern)
|
|
528
|
-
*/
|
|
529
|
-
export async function getGraphContext(
|
|
530
|
-
token: string | null,
|
|
531
|
-
fetcher?: GraphFetcher
|
|
532
|
-
): Promise<string> {
|
|
533
|
-
if (token === null || token === '') return '';
|
|
534
|
-
|
|
535
|
-
const defaultFetcher: GraphFetcher = async (path, tok, timeoutMs = GRAPHRAG_TIMEOUT_MS) => {
|
|
536
|
-
const resp = await fetch(`http://localhost:3000${path}`, {
|
|
537
|
-
headers: { Authorization: `Bearer ${tok}` },
|
|
538
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
539
|
-
});
|
|
540
|
-
if (!resp.ok) return null;
|
|
541
|
-
return resp.json() as Promise<unknown>;
|
|
542
|
-
};
|
|
543
|
-
|
|
544
|
-
const fetch_ = fetcher ?? defaultFetcher;
|
|
545
|
-
|
|
546
|
-
try {
|
|
547
|
-
const [statsRaw, atRiskRaw] = await Promise.all([
|
|
548
|
-
fetch_('/api/v1/graphrag/stats', token, GRAPHRAG_TIMEOUT_MS),
|
|
549
|
-
fetch_('/api/v1/graphrag/at-risk', token, GRAPHRAG_TIMEOUT_MS),
|
|
550
|
-
]);
|
|
551
|
-
|
|
552
|
-
const stats = statsRaw as GraphStatsResponse | null;
|
|
553
|
-
if (stats?.nodeCount === undefined) return '';
|
|
554
|
-
|
|
555
|
-
const atRiskData = atRiskRaw as AtRiskResponse | null;
|
|
556
|
-
const components = String(stats.nodeCount);
|
|
557
|
-
const edges = String(stats.edgeCount ?? 0);
|
|
558
|
-
const hotPaths = (stats.hotPaths ?? []).join(',');
|
|
559
|
-
const atRisk = (atRiskData?.atRisk ?? []).join(',');
|
|
560
|
-
|
|
561
|
-
return `[GRAPH-CONTEXT] components=${components} edges=${edges} hot_paths=[${hotPaths}] at_risk=[${atRisk}]`;
|
|
562
|
-
} catch {
|
|
563
|
-
return '';
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
|
|
567
512
|
async function main(): Promise<void> {
|
|
568
513
|
log(HOOK, 'SessionStart hook triggered');
|
|
569
514
|
|
|
@@ -586,15 +531,15 @@ async function main(): Promise<void> {
|
|
|
586
531
|
// Sprint inbox check (RFC-027 S4 — only for sprint panes)
|
|
587
532
|
const sprintInboxContext = checkSprintInbox();
|
|
588
533
|
|
|
589
|
-
//
|
|
590
|
-
// (S372): this getToken() call MUST stay HERE, before
|
|
534
|
+
// RFC-040 TD-206 (S372): this getToken() call MUST stay HERE, before
|
|
591
535
|
// resolveSessionProjectBinding() below — it is what bootstraps the
|
|
592
536
|
// cwd-hint tier (getToken -> ensureProjectIdResolved ->
|
|
593
537
|
// getEffectiveProjectId() reads the resolved value just below). Moving
|
|
594
538
|
// getActiveWorkflow()/getNextAction() later (see below) must not remove
|
|
595
539
|
// this bootstrap, or the cwd-hint tier would regress to reading '' .
|
|
596
|
-
|
|
597
|
-
|
|
540
|
+
// Return value no longer consumed directly (TD-211 F9 — getGraphContext,
|
|
541
|
+
// its sole prior consumer, was removed along with the graph-context surface).
|
|
542
|
+
await getToken();
|
|
598
543
|
|
|
599
544
|
// RFC-040 TD-204 ⑤-C: read a previously-persisted selection for THIS
|
|
600
545
|
// sessionId — feature-flagged (default OFF, kill-switch). With the flag
|
|
@@ -670,7 +615,6 @@ async function main(): Promise<void> {
|
|
|
670
615
|
'Do NOT invoke /orchestrator skill for the greeting — use AskUserQuestion directly.',
|
|
671
616
|
'Invoke /orchestrator skill only when the user chooses to start a workflow.',
|
|
672
617
|
capabilityContext,
|
|
673
|
-
graphContext,
|
|
674
618
|
sprintInboxContext,
|
|
675
619
|
projectSelectionPrompt,
|
|
676
620
|
]
|
|
@@ -696,7 +640,6 @@ async function main(): Promise<void> {
|
|
|
696
640
|
'Do NOT invoke /orchestrator skill for the greeting — use AskUserQuestion directly.',
|
|
697
641
|
'Invoke /orchestrator skill when the user chooses to continue the workflow.',
|
|
698
642
|
capabilityContext,
|
|
699
|
-
graphContext,
|
|
700
643
|
projectSelectionPrompt,
|
|
701
644
|
]
|
|
702
645
|
.filter(Boolean)
|
package/hooks/subagent-stop.mjs
CHANGED
|
@@ -7595,16 +7595,6 @@ function getField(obj, path) {
|
|
|
7595
7595
|
}
|
|
7596
7596
|
return current != null ? String(current) : "";
|
|
7597
7597
|
}
|
|
7598
|
-
function outputContext(hookEvent, context) {
|
|
7599
|
-
console.log(
|
|
7600
|
-
JSON.stringify({
|
|
7601
|
-
hookSpecificOutput: {
|
|
7602
|
-
hookEventName: hookEvent,
|
|
7603
|
-
additionalContext: context
|
|
7604
|
-
}
|
|
7605
|
-
})
|
|
7606
|
-
);
|
|
7607
|
-
}
|
|
7608
7598
|
|
|
7609
7599
|
// hooks/lib/git-checkpoint.ts
|
|
7610
7600
|
import { execSync } from "node:child_process";
|
|
@@ -7766,14 +7756,6 @@ async function main() {
|
|
|
7766
7756
|
);
|
|
7767
7757
|
}
|
|
7768
7758
|
const commitHash = await createCheckpointIfNeeded(workflow.id, workflow.currentPhase);
|
|
7769
|
-
const parts = [
|
|
7770
|
-
`[ORCHESTRATOR] Agent '${agentType}' completed for workflow ${workflow.id}.`
|
|
7771
|
-
];
|
|
7772
|
-
if (commitHash) {
|
|
7773
|
-
parts.push(`Checkpoint: ${commitHash}.`);
|
|
7774
|
-
}
|
|
7775
|
-
parts.push("Proceed to gate check via AskUserQuestion before advancing phase.");
|
|
7776
|
-
outputContext("SubagentStop", parts.join(" "));
|
|
7777
7759
|
try {
|
|
7778
7760
|
const sessionId = getField(stdin, "session_id") || getField(stdin, "claude_session_id") || "";
|
|
7779
7761
|
const memoryBaseDir = resolveAgentMemoryBaseDir(process.cwd());
|
package/hooks/subagent-stop.ts
CHANGED
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
readStdin,
|
|
14
14
|
getField,
|
|
15
15
|
getActiveWorkflow,
|
|
16
|
-
outputContext,
|
|
17
16
|
readInvocationState,
|
|
18
17
|
consumeInvocationState,
|
|
19
18
|
completeAgentInvocation,
|
|
@@ -82,18 +81,35 @@ async function main(): Promise<void> {
|
|
|
82
81
|
// 2. Git checkpoint if phase changed
|
|
83
82
|
const commitHash = await createCheckpointIfNeeded(workflow.id, workflow.currentPhase);
|
|
84
83
|
|
|
85
|
-
// 3.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
84
|
+
// 3. NO context emission on SubagentStop — see below.
|
|
85
|
+
//
|
|
86
|
+
// This block used to call outputContext('SubagentStop', "[ORCHESTRATOR] Agent 'X'
|
|
87
|
+
// completed... Proceed to gate check via AskUserQuestion before advancing phase.").
|
|
88
|
+
// The intent was to guide the MAIN conversation (RFC-022: main IS the orchestrator).
|
|
89
|
+
// That intent is UNREACHABLE from this event, and the attempt was actively harmful:
|
|
90
|
+
//
|
|
91
|
+
// outputContext() emits hookSpecificOutput.additionalContext. On SubagentStop that
|
|
92
|
+
// context is injected into the SUB-AGENT's turn, not the orchestrator's — so the
|
|
93
|
+
// sub-agent was handed an orchestrator-addressed instruction naming a tool it does
|
|
94
|
+
// not have (AskUserQuestion). It cannot comply and cannot decline; it replies, stops
|
|
95
|
+
// again, the hook re-fires with the identical message, and the cycle repeats.
|
|
96
|
+
//
|
|
97
|
+
// MEASURED (S400, .orchestrator/.state/hook-debug.log): exactly 9 re-fires per dispatch,
|
|
98
|
+
// for `specifier`, `planner` AND — in logs predating this session — `implementer`. The
|
|
99
|
+
// sub-agent's final report is consumed by the loop (the orchestrator receives only the
|
|
100
|
+
// agent handle), while the real work survives because it lands via artifactStore.
|
|
101
|
+
// The planner of workflow 8f149ec6 self-reported the loop from inside it.
|
|
102
|
+
//
|
|
103
|
+
// Do NOT reintroduce an additionalContext emission here. Any payload on this event
|
|
104
|
+
// re-prompts the sub-agent — the defect is the emission itself, not its wording.
|
|
105
|
+
// The orchestrator already receives the sub-agent's result directly and owns the gate
|
|
106
|
+
// check unconditionally (orchestrator Skin, MUST #2). The RFC-034 post-implement signal
|
|
107
|
+
// is unaffected: it lives in post-implement.ts on PostToolUse[advancePhase], which fires
|
|
108
|
+
// in the MAIN conversation's context, where additionalContext is the correct mechanism.
|
|
109
|
+
//
|
|
110
|
+
// The side effects above (invocation-completion safety net, git checkpoint) and the
|
|
111
|
+
// observation below are unaffected — they are writes, not context injection.
|
|
112
|
+
// (commitHash is still consumed by the observation payload below.)
|
|
97
113
|
|
|
98
114
|
// 4. Write Tier B observation (fail-open)
|
|
99
115
|
try {
|