@orchestrator-claude/definitions 3.31.1 → 3.32.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/docs-guardian.md +13 -73
- package/hooks/dangling-guard.ts +148 -10
- package/hooks/gate-guardian.ts +177 -40
- package/hooks/lib/api-client.ts +419 -38
- package/hooks/lib/capability-scanner.ts +640 -0
- package/hooks/lib/finops-parser.ts +247 -0
- package/hooks/lib/gate-logic.ts +7 -7
- package/hooks/lib/git-checkpoint.ts +20 -20
- package/hooks/lib/observations-helper.ts +104 -0
- package/hooks/lib/sprint-context.ts +102 -0
- package/hooks/mailbox-listener.ts +139 -25
- package/hooks/post-compact.ts +137 -11
- package/hooks/post-implement.ts +88 -0
- package/hooks/session-start.ts +416 -47
- package/hooks/sprint-registry.ts +6 -5
- package/hooks/subagent-start.ts +27 -15
- package/hooks/subagent-stop.ts +72 -13
- package/hooks/user-prompt.ts +43 -15
- package/hooks/workflow-guard.ts +96 -37
- package/manifest.json +53 -29
- package/package.json +1 -1
- package/skills/backlog-synthesis/SKILL.md +1 -1
- package/skills/workflow-status/SKILL.md +1 -1
- package/hooks/pre-agent-dispatch.ts +0 -57
- package/hooks/verification-before-completion.ts +0 -97
package/hooks/session-start.ts
CHANGED
|
@@ -4,93 +4,462 @@
|
|
|
4
4
|
* Injects workflow status context on session start.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { readFileSync } from
|
|
8
|
-
import { join, dirname } from
|
|
9
|
-
import { fileURLToPath } from
|
|
7
|
+
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
8
|
+
import { join, dirname } from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { parse as parseYaml } from 'yaml';
|
|
10
11
|
import {
|
|
11
12
|
log,
|
|
12
13
|
getActiveWorkflow,
|
|
13
14
|
getNextAction,
|
|
14
15
|
outputContext,
|
|
15
|
-
|
|
16
|
+
getToken,
|
|
17
|
+
} from './lib/api-client.js';
|
|
18
|
+
import {
|
|
19
|
+
needsRescan,
|
|
20
|
+
buildCapabilityIndex,
|
|
21
|
+
writeCapabilitiesToVault,
|
|
22
|
+
readCapabilitiesFromVault,
|
|
23
|
+
scanAgentDefinitions,
|
|
24
|
+
formatCapabilityMatrix,
|
|
25
|
+
readMemoryIndex,
|
|
26
|
+
filterRelevantMemories,
|
|
27
|
+
formatMemoryContext,
|
|
28
|
+
} from './lib/capability-scanner.js';
|
|
16
29
|
|
|
17
|
-
const HOOK =
|
|
30
|
+
const HOOK = 'SESSION-START';
|
|
18
31
|
const __filename = fileURLToPath(import.meta.url);
|
|
19
32
|
const __dirname = dirname(__filename);
|
|
20
|
-
const
|
|
33
|
+
const GRAPHRAG_TIMEOUT_MS = 2000;
|
|
34
|
+
const PROJECT_ROOT = join(__dirname, '..', '..');
|
|
35
|
+
|
|
36
|
+
// ─── Capability Matrix paths (RFC-027 Sprint 2) ─────────
|
|
37
|
+
const HOME = process.env.HOME ?? process.env.USERPROFILE ?? '/home/user';
|
|
38
|
+
const INSTALLED_PLUGINS_PATH = join(HOME, '.claude', 'plugins', 'installed_plugins.json');
|
|
39
|
+
const AGENTS_DIR = join(PROJECT_ROOT, '.claude', 'agents');
|
|
40
|
+
const VAULT_CONFIG_PATH = join(PROJECT_ROOT, 'project-guidelines', '.vault-config.yaml');
|
|
41
|
+
const PROFILES_PATH = join(PROJECT_ROOT, '.claude', 'plugin-profiles.yaml');
|
|
42
|
+
const MEMORY_INDEX_PATH = join(
|
|
43
|
+
PROJECT_ROOT,
|
|
44
|
+
'.claude',
|
|
45
|
+
'agent-memory',
|
|
46
|
+
'orchestrator',
|
|
47
|
+
'memory-index.yaml'
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Read session_id from SessionStart stdin payload.
|
|
52
|
+
* Claude Code sends JSON with session_id on SessionStart.
|
|
53
|
+
*/
|
|
54
|
+
function readSessionId(): string | null {
|
|
55
|
+
try {
|
|
56
|
+
const raw = readFileSync('/dev/stdin', 'utf-8').trim();
|
|
57
|
+
if (!raw) return null;
|
|
58
|
+
const payload = JSON.parse(raw) as Record<string, unknown>;
|
|
59
|
+
return (payload.session_id as string) ?? null;
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ─── RFC-033 Phase 1: project-state.yaml reader ─────────────────────────────
|
|
66
|
+
|
|
67
|
+
const PROJECT_STATE_PATH = join(PROJECT_ROOT, 'project-guidelines', 'project-state.yaml');
|
|
68
|
+
|
|
69
|
+
interface PendingCounts {
|
|
70
|
+
critical: number;
|
|
71
|
+
high: number;
|
|
72
|
+
medium: number;
|
|
73
|
+
ci_gating: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface BacklogSummary {
|
|
77
|
+
version: string;
|
|
78
|
+
next: string;
|
|
79
|
+
status: string;
|
|
80
|
+
lastSession: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Synthesize a short status string from pending counts (RFC-033 Phase 1).
|
|
85
|
+
* Output format: "0 CRITICAL · 0 HIGH · 1 MEDIUM · 0 CI-gating" (<=80 chars).
|
|
86
|
+
* Exported for unit-testability.
|
|
87
|
+
*/
|
|
88
|
+
export function synthesizeStatus(pending: PendingCounts): string {
|
|
89
|
+
const { critical, high, medium, ci_gating: ciGating } = pending;
|
|
90
|
+
return `${String(critical)} CRITICAL · ${String(high)} HIGH · ${String(medium)} MEDIUM · ${String(ciGating)} CI-gating`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Try to read project-state.yaml and map fields to BacklogSummary shape.
|
|
95
|
+
* Returns null on ANY failure (file absent, parse error, missing required fields).
|
|
96
|
+
* Exported for unit-testability.
|
|
97
|
+
*/
|
|
98
|
+
export function readProjectState(filePath: string): BacklogSummary | null {
|
|
99
|
+
try {
|
|
100
|
+
if (!existsSync(filePath)) return null;
|
|
101
|
+
const raw = readFileSync(filePath, 'utf-8');
|
|
102
|
+
const doc = parseYaml(raw) as Record<string, unknown>;
|
|
103
|
+
|
|
104
|
+
// Required field guard
|
|
105
|
+
const version = doc?.version as string | undefined;
|
|
106
|
+
const session = doc?.session as Record<string, unknown> | undefined;
|
|
107
|
+
const nextStep = doc?.next_step as Record<string, unknown> | undefined;
|
|
108
|
+
const pending = doc?.pending as Record<string, unknown> | undefined;
|
|
109
|
+
|
|
110
|
+
if (!version || typeof version !== 'string') return null;
|
|
111
|
+
const nextSummary = nextStep?.summary as string | undefined;
|
|
112
|
+
if (!nextSummary || typeof nextSummary !== 'string') return null;
|
|
113
|
+
|
|
114
|
+
const lastSummary = (session?.last_summary as string | undefined) ?? '';
|
|
115
|
+
const counts: PendingCounts = {
|
|
116
|
+
critical: Number(pending?.critical ?? 0),
|
|
117
|
+
high: Number(pending?.high ?? 0),
|
|
118
|
+
medium: Number(pending?.medium ?? 0),
|
|
119
|
+
ci_gating: Number(pending?.ci_gating ?? 0),
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
version,
|
|
124
|
+
next: nextSummary,
|
|
125
|
+
status: synthesizeStatus(counts),
|
|
126
|
+
lastSession: lastSummary,
|
|
127
|
+
};
|
|
128
|
+
} catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
21
132
|
|
|
22
133
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
134
|
+
* Legacy BACKLOG.md parser — kept as the fallback path (RFC-033 Phase 1).
|
|
135
|
+
* Extracted from the original getBacklogSummary() body; behaviour unchanged.
|
|
25
136
|
*/
|
|
26
|
-
function
|
|
137
|
+
function readBacklogFallback(): BacklogSummary {
|
|
27
138
|
try {
|
|
28
|
-
const backlogPath = join(PROJECT_ROOT,
|
|
29
|
-
const content = readFileSync(backlogPath,
|
|
30
|
-
const
|
|
139
|
+
const backlogPath = join(PROJECT_ROOT, 'project-guidelines', 'BACKLOG.md');
|
|
140
|
+
const content = readFileSync(backlogPath, 'utf-8');
|
|
141
|
+
const allLines = content.split('\n');
|
|
142
|
+
// Version is always near the top (frontmatter + header area)
|
|
143
|
+
const headerLines = allLines.slice(0, 40);
|
|
31
144
|
|
|
32
145
|
// Extract version from "**Release Atual:** vX.Y.Z" or "Current Release: vX.Y.Z"
|
|
33
|
-
const versionLine =
|
|
146
|
+
const versionLine = headerLines.find((l) => /release atual|current release/i.test(l));
|
|
34
147
|
const versionMatch = versionLine?.match(/v(\d+\.\d+\.\d+)/);
|
|
35
|
-
const version = versionMatch ? `v${versionMatch[1]}` :
|
|
36
|
-
|
|
37
|
-
// Extract pending status from "**Pendente:**" line
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
148
|
+
const version = versionMatch ? `v${versionMatch[1]}` : 'v?.?.?';
|
|
149
|
+
|
|
150
|
+
// Extract pending status from "**Pendente:**" line — must be line-anchored
|
|
151
|
+
// to avoid matching inline mentions inside session summaries.
|
|
152
|
+
const pendenteLine = allLines.find((l) => /^\*\*pendente/i.test(l));
|
|
153
|
+
const status =
|
|
154
|
+
pendenteLine
|
|
155
|
+
?.replace(/.*\*\*pendente:?\*\*:?\s*/i, '')
|
|
156
|
+
.replace(/\s*$/, '')
|
|
157
|
+
.slice(0, 80) || '';
|
|
158
|
+
|
|
159
|
+
// Extract last session summary — allows "Sessao N:**", "Sessao N FINAL:**",
|
|
160
|
+
// "Sessao N (suffix):**". Picks the entry with the highest session number
|
|
161
|
+
// so updates at the file top are preferred over older entries.
|
|
162
|
+
const sessionRegex = /^\*\*Sessao (\d+)[^*]*:\*\*/i;
|
|
163
|
+
const sessionMatches = allLines
|
|
164
|
+
.map((l, idx) => {
|
|
165
|
+
const m = sessionRegex.exec(l);
|
|
166
|
+
return m ? { line: l, num: Number(m[1]), idx } : null;
|
|
167
|
+
})
|
|
168
|
+
.filter((x): x is { line: string; num: number; idx: number } => x !== null);
|
|
169
|
+
const latestSession = sessionMatches.sort((a, b) => b.num - a.num || a.idx - b.idx)[0];
|
|
170
|
+
const lastSession = latestSession?.line.replace(/\*\*/g, '').slice(0, 120) || '';
|
|
171
|
+
|
|
172
|
+
// Extract next step from "**Proximo passo:**" or "**Next:**" line — must be
|
|
173
|
+
// line-anchored to avoid matching inline mentions inside session summaries.
|
|
174
|
+
const nextLine = allLines.find((l) => /^\*\*proximo passo/i.test(l) || /^\*\*next/i.test(l));
|
|
175
|
+
const next =
|
|
176
|
+
nextLine
|
|
177
|
+
?.replace(/.*\*\*(proximo passo|next):?\*\*:?\s*/i, '')
|
|
178
|
+
.replace(/\s*$/, '')
|
|
179
|
+
.slice(0, 120) || 'see BACKLOG.md';
|
|
180
|
+
|
|
181
|
+
log(
|
|
182
|
+
HOOK,
|
|
183
|
+
`BACKLOG.md fallback parsed: version=${version} next=${next.slice(0, 50)} lastSession=${lastSession.slice(0, 50)}`
|
|
184
|
+
);
|
|
185
|
+
return { version, next, status, lastSession };
|
|
53
186
|
} catch (err) {
|
|
54
187
|
log(HOOK, `BACKLOG read failed: ${err}`);
|
|
55
|
-
return { version:
|
|
188
|
+
return { version: 'v?.?.?', next: 'see BACKLOG.md', status: '', lastSession: '' };
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Read session context: try project-state.yaml first (RFC-033 Phase 1),
|
|
194
|
+
* fall back to legacy BACKLOG.md parsing on any failure.
|
|
195
|
+
* Return shape is IDENTICAL to the original function.
|
|
196
|
+
*/
|
|
197
|
+
function getBacklogSummary(): BacklogSummary {
|
|
198
|
+
const structured = readProjectState(PROJECT_STATE_PATH);
|
|
199
|
+
if (structured !== null) {
|
|
200
|
+
log(
|
|
201
|
+
HOOK,
|
|
202
|
+
`project-state.yaml: version=${structured.version} next=${structured.next.slice(0, 50)}`
|
|
203
|
+
);
|
|
204
|
+
return structured;
|
|
205
|
+
}
|
|
206
|
+
log(HOOK, 'project-state.yaml absent/invalid — using BACKLOG.md fallback');
|
|
207
|
+
return readBacklogFallback();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
type TierInfo = { onboardingNeeded: true } | { tier: number; source: string } | null;
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Check the project tier from vault YAML config.
|
|
214
|
+
* Returns null on any unexpected error (tier omitted from output).
|
|
215
|
+
* Note: API call removed (I-5) — the endpoint needs a real project UUID which the hook doesn't have.
|
|
216
|
+
*/
|
|
217
|
+
async function checkTier(): Promise<TierInfo> {
|
|
218
|
+
try {
|
|
219
|
+
// Use module-level VAULT_CONFIG_PATH constant (IMP-003)
|
|
220
|
+
if (!existsSync(VAULT_CONFIG_PATH)) {
|
|
221
|
+
return { onboardingNeeded: true };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const content = readFileSync(VAULT_CONFIG_PATH, 'utf-8');
|
|
225
|
+
const config = parseYaml(content) as Record<string, unknown>;
|
|
226
|
+
const project = config?.project as Record<string, unknown> | undefined;
|
|
227
|
+
const tier = project?.tier as number | undefined;
|
|
228
|
+
if (tier !== undefined) {
|
|
229
|
+
return { tier, source: 'config' };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return null;
|
|
233
|
+
} catch (err) {
|
|
234
|
+
log(HOOK, `Tier check failed: ${err}`);
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Format tier info as a segment string for the ORCHESTRATOR-DATA line.
|
|
241
|
+
*/
|
|
242
|
+
export function formatTierSegment(info: TierInfo): string {
|
|
243
|
+
if (info === null) return '';
|
|
244
|
+
if ('onboardingNeeded' in info) return ' | onboarding_needed=true';
|
|
245
|
+
return ` | tier=${info.tier} (${info.source})`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Build [CAPABILITY-MATRIX] + [MEMORY-CONTEXT] lines for injection.
|
|
250
|
+
* Fail-open: returns empty string on any error.
|
|
251
|
+
*/
|
|
252
|
+
function buildCapabilityMatrixContext(): string {
|
|
253
|
+
try {
|
|
254
|
+
let index = readCapabilitiesFromVault(VAULT_CONFIG_PATH);
|
|
255
|
+
const cachedMtime = index?.installedPluginsMtime ?? '';
|
|
256
|
+
|
|
257
|
+
if (!index || needsRescan(INSTALLED_PLUGINS_PATH, cachedMtime)) {
|
|
258
|
+
log(HOOK, 'Capability rescan triggered');
|
|
259
|
+
index = buildCapabilityIndex({
|
|
260
|
+
installedPluginsPath: INSTALLED_PLUGINS_PATH,
|
|
261
|
+
agentsDir: AGENTS_DIR,
|
|
262
|
+
vaultConfigPath: VAULT_CONFIG_PATH,
|
|
263
|
+
profilesPath: PROFILES_PATH,
|
|
264
|
+
});
|
|
265
|
+
writeCapabilitiesToVault(VAULT_CONFIG_PATH, index);
|
|
266
|
+
} else {
|
|
267
|
+
// Agents are always scanned fresh (fast, <10ms)
|
|
268
|
+
index.agents = scanAgentDefinitions(AGENTS_DIR);
|
|
269
|
+
log(HOOK, 'Using cached capability index');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const matrixLine = formatCapabilityMatrix(index);
|
|
273
|
+
|
|
274
|
+
// Memory context
|
|
275
|
+
const memoryEntries = readMemoryIndex(MEMORY_INDEX_PATH);
|
|
276
|
+
let memoryLine = '';
|
|
277
|
+
if (memoryEntries) {
|
|
278
|
+
const filtered = filterRelevantMemories(memoryEntries);
|
|
279
|
+
memoryLine = formatMemoryContext(filtered);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return [matrixLine, memoryLine].filter(Boolean).join('\n');
|
|
283
|
+
} catch (err) {
|
|
284
|
+
log(HOOK, `Capability matrix failed: ${err}`);
|
|
285
|
+
return '';
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Check sprint mailbox inbox for pending tasks (RFC-027 Sprint 4).
|
|
291
|
+
* Only runs when SPRINT_ID and PANE_ROLE are set (sprint pane sessions).
|
|
292
|
+
* Returns formatted [ACTION-REQUIRED] lines or empty string.
|
|
293
|
+
*/
|
|
294
|
+
function checkSprintInbox(): string {
|
|
295
|
+
const sprintId = process.env.SPRINT_ID;
|
|
296
|
+
const paneRole = process.env.PANE_ROLE;
|
|
297
|
+
|
|
298
|
+
if (!sprintId || !paneRole) return '';
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
const inboxDir = join('/tmp', sprintId, 'mailbox', paneRole, 'inbox');
|
|
302
|
+
if (!existsSync(inboxDir)) return '';
|
|
303
|
+
|
|
304
|
+
const files = readdirSync(inboxDir).filter((f) => f.endsWith('.json'));
|
|
305
|
+
if (files.length === 0) return '';
|
|
306
|
+
|
|
307
|
+
const pendingTasks: string[] = [];
|
|
308
|
+
const maxShow = 5;
|
|
309
|
+
|
|
310
|
+
for (const file of files.slice(0, maxShow)) {
|
|
311
|
+
try {
|
|
312
|
+
const content = readFileSync(join(inboxDir, file), 'utf-8');
|
|
313
|
+
const envelope = JSON.parse(content) as Record<string, unknown>;
|
|
314
|
+
const body = envelope.body as Record<string, unknown> | undefined;
|
|
315
|
+
if (body?.type) {
|
|
316
|
+
pendingTasks.push(
|
|
317
|
+
` → ${body.type}: ${body.title || body.summary || body.originalTaskId || '(no title)'}`
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
} catch {
|
|
321
|
+
// skip malformed files
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (pendingTasks.length === 0) return '';
|
|
326
|
+
|
|
327
|
+
const extra = files.length > maxShow ? `\n (+ ${files.length - maxShow} more)` : '';
|
|
328
|
+
return [
|
|
329
|
+
`[ACTION-REQUIRED] ${files.length} pending task(s) in mailbox:`,
|
|
330
|
+
...pendingTasks,
|
|
331
|
+
extra,
|
|
332
|
+
`[ACTION-REQUIRED] Execute NOW. Send task_complete when done.`,
|
|
333
|
+
]
|
|
334
|
+
.filter(Boolean)
|
|
335
|
+
.join('\n');
|
|
336
|
+
} catch (err) {
|
|
337
|
+
log(HOOK, `checkSprintInbox failed: ${err}`);
|
|
338
|
+
return '';
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ─── Graph Context (RFC-021 T-G4-06) ──────────────────────────────────────────
|
|
343
|
+
|
|
344
|
+
type GraphFetcher = (path: string, token: string, timeoutMs?: number) => Promise<unknown>;
|
|
345
|
+
|
|
346
|
+
interface GraphStatsResponse {
|
|
347
|
+
nodeCount?: number;
|
|
348
|
+
edgeCount?: number;
|
|
349
|
+
hotPaths?: string[];
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
interface AtRiskResponse {
|
|
353
|
+
atRisk?: string[];
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Fetch graph stats and at-risk components from the GraphRAG API.
|
|
358
|
+
* Uses Promise.all for parallel calls with a 2s AbortController timeout.
|
|
359
|
+
* Fail-open: returns "" on any error so session-start never blocks.
|
|
360
|
+
*
|
|
361
|
+
* @param token - JWT token (null → skip API calls, return "")
|
|
362
|
+
* @param fetcher - injectable fetch function (default: real HTTP via api-client pattern)
|
|
363
|
+
*/
|
|
364
|
+
export async function getGraphContext(
|
|
365
|
+
token: string | null,
|
|
366
|
+
fetcher?: GraphFetcher
|
|
367
|
+
): Promise<string> {
|
|
368
|
+
if (token === null || token === '') return '';
|
|
369
|
+
|
|
370
|
+
const defaultFetcher: GraphFetcher = async (path, tok, timeoutMs = GRAPHRAG_TIMEOUT_MS) => {
|
|
371
|
+
const resp = await fetch(`http://localhost:3000${path}`, {
|
|
372
|
+
headers: { Authorization: `Bearer ${tok}` },
|
|
373
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
374
|
+
});
|
|
375
|
+
if (!resp.ok) return null;
|
|
376
|
+
return resp.json() as Promise<unknown>;
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
const fetch_ = fetcher ?? defaultFetcher;
|
|
380
|
+
|
|
381
|
+
try {
|
|
382
|
+
const [statsRaw, atRiskRaw] = await Promise.all([
|
|
383
|
+
fetch_('/api/v1/graphrag/stats', token, GRAPHRAG_TIMEOUT_MS),
|
|
384
|
+
fetch_('/api/v1/graphrag/at-risk', token, GRAPHRAG_TIMEOUT_MS),
|
|
385
|
+
]);
|
|
386
|
+
|
|
387
|
+
const stats = statsRaw as GraphStatsResponse | null;
|
|
388
|
+
if (stats?.nodeCount === undefined) return '';
|
|
389
|
+
|
|
390
|
+
const atRiskData = atRiskRaw as AtRiskResponse | null;
|
|
391
|
+
const components = String(stats.nodeCount);
|
|
392
|
+
const edges = String(stats.edgeCount ?? 0);
|
|
393
|
+
const hotPaths = (stats.hotPaths ?? []).join(',');
|
|
394
|
+
const atRisk = (atRiskData?.atRisk ?? []).join(',');
|
|
395
|
+
|
|
396
|
+
return `[GRAPH-CONTEXT] components=${components} edges=${edges} hot_paths=[${hotPaths}] at_risk=[${atRisk}]`;
|
|
397
|
+
} catch {
|
|
398
|
+
return '';
|
|
56
399
|
}
|
|
57
400
|
}
|
|
58
401
|
|
|
59
402
|
async function main(): Promise<void> {
|
|
60
|
-
log(HOOK,
|
|
403
|
+
log(HOOK, 'SessionStart hook triggered');
|
|
404
|
+
|
|
405
|
+
const sessionId = readSessionId();
|
|
406
|
+
const sessionSegment = sessionId ? ` | session=${sessionId}` : '';
|
|
407
|
+
if (sessionId) log(HOOK, `Claude session: ${sessionId}`);
|
|
61
408
|
|
|
62
409
|
const backlog = getBacklogSummary();
|
|
410
|
+
const tierInfo = await checkTier();
|
|
411
|
+
const tierSegment = formatTierSegment(tierInfo);
|
|
412
|
+
const capabilityContext = buildCapabilityMatrixContext();
|
|
63
413
|
const workflow = await getActiveWorkflow();
|
|
64
414
|
|
|
415
|
+
// Sprint inbox check (RFC-027 S4 — only for sprint panes)
|
|
416
|
+
const sprintInboxContext = checkSprintInbox();
|
|
417
|
+
|
|
418
|
+
// Graph context (RFC-021 T-G4-06) — fail-open, 2s timeout
|
|
419
|
+
const token = await getToken();
|
|
420
|
+
const graphContext = await getGraphContext(token);
|
|
421
|
+
|
|
65
422
|
if (!workflow) {
|
|
66
|
-
log(HOOK,
|
|
423
|
+
log(HOOK, 'No active workflow, injecting greeting data');
|
|
67
424
|
outputContext(
|
|
68
|
-
|
|
425
|
+
'SessionStart',
|
|
69
426
|
[
|
|
70
|
-
`[ORCHESTRATOR-DATA] version=${backlog.version} | workflow=none | next=${backlog.next}`,
|
|
427
|
+
`[ORCHESTRATOR-DATA] version=${backlog.version}${sessionSegment} | workflow=none${tierSegment} | next=${backlog.next}`,
|
|
428
|
+
backlog.lastSession ? `[LAST-SESSION] ${backlog.lastSession}` : '',
|
|
429
|
+
backlog.status ? `[PENDENTE] ${backlog.status}` : '',
|
|
71
430
|
`[ORCHESTRATOR] Greet the user via AskUserQuestion using the data above (CLAUDE.md Rule #1).`,
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
431
|
+
'Do NOT invoke /orchestrator skill for the greeting — use AskUserQuestion directly.',
|
|
432
|
+
'Invoke /orchestrator skill only when the user chooses to start a workflow.',
|
|
433
|
+
capabilityContext,
|
|
434
|
+
graphContext,
|
|
435
|
+
sprintInboxContext,
|
|
436
|
+
]
|
|
437
|
+
.filter(Boolean)
|
|
438
|
+
.join('\n')
|
|
75
439
|
);
|
|
76
440
|
return;
|
|
77
441
|
}
|
|
78
442
|
|
|
79
443
|
// Active workflow — inject state + backlog
|
|
80
444
|
const action = await getNextAction(workflow.id);
|
|
81
|
-
const actionHint =
|
|
82
|
-
|
|
83
|
-
|
|
445
|
+
const actionHint =
|
|
446
|
+
action?.hasAction && action.pendingAction
|
|
447
|
+
? ` | pending=${action.pendingAction.agent} (${action.pendingAction.status})`
|
|
448
|
+
: '';
|
|
84
449
|
|
|
85
450
|
log(HOOK, `Injecting workflow context (wf=${workflow.id} phase=${workflow.currentPhase})`);
|
|
86
451
|
outputContext(
|
|
87
|
-
|
|
452
|
+
'SessionStart',
|
|
88
453
|
[
|
|
89
|
-
`[ORCHESTRATOR-DATA] version=${backlog.version} | workflow=${workflow.id} | type=${workflow.type} | phase=${workflow.currentPhase} | status=${workflow.status}${actionHint}`,
|
|
454
|
+
`[ORCHESTRATOR-DATA] version=${backlog.version}${sessionSegment} | workflow=${workflow.id} | type=${workflow.type} | phase=${workflow.currentPhase} | status=${workflow.status}${actionHint}${tierSegment}`,
|
|
90
455
|
`[ORCHESTRATOR] Active workflow detected. Greet the user via AskUserQuestion showing workflow state (CLAUDE.md Rule #1).`,
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
456
|
+
'Do NOT invoke /orchestrator skill for the greeting — use AskUserQuestion directly.',
|
|
457
|
+
'Invoke /orchestrator skill when the user chooses to continue the workflow.',
|
|
458
|
+
capabilityContext,
|
|
459
|
+
graphContext,
|
|
460
|
+
]
|
|
461
|
+
.filter(Boolean)
|
|
462
|
+
.join('\n')
|
|
94
463
|
);
|
|
95
464
|
}
|
|
96
465
|
|
package/hooks/sprint-registry.ts
CHANGED
|
@@ -39,10 +39,9 @@ async function main(): Promise<void> {
|
|
|
39
39
|
const stdin = await readStdin();
|
|
40
40
|
const sessionId = (stdin.session_id as string) ?? undefined;
|
|
41
41
|
|
|
42
|
-
|
|
43
42
|
// Read required env vars
|
|
44
|
-
const sprintId = process.env
|
|
45
|
-
const paneRole = process.env
|
|
43
|
+
const sprintId = process.env.SPRINT_ID;
|
|
44
|
+
const paneRole = process.env.PANE_ROLE;
|
|
46
45
|
|
|
47
46
|
if (!sprintId || !paneRole) {
|
|
48
47
|
log(`Skipping: SPRINT_ID=${sprintId ?? 'unset'} PANE_ROLE=${paneRole ?? 'unset'}`);
|
|
@@ -52,7 +51,7 @@ async function main(): Promise<void> {
|
|
|
52
51
|
const registryDir = `/tmp/${sprintId}/registry`;
|
|
53
52
|
const entryPath = join(registryDir, `${paneRole}.json`);
|
|
54
53
|
|
|
55
|
-
const workflowId = process.env
|
|
54
|
+
const workflowId = process.env.WORKFLOW_ID;
|
|
56
55
|
|
|
57
56
|
const entry: Record<string, unknown> = {
|
|
58
57
|
role: paneRole,
|
|
@@ -73,7 +72,9 @@ async function main(): Promise<void> {
|
|
|
73
72
|
try {
|
|
74
73
|
mkdirSync(registryDir, { recursive: true });
|
|
75
74
|
writeFileSync(entryPath, JSON.stringify(entry, null, 2), 'utf-8');
|
|
76
|
-
log(
|
|
75
|
+
log(
|
|
76
|
+
`Registered ${paneRole} for sprint ${sprintId} (pid=${process.pid}, session=${sessionId ?? 'unknown'})`
|
|
77
|
+
);
|
|
77
78
|
} catch (err) {
|
|
78
79
|
// Log but do not fail — registration is best-effort
|
|
79
80
|
log(`Registration failed: ${err}`);
|
package/hooks/subagent-start.ts
CHANGED
|
@@ -10,17 +10,18 @@ import {
|
|
|
10
10
|
getField,
|
|
11
11
|
getActiveWorkflow,
|
|
12
12
|
startInvocation,
|
|
13
|
-
|
|
13
|
+
writeInvocationState,
|
|
14
|
+
} from './lib/api-client.js';
|
|
14
15
|
|
|
15
|
-
const HOOK =
|
|
16
|
+
const HOOK = 'SUBAGENT-START';
|
|
16
17
|
|
|
17
18
|
const PHASE_MAP: Record<string, string> = {
|
|
18
|
-
specifier:
|
|
19
|
-
planner:
|
|
20
|
-
|
|
21
|
-
implementer:
|
|
22
|
-
researcher:
|
|
23
|
-
reviewer:
|
|
19
|
+
specifier: 'specify',
|
|
20
|
+
planner: 'plan',
|
|
21
|
+
'task-generator': 'tasks',
|
|
22
|
+
implementer: 'implement',
|
|
23
|
+
researcher: 'research',
|
|
24
|
+
reviewer: 'review',
|
|
24
25
|
};
|
|
25
26
|
|
|
26
27
|
async function main(): Promise<void> {
|
|
@@ -28,10 +29,10 @@ async function main(): Promise<void> {
|
|
|
28
29
|
log(HOOK, `SubagentStart triggered`);
|
|
29
30
|
|
|
30
31
|
const agentType =
|
|
31
|
-
getField(stdin,
|
|
32
|
-
getField(stdin,
|
|
33
|
-
getField(stdin,
|
|
34
|
-
|
|
32
|
+
getField(stdin, 'tool_input.subagent_type') ||
|
|
33
|
+
getField(stdin, 'subagent_type') ||
|
|
34
|
+
getField(stdin, 'tool_input.type') ||
|
|
35
|
+
'unknown';
|
|
35
36
|
|
|
36
37
|
const phase = PHASE_MAP[agentType] || agentType;
|
|
37
38
|
|
|
@@ -39,12 +40,23 @@ async function main(): Promise<void> {
|
|
|
39
40
|
|
|
40
41
|
const workflow = await getActiveWorkflow();
|
|
41
42
|
if (!workflow) {
|
|
42
|
-
log(HOOK,
|
|
43
|
+
log(HOOK, 'No active workflow, skipping');
|
|
43
44
|
return;
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
await startInvocation(agentType, workflow.id, phase);
|
|
47
|
-
|
|
47
|
+
const invocationId = await startInvocation(agentType, workflow.id, phase);
|
|
48
|
+
if (invocationId) {
|
|
49
|
+
const written = writeInvocationState(workflow.id, agentType, invocationId);
|
|
50
|
+
log(
|
|
51
|
+
HOOK,
|
|
52
|
+
`START: agent=${agentType} phase=${phase} workflow=${workflow.id} invocationId=${invocationId} stateWritten=${written}`
|
|
53
|
+
);
|
|
54
|
+
} else {
|
|
55
|
+
log(
|
|
56
|
+
HOOK,
|
|
57
|
+
`START: agent=${agentType} phase=${phase} workflow=${workflow.id} — startInvocation returned null (POST failed or no token)`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
48
60
|
}
|
|
49
61
|
|
|
50
62
|
main().catch(() => process.exit(0));
|