@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.
@@ -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 BACKLOG.md";
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 { version: "v?.?.?", next: "see BACKLOG.md", status: "", lastSession: "" };
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
- const token = await getToken();
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
@@ -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) || 'see BACKLOG.md';
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 { version: 'v?.?.?', next: 'see BACKLOG.md', status: '', lastSession: '' };
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
- * / getGraphContext): returns "" on any error — a failed project-list
376
- * fetch must never block or crash the hook.
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
- // Graph context (RFC-021 T-G4-06) — fail-open, 2s timeout. RFC-040 TD-206
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
- const token = await getToken();
597
- const graphContext = await getGraphContext(token);
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)
@@ -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());
@@ -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. Guide main conversation — NO ping-pong, main IS the orchestrator
86
- const parts: string[] = [
87
- `[ORCHESTRATOR] Agent '${agentType}' completed for workflow ${workflow.id}.`,
88
- ];
89
-
90
- if (commitHash) {
91
- parts.push(`Checkpoint: ${commitHash}.`);
92
- }
93
-
94
- parts.push('Proceed to gate check via AskUserQuestion before advancing phase.');
95
-
96
- outputContext('SubagentStop', parts.join(' '));
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": "mcp__orchestrator-tools__advancePhase",
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": "mcp__orchestrator-tools__advancePhase",
67
+ "matcher": "mcp__(plugin_orchestrator_)?orchestrator-tools__advancePhase",
53
68
  "hooks": [{ "type": "command", "command": "hooks/post-implement.mjs", "timeout": 15000 }]
54
69
  }
55
70
  ],