@orchestrator-claude/definitions 3.34.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/agents/ciclo-engineer.md +267 -0
- package/agents/orchestrator.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 +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 +54 -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
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 {
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { createRequire as __createRequireShim } from 'node:module'; const require = __createRequireShim(import.meta.url);
|
|
2
|
+
|
|
3
|
+
// hooks/lib/api-client.ts
|
|
4
|
+
import { readFileSync, writeFileSync, mkdirSync, statSync, unlinkSync, readdirSync } from "node:fs";
|
|
5
|
+
import { join, dirname } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
var __dirname = dirname(__filename);
|
|
9
|
+
var PROJECT_ROOT = join(__dirname, "..", "..", "..");
|
|
10
|
+
var STATE_DIR = join(PROJECT_ROOT, ".orchestrator", ".state");
|
|
11
|
+
var JWT_CACHE = join(STATE_DIR, "jwt-token");
|
|
12
|
+
var DEBUG_LOG = join(STATE_DIR, "hook-debug.log");
|
|
13
|
+
mkdirSync(STATE_DIR, { recursive: true });
|
|
14
|
+
var API_URL = process.env.ORCHESTRATOR_API_URL || "http://localhost:3001";
|
|
15
|
+
var AUTH_EMAIL = process.env.ORCHESTRATOR_ADMIN_EMAIL || process.env.ORCHESTRATOR_AUTH_EMAIL || "system@orchestrator.local";
|
|
16
|
+
var AUTH_PASSWORD = process.env.ORCHESTRATOR_ADMIN_PASSWORD || process.env.ORCHESTRATOR_AUTH_PASSWORD || "system123";
|
|
17
|
+
var PROJECT_ID = process.env.ORCHESTRATOR_PROJECT_ID || "";
|
|
18
|
+
function log(prefix, msg) {
|
|
19
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "Z");
|
|
20
|
+
try {
|
|
21
|
+
const line = `[${ts}] ${prefix}: ${msg}
|
|
22
|
+
`;
|
|
23
|
+
writeFileSync(DEBUG_LOG, line, { flag: "a" });
|
|
24
|
+
} catch {
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
var LAST_COMPLETED_FILE = join(STATE_DIR, "last-completed-workflow.json");
|
|
28
|
+
var LAST_COMPLETED_TTL_MS = 60 * 60 * 1e3;
|
|
29
|
+
var INVOCATIONS_DIR = join(STATE_DIR, "invocations");
|
|
30
|
+
var INVOCATION_STATE_TTL_MS = 4 * 60 * 60 * 1e3;
|
|
31
|
+
var DISPATCH_INTENT_DIR = join(STATE_DIR, "dispatch-intent");
|
|
32
|
+
async function readStdin() {
|
|
33
|
+
const chunks = [];
|
|
34
|
+
for await (const chunk of process.stdin) {
|
|
35
|
+
chunks.push(chunk);
|
|
36
|
+
}
|
|
37
|
+
const raw = Buffer.concat(chunks).toString("utf-8").trim();
|
|
38
|
+
if (!raw) return {};
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(raw);
|
|
41
|
+
} catch {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function getField(obj, path) {
|
|
46
|
+
const parts = path.split(".");
|
|
47
|
+
let current = obj;
|
|
48
|
+
for (const part of parts) {
|
|
49
|
+
if (current == null || typeof current !== "object") return "";
|
|
50
|
+
current = current[part];
|
|
51
|
+
}
|
|
52
|
+
return current != null ? String(current) : "";
|
|
53
|
+
}
|
|
54
|
+
function outputPreToolUse(opts) {
|
|
55
|
+
const output = {
|
|
56
|
+
hookSpecificOutput: {
|
|
57
|
+
hookEventName: "PreToolUse",
|
|
58
|
+
permissionDecision: opts.decision,
|
|
59
|
+
...opts.reason && { permissionDecisionReason: opts.reason },
|
|
60
|
+
...opts.context && { additionalContext: opts.context }
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
console.log(JSON.stringify(output));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// hooks/verification-before-completion.ts
|
|
67
|
+
var HOOK = "VERIFICATION-REMINDER";
|
|
68
|
+
var defaultDeps = {
|
|
69
|
+
readStdin,
|
|
70
|
+
output: outputPreToolUse,
|
|
71
|
+
log
|
|
72
|
+
};
|
|
73
|
+
function commandMatchesGitOp(command) {
|
|
74
|
+
return /git\s+(commit|push)/.test(command);
|
|
75
|
+
}
|
|
76
|
+
async function mainWithDeps(deps) {
|
|
77
|
+
const { readStdin: readStdin2, output, log: log2 } = deps;
|
|
78
|
+
const stdin = await readStdin2();
|
|
79
|
+
const command = getField(stdin, "tool_input.command") || getField(stdin, "command") || "";
|
|
80
|
+
if (!commandMatchesGitOp(command)) {
|
|
81
|
+
log2(HOOK, `PASS (no git op): ${command.slice(0, 60)}`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
log2(HOOK, `REMIND git op detected: ${command.slice(0, 80)}`);
|
|
85
|
+
output({
|
|
86
|
+
decision: "allow",
|
|
87
|
+
context: [
|
|
88
|
+
"[VERIFICATION-REMINDER] You are about to run a git commit/push.",
|
|
89
|
+
"Before proceeding, confirm:",
|
|
90
|
+
" 1. Tests pass \u2014 run the test command and paste the output.",
|
|
91
|
+
" 2. Lint clean \u2014 run the lint command and confirm 0 errors.",
|
|
92
|
+
" 3. Build succeeds \u2014 run the build command and confirm exit 0.",
|
|
93
|
+
" 4. All acceptance criteria have matching test evidence.",
|
|
94
|
+
"Evidence before claims. No shortcuts for verification."
|
|
95
|
+
].join("\n")
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
async function main() {
|
|
99
|
+
await mainWithDeps(defaultDeps);
|
|
100
|
+
}
|
|
101
|
+
main().catch(() => process.exit(0));
|
|
102
|
+
export {
|
|
103
|
+
commandMatchesGitOp,
|
|
104
|
+
mainWithDeps
|
|
105
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* verification-before-completion.ts — PreToolUse[Bash] hook
|
|
3
|
+
*
|
|
4
|
+
* Injects a verification reminder when the model is about to run
|
|
5
|
+
* `git commit` or `git push`. Does NOT block — only adds context.
|
|
6
|
+
*
|
|
7
|
+
* Principle: Evidence before claims, always.
|
|
8
|
+
* Source: verification-before-completion skill (superpowers 5.0.7)
|
|
9
|
+
*
|
|
10
|
+
* Flow:
|
|
11
|
+
* 1. readStdin() → extract bash command
|
|
12
|
+
* 2. commandMatchesGitOp() → check for commit/push
|
|
13
|
+
* 3. If match → output allow + context reminder
|
|
14
|
+
* 4. If no match → silent pass-through (no output)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { log, readStdin, getField, outputPreToolUse } from './lib/api-client.js';
|
|
18
|
+
|
|
19
|
+
const HOOK = 'VERIFICATION-REMINDER';
|
|
20
|
+
|
|
21
|
+
// ─────────────────────────────────────────────────────────────
|
|
22
|
+
// Dependency injection interface (enables testing without IO)
|
|
23
|
+
// ─────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
export interface HookDeps {
|
|
26
|
+
readStdin: () => Promise<Record<string, unknown>>;
|
|
27
|
+
output: (opts: { decision: 'allow' | 'deny'; reason?: string; context?: string }) => void;
|
|
28
|
+
log: (prefix: string, msg: string) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const defaultDeps: HookDeps = {
|
|
32
|
+
readStdin,
|
|
33
|
+
output: outputPreToolUse,
|
|
34
|
+
log,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// ─────────────────────────────────────────────────────────────
|
|
38
|
+
// Pure helpers (exported for testing)
|
|
39
|
+
// ─────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Returns true when the bash command string contains a git commit or git push
|
|
43
|
+
* operation. Handles common variations: `git commit`, `git push`, multi-command
|
|
44
|
+
* pipelines (`&&`, `;`), and quoted forms.
|
|
45
|
+
*/
|
|
46
|
+
export function commandMatchesGitOp(command: string): boolean {
|
|
47
|
+
return /git\s+(commit|push)/.test(command);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ─────────────────────────────────────────────────────────────
|
|
51
|
+
// Core logic (exported for testing)
|
|
52
|
+
// ─────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
export async function mainWithDeps(deps: HookDeps): Promise<void> {
|
|
55
|
+
const { readStdin, output, log } = deps;
|
|
56
|
+
|
|
57
|
+
const stdin = await readStdin();
|
|
58
|
+
|
|
59
|
+
// Extract the bash command from the tool input envelope
|
|
60
|
+
const command =
|
|
61
|
+
(getField(stdin, 'tool_input.command') as string | undefined) ||
|
|
62
|
+
(getField(stdin, 'command') as string | undefined) ||
|
|
63
|
+
'';
|
|
64
|
+
|
|
65
|
+
if (!commandMatchesGitOp(command)) {
|
|
66
|
+
// Silent pass-through — not a git commit/push, nothing to do
|
|
67
|
+
log(HOOK, `PASS (no git op): ${command.slice(0, 60)}`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
log(HOOK, `REMIND git op detected: ${command.slice(0, 80)}`);
|
|
72
|
+
|
|
73
|
+
// Allow the command but inject a verification reminder as context.
|
|
74
|
+
// This hook NEVER blocks — it only nudges the model to verify first.
|
|
75
|
+
output({
|
|
76
|
+
decision: 'allow',
|
|
77
|
+
context: [
|
|
78
|
+
'[VERIFICATION-REMINDER] You are about to run a git commit/push.',
|
|
79
|
+
'Before proceeding, confirm:',
|
|
80
|
+
' 1. Tests pass — run the test command and paste the output.',
|
|
81
|
+
' 2. Lint clean — run the lint command and confirm 0 errors.',
|
|
82
|
+
' 3. Build succeeds — run the build command and confirm exit 0.',
|
|
83
|
+
' 4. All acceptance criteria have matching test evidence.',
|
|
84
|
+
'Evidence before claims. No shortcuts for verification.',
|
|
85
|
+
].join('\n'),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ─────────────────────────────────────────────────────────────
|
|
90
|
+
// Entry point (production use)
|
|
91
|
+
// ─────────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
async function main(): Promise<void> {
|
|
94
|
+
await mainWithDeps(defaultDeps);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
main().catch(() => process.exit(0));
|
|
@@ -17,23 +17,38 @@
|
|
|
17
17
|
],
|
|
18
18
|
"PreToolUse": [
|
|
19
19
|
{
|
|
20
|
-
"matcher": "
|
|
20
|
+
"matcher": "mcp__(plugin_orchestrator_)?orchestrator-tools__advancePhase",
|
|
21
21
|
"hooks": [{ "type": "command", "command": "hooks/gate-guardian.mjs", "timeout": 15000 }]
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
"matcher": "Write|Edit",
|
|
25
25
|
"hooks": [{ "type": "command", "command": "hooks/workflow-guard.mjs", "timeout": 10000 }]
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"matcher": "Bash",
|
|
29
|
+
"hooks": [
|
|
30
|
+
{ "type": "command", "command": "hooks/verification-before-completion.mjs", "timeout": 5000 },
|
|
31
|
+
{ "type": "command", "command": "hooks/agent-git-guard.mjs", "timeout": 5000 }
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"matcher": "Agent",
|
|
36
|
+
"hooks": [{ "type": "command", "command": "hooks/pre-agent-dispatch.mjs", "timeout": 5000 }]
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"matcher": "mcp__plugin_playwright_playwright__browser_take_screenshot|mcp__plugin_playwright_playwright__browser_pdf_save",
|
|
40
|
+
"hooks": [{ "type": "command", "command": "hooks/playwright-output-guard.mjs", "timeout": 5000 }]
|
|
26
41
|
}
|
|
27
42
|
],
|
|
28
43
|
"SubagentStart": [
|
|
29
44
|
{
|
|
30
|
-
"matcher": "implementer|specifier|planner|task-generator|reviewer|researcher",
|
|
45
|
+
"matcher": "(orchestrator:)?(implementer|specifier|planner|task-generator|reviewer|researcher)",
|
|
31
46
|
"hooks": [{ "type": "command", "command": "hooks/subagent-start.mjs", "timeout": 10000 }]
|
|
32
47
|
}
|
|
33
48
|
],
|
|
34
49
|
"SubagentStop": [
|
|
35
50
|
{
|
|
36
|
-
"matcher": "implementer|specifier|planner|task-generator|reviewer|researcher",
|
|
51
|
+
"matcher": "(orchestrator:)?(implementer|specifier|planner|task-generator|reviewer|researcher)",
|
|
37
52
|
"hooks": [{ "type": "command", "command": "hooks/subagent-stop.mjs", "timeout": 30000 }]
|
|
38
53
|
}
|
|
39
54
|
],
|
|
@@ -49,7 +64,7 @@
|
|
|
49
64
|
"hooks": [{ "type": "command", "command": "hooks/mailbox-listener.mjs", "timeout": 5000 }]
|
|
50
65
|
},
|
|
51
66
|
{
|
|
52
|
-
"matcher": "
|
|
67
|
+
"matcher": "mcp__(plugin_orchestrator_)?orchestrator-tools__advancePhase",
|
|
53
68
|
"hooks": [{ "type": "command", "command": "hooks/post-implement.mjs", "timeout": 15000 }]
|
|
54
69
|
}
|
|
55
70
|
],
|