@orchestrator-claude/definitions 3.32.1 → 3.33.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.
Files changed (87) hide show
  1. package/README.md +113 -0
  2. package/agents/debug-sidecar.md +1 -1
  3. package/agents/doc-sidecar.md +1 -1
  4. package/agents/test-sidecar.md +1 -1
  5. package/dist/cli.js +78 -0
  6. package/dist/cli.js.map +1 -1
  7. package/dist/commands/DiffCommand.js +1 -1
  8. package/dist/commands/DiffCommand.js.map +1 -1
  9. package/dist/commands/InstallCommand.d.ts +41 -0
  10. package/dist/commands/InstallCommand.d.ts.map +1 -0
  11. package/dist/commands/InstallCommand.js +253 -0
  12. package/dist/commands/InstallCommand.js.map +1 -0
  13. package/dist/http/ApiClient.d.ts +5 -0
  14. package/dist/http/ApiClient.d.ts.map +1 -1
  15. package/dist/http/ApiClient.js.map +1 -1
  16. package/dist/install/DevCheckoutDetector.d.ts +66 -0
  17. package/dist/install/DevCheckoutDetector.d.ts.map +1 -0
  18. package/dist/install/DevCheckoutDetector.js +150 -0
  19. package/dist/install/DevCheckoutDetector.js.map +1 -0
  20. package/dist/install/HooksWirer.d.ts +29 -0
  21. package/dist/install/HooksWirer.d.ts.map +1 -0
  22. package/dist/install/HooksWirer.js +80 -0
  23. package/dist/install/HooksWirer.js.map +1 -0
  24. package/dist/install/LocalDiffReporter.d.ts +25 -0
  25. package/dist/install/LocalDiffReporter.d.ts.map +1 -0
  26. package/dist/install/LocalDiffReporter.js +50 -0
  27. package/dist/install/LocalDiffReporter.js.map +1 -0
  28. package/dist/install/Materializer.d.ts +67 -0
  29. package/dist/install/Materializer.d.ts.map +1 -0
  30. package/dist/install/Materializer.js +196 -0
  31. package/dist/install/Materializer.js.map +1 -0
  32. package/dist/install/McpRegistrar.d.ts +63 -0
  33. package/dist/install/McpRegistrar.d.ts.map +1 -0
  34. package/dist/install/McpRegistrar.js +114 -0
  35. package/dist/install/McpRegistrar.js.map +1 -0
  36. package/dist/install/ProcessRunner.d.ts +25 -0
  37. package/dist/install/ProcessRunner.d.ts.map +1 -0
  38. package/dist/install/ProcessRunner.js +41 -0
  39. package/dist/install/ProcessRunner.js.map +1 -0
  40. package/dist/install/ProjectSettingsNeutralizer.d.ts +39 -0
  41. package/dist/install/ProjectSettingsNeutralizer.d.ts.map +1 -0
  42. package/dist/install/ProjectSettingsNeutralizer.js +100 -0
  43. package/dist/install/ProjectSettingsNeutralizer.js.map +1 -0
  44. package/dist/install/types.d.ts +52 -0
  45. package/dist/install/types.d.ts.map +1 -0
  46. package/dist/install/types.js +8 -0
  47. package/dist/install/types.js.map +1 -0
  48. package/dist/manifest/ManifestLoader.d.ts.map +1 -1
  49. package/dist/manifest/ManifestLoader.js +3 -2
  50. package/dist/manifest/ManifestLoader.js.map +1 -1
  51. package/dist/manifest/types.d.ts +3 -0
  52. package/dist/manifest/types.d.ts.map +1 -1
  53. package/dist/scripts/bundle-hooks.d.ts +39 -0
  54. package/dist/scripts/bundle-hooks.d.ts.map +1 -0
  55. package/dist/scripts/bundle-hooks.js +165 -0
  56. package/dist/scripts/bundle-hooks.js.map +1 -0
  57. package/dist/scripts/generate-manifest.d.ts +22 -3
  58. package/dist/scripts/generate-manifest.d.ts.map +1 -1
  59. package/dist/scripts/generate-manifest.js +78 -35
  60. package/dist/scripts/generate-manifest.js.map +1 -1
  61. package/hooks/dangling-guard.mjs +7426 -0
  62. package/hooks/dangling-guard.ts +223 -6
  63. package/hooks/gate-guardian.mjs +463 -0
  64. package/hooks/lib/additional-directories-sync.ts +295 -0
  65. package/hooks/lib/api-client.ts +388 -7
  66. package/hooks/lib/session-selection.ts +81 -0
  67. package/hooks/mailbox-listener.mjs +259 -0
  68. package/hooks/post-compact.mjs +7931 -0
  69. package/hooks/post-implement.mjs +238 -0
  70. package/hooks/session-start.mjs +8234 -0
  71. package/hooks/session-start.ts +264 -23
  72. package/hooks/sprint-registry.mjs +59 -0
  73. package/hooks/subagent-start.mjs +289 -0
  74. package/hooks/subagent-stop.mjs +7793 -0
  75. package/hooks/subagent-stop.ts +4 -2
  76. package/hooks/user-prompt.mjs +329 -0
  77. package/hooks/workflow-guard.mjs +7805 -0
  78. package/hooks/workflow-guard.ts +360 -119
  79. package/hooks-settings-template.json +63 -0
  80. package/manifest.json +54 -68
  81. package/mcp-servers.json +48 -0
  82. package/package.json +10 -4
  83. package/personas/auditor-compliance.md +104 -0
  84. package/personas/cfo-conservador.md +88 -0
  85. package/personas/designer-perfeccionista.md +104 -0
  86. package/personas/diretor-cetico-fmcg.md +80 -0
  87. package/personas/engenheiro-cetico-construcao.md +107 -0
@@ -14,6 +14,12 @@ import {
14
14
  getNextAction,
15
15
  outputContext,
16
16
  getToken,
17
+ allocateSessionNumber,
18
+ resolveHookProjectRoot,
19
+ getEffectiveProjectId,
20
+ setEffectiveProjectId,
21
+ listRegisteredProjects,
22
+ type AllocateSessionResult,
17
23
  } from './lib/api-client.js';
18
24
  import {
19
25
  needsRescan,
@@ -26,26 +32,45 @@ import {
26
32
  filterRelevantMemories,
27
33
  formatMemoryContext,
28
34
  } from './lib/capability-scanner.js';
35
+ import { readSessionSelection } from './lib/session-selection.js';
29
36
 
30
37
  const HOOK = 'SESSION-START';
31
38
  const __filename = fileURLToPath(import.meta.url);
32
39
  const __dirname = dirname(__filename);
33
40
  const GRAPHRAG_TIMEOUT_MS = 2000;
34
- const PROJECT_ROOT = join(__dirname, '..', '..');
41
+ // RFC-040 Operator Multi-Project — Slice 5 (C5), S358/S361: cwd-aware
42
+ // resolution (walks up for the .orchestrator sentinel) replaces the
43
+ // __dirname-hardcoded path. `null` means an unregistered cwd — deliberately
44
+ // NO __dirname fallback (would leak the Orchestrator's own vault into an
45
+ // unregistered project). All PROJECT_ROOT-derived paths below degrade to
46
+ // `null` and their consumers skip gracefully (AC-C5-2).
47
+ const PROJECT_ROOT = resolveHookProjectRoot(process.cwd());
48
+ // RFC-040 Operator Multi-Project — Slice 2C (C9), S358, TASK-C9-03
49
+ const PROJECT_ID = process.env.ORCHESTRATOR_PROJECT_ID ?? '';
35
50
 
36
51
  // ─── Capability Matrix paths (RFC-027 Sprint 2) ─────────
37
52
  const HOME = process.env.HOME ?? process.env.USERPROFILE ?? '/home/user';
38
53
  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
- );
54
+
55
+ // ─── 2-Phase Selection (RFC-040 TD-204 ⑤-C) ─────────────────────────────
56
+ // Feature-flagged, default OFF — the kill-switch for the whole phase-1
57
+ // prompt + persisted-selection-read path. With the flag unset/false,
58
+ // `selection` stays permanently undefined (byte-for-byte the pre-⑤-C
59
+ // single-phase behaviour tested by session-start-project-binding.test.ts).
60
+ const PROJECT_SELECTION_ENABLED = process.env.ORCHESTRATOR_ENABLE_PROJECT_SELECTION === 'true';
61
+ // Deliberately NOT under ~/.claude (TD-204 hazard boundary — this hook
62
+ // never reads/writes that tree) and NOT under PROJECT_ROOT (a selection
63
+ // must be persistable even when cwd has no .orchestrator sentinel yet —
64
+ // that is exactly the "no binding" state phase-1 prompts for).
65
+ const SESSION_SELECTION_STATE_DIR = join(HOME, '.orchestrator', 'session-selection');
66
+ const AGENTS_DIR = PROJECT_ROOT ? join(PROJECT_ROOT, '.claude', 'agents') : null;
67
+ const VAULT_CONFIG_PATH = PROJECT_ROOT
68
+ ? join(PROJECT_ROOT, 'project-guidelines', '.vault-config.yaml')
69
+ : null;
70
+ const PROFILES_PATH = PROJECT_ROOT ? join(PROJECT_ROOT, '.claude', 'plugin-profiles.yaml') : null;
71
+ const MEMORY_INDEX_PATH = PROJECT_ROOT
72
+ ? join(PROJECT_ROOT, '.claude', 'agent-memory', 'orchestrator', 'memory-index.yaml')
73
+ : null;
49
74
 
50
75
  /**
51
76
  * Read session_id from SessionStart stdin payload.
@@ -64,7 +89,9 @@ function readSessionId(): string | null {
64
89
 
65
90
  // ─── RFC-033 Phase 1: project-state.yaml reader ─────────────────────────────
66
91
 
67
- const PROJECT_STATE_PATH = join(PROJECT_ROOT, 'project-guidelines', 'project-state.yaml');
92
+ const PROJECT_STATE_PATH = PROJECT_ROOT
93
+ ? join(PROJECT_ROOT, 'project-guidelines', 'project-state.yaml')
94
+ : null;
68
95
 
69
96
  interface PendingCounts {
70
97
  critical: number;
@@ -136,6 +163,11 @@ export function readProjectState(filePath: string): BacklogSummary | null {
136
163
  */
137
164
  function readBacklogFallback(): BacklogSummary {
138
165
  try {
166
+ if (PROJECT_ROOT === null) {
167
+ throw new Error(
168
+ 'PROJECT_ROOT unresolved (unregistered cwd) — no .orchestrator sentinel found'
169
+ );
170
+ }
139
171
  const backlogPath = join(PROJECT_ROOT, 'project-guidelines', 'BACKLOG.md');
140
172
  const content = readFileSync(backlogPath, 'utf-8');
141
173
  const allLines = content.split('\n');
@@ -195,13 +227,15 @@ function readBacklogFallback(): BacklogSummary {
195
227
  * Return shape is IDENTICAL to the original function.
196
228
  */
197
229
  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;
230
+ if (PROJECT_STATE_PATH !== null) {
231
+ const structured = readProjectState(PROJECT_STATE_PATH);
232
+ if (structured !== null) {
233
+ log(
234
+ HOOK,
235
+ `project-state.yaml: version=${structured.version} next=${structured.next.slice(0, 50)}`
236
+ );
237
+ return structured;
238
+ }
205
239
  }
206
240
  log(HOOK, 'project-state.yaml absent/invalid — using BACKLOG.md fallback');
207
241
  return readBacklogFallback();
@@ -216,6 +250,11 @@ type TierInfo = { onboardingNeeded: true } | { tier: number; source: string } |
216
250
  */
217
251
  async function checkTier(): Promise<TierInfo> {
218
252
  try {
253
+ // RFC-040 C5: unregistered cwd — no vault to check tier against, skip.
254
+ if (VAULT_CONFIG_PATH === null) {
255
+ log(HOOK, 'checkTier: PROJECT_ROOT unresolved (unregistered cwd) — skipping tier check');
256
+ return null;
257
+ }
219
258
  // Use module-level VAULT_CONFIG_PATH constant (IMP-003)
220
259
  if (!existsSync(VAULT_CONFIG_PATH)) {
221
260
  return { onboardingNeeded: true };
@@ -245,12 +284,138 @@ export function formatTierSegment(info: TierInfo): string {
245
284
  return ` | tier=${info.tier} (${info.source})`;
246
285
  }
247
286
 
287
+ /**
288
+ * Format the result of allocateSessionNumber() as a segment string for the
289
+ * ORCHESTRATOR-DATA line. Fail-open: returns "" when allocation failed
290
+ * (API down, no token, non-2xx) — RFC-040 Operator Multi-Project, Slice 2C
291
+ * (C9), S358, TASK-C9-03.
292
+ */
293
+ export function formatSessionNumberSegment(result: AllocateSessionResult | null): string {
294
+ if (result === null) return '';
295
+ return ` | session_number=${result.number}`;
296
+ }
297
+
298
+ // ─── Session Project Binding Precedence (RFC-040 TD-204 §⑥-c) ──────────────
299
+ // Slice 5 (C5) resolves a project ROOT for filesystem access
300
+ // (resolveHookProjectRoot(cwd), walk-up for the .orchestrator sentinel).
301
+ // This primitive instead resolves the project IDENTITY (projectId) a
302
+ // session is BOUND to, applying a strict first-match precedence:
303
+ // 1. env-override (ORCHESTRATOR_PROJECT_ID) — an IMMUTABLE pin. If set,
304
+ // the session is DEFINITIVELY that project; no other signal may
305
+ // override it.
306
+ // 2. explicit selection — forward-compat for Slice 5-C (⑤-C, NOT YET
307
+ // IMPLEMENTED). `selection` is always undefined until ⑤-C lands, so
308
+ // this tier is dormant-but-tested (mirrors the dormant-but-tested
309
+ // branches of decideWorkflowGuardAction in workflow-guard.ts, TD-204
310
+ // ⑤-A).
311
+ // 3. cwd-hint — the project resolved lazily from cwd via
312
+ // ensureProjectIdResolved/getEffectiveProjectId() (RFC-040 C5/C9).
313
+ // 4. none — no signal available; caller must force selection/registration.
314
+ // The `pinned` flag drives formatProjectBindingSegment's hard surfacing
315
+ // requirement below: an env-override pin is NEVER a silent surprise.
316
+
317
+ export type SessionProjectBindingSource = 'env-override' | 'selection' | 'cwd-hint' | 'none';
318
+
319
+ export interface SessionProjectBinding {
320
+ projectId: string;
321
+ source: SessionProjectBindingSource;
322
+ pinned: boolean;
323
+ }
324
+
325
+ export interface SessionProjectBindingInput {
326
+ envOverride: string;
327
+ /** Explicit project selection (RFC-040 Slice 5-C, ⑤-C — NOT YET IMPLEMENTED). Always undefined until ⑤-C lands. */
328
+ selection?: string;
329
+ cwdHintProjectId: string;
330
+ }
331
+
332
+ /**
333
+ * Pure precedence resolver for session project binding (RFC-040 TD-204
334
+ * ⑥-c). Zero fs/API calls — takes only injected inputs, so every tier
335
+ * (including the dormant `selection` tier) is independently testable.
336
+ */
337
+ export function resolveSessionProjectBinding(
338
+ input: SessionProjectBindingInput
339
+ ): SessionProjectBinding {
340
+ const { envOverride, selection, cwdHintProjectId } = input;
341
+
342
+ if (envOverride) {
343
+ return { projectId: envOverride, source: 'env-override', pinned: true };
344
+ }
345
+ if (selection) {
346
+ return { projectId: selection, source: 'selection', pinned: false };
347
+ }
348
+ if (cwdHintProjectId) {
349
+ return { projectId: cwdHintProjectId, source: 'cwd-hint', pinned: false };
350
+ }
351
+ return { projectId: '', source: 'none', pinned: false };
352
+ }
353
+
354
+ /**
355
+ * Formats a SessionProjectBinding as an ADDITIVE segment for the
356
+ * [ORCHESTRATOR-DATA] line (RFC-040 TD-204 ⑥-c). Hard requirement: when
357
+ * `pinned` (env-override), the pin MUST be surfaced explicitly — zero
358
+ * silent surprise.
359
+ */
360
+ export function formatProjectBindingSegment(binding: SessionProjectBinding): string {
361
+ if (binding.source === 'none') return ' | project=none';
362
+ if (binding.pinned) return ` | project=${binding.projectId} (pinned via env-override)`;
363
+ return ` | project=${binding.projectId} (${binding.source})`;
364
+ }
365
+
366
+ /**
367
+ * Phase 1 of the 2-phase selection flow (RFC-040 TD-204 ⑤-C, feature-
368
+ * flagged): builds the [ORCHESTRATOR-PROJECT-SELECTION] prompt emitted
369
+ * when NO binding resolved (env unset, no persisted selection, cwd
370
+ * unregistered). Instructs the assistant to ask (AskUserQuestion) which
371
+ * registered project this session belongs to, listing every registered
372
+ * project by name/id/rootPath.
373
+ *
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.
377
+ */
378
+ export async function buildProjectSelectionPrompt(): Promise<string> {
379
+ try {
380
+ const projects = await listRegisteredProjects();
381
+
382
+ if (projects.length === 0) {
383
+ return [
384
+ '[ORCHESTRATOR-PROJECT-SELECTION] No project is bound to this session (env unset, cwd unregistered, no prior selection) and no projects are registered yet.',
385
+ '[ORCHESTRATOR] Ask the user (AskUserQuestion) whether to register this cwd as a new project, or proceed unmanaged for this session.',
386
+ ].join('\n');
387
+ }
388
+
389
+ const options = projects.map((p) => `${p.name} (${p.id}) — ${p.rootPath}`).join('; ');
390
+ return [
391
+ '[ORCHESTRATOR-PROJECT-SELECTION] No project is bound to this session (env unset, cwd unregistered, no prior selection).',
392
+ `[ORCHESTRATOR] Ask the user (AskUserQuestion) which project this session belongs to. Registered projects: ${options}`,
393
+ ].join('\n');
394
+ } catch (err) {
395
+ log(HOOK, `buildProjectSelectionPrompt failed: ${String(err)}`);
396
+ return '';
397
+ }
398
+ }
399
+
248
400
  /**
249
401
  * Build [CAPABILITY-MATRIX] + [MEMORY-CONTEXT] lines for injection.
250
402
  * Fail-open: returns empty string on any error.
251
403
  */
252
404
  function buildCapabilityMatrixContext(): string {
253
405
  try {
406
+ // RFC-040 C5: unregistered cwd — no vault/agents/memory-index to read, skip.
407
+ if (
408
+ VAULT_CONFIG_PATH === null ||
409
+ AGENTS_DIR === null ||
410
+ PROFILES_PATH === null ||
411
+ MEMORY_INDEX_PATH === null
412
+ ) {
413
+ log(
414
+ HOOK,
415
+ 'buildCapabilityMatrixContext: PROJECT_ROOT unresolved (unregistered cwd) — skipping'
416
+ );
417
+ return '';
418
+ }
254
419
  let index = readCapabilitiesFromVault(VAULT_CONFIG_PATH);
255
420
  const cachedMtime = index?.installedPluginsMtime ?? '';
256
421
 
@@ -402,6 +567,13 @@ export async function getGraphContext(
402
567
  async function main(): Promise<void> {
403
568
  log(HOOK, 'SessionStart hook triggered');
404
569
 
570
+ if (PROJECT_ROOT === null) {
571
+ log(
572
+ HOOK,
573
+ 'PROJECT_ROOT unresolved: cwd has no .orchestrator sentinel up to the filesystem root — vault-dependent sections (tier, capability matrix, BACKLOG fallback) will be skipped this session (RFC-040 C5, AC-C5-2).'
574
+ );
575
+ }
576
+
405
577
  const sessionId = readSessionId();
406
578
  const sessionSegment = sessionId ? ` | session=${sessionId}` : '';
407
579
  if (sessionId) log(HOOK, `Claude session: ${sessionId}`);
@@ -410,21 +582,88 @@ async function main(): Promise<void> {
410
582
  const tierInfo = await checkTier();
411
583
  const tierSegment = formatTierSegment(tierInfo);
412
584
  const capabilityContext = buildCapabilityMatrixContext();
413
- const workflow = await getActiveWorkflow();
414
585
 
415
586
  // Sprint inbox check (RFC-027 S4 — only for sprint panes)
416
587
  const sprintInboxContext = checkSprintInbox();
417
588
 
418
- // Graph context (RFC-021 T-G4-06) — fail-open, 2s timeout
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
591
+ // resolveSessionProjectBinding() below — it is what bootstraps the
592
+ // cwd-hint tier (getToken -> ensureProjectIdResolved ->
593
+ // getEffectiveProjectId() reads the resolved value just below). Moving
594
+ // getActiveWorkflow()/getNextAction() later (see below) must not remove
595
+ // this bootstrap, or the cwd-hint tier would regress to reading '' .
419
596
  const token = await getToken();
420
597
  const graphContext = await getGraphContext(token);
421
598
 
599
+ // RFC-040 TD-204 ⑤-C: read a previously-persisted selection for THIS
600
+ // sessionId — feature-flagged (default OFF, kill-switch). With the flag
601
+ // off, `selection` stays undefined and the tier is permanently dormant,
602
+ // exactly as before ⑤-C (byte-for-byte pre-existing single-phase
603
+ // behaviour). readSessionSelection is Class C benign-degrade — never
604
+ // throws, so no try/catch needed here.
605
+ const selection =
606
+ PROJECT_SELECTION_ENABLED && sessionId
607
+ ? readSessionSelection(SESSION_SELECTION_STATE_DIR, sessionId)
608
+ : undefined;
609
+
610
+ // RFC-040 TD-204 ⑥-c: resolve the session's project binding via strict
611
+ // precedence (env-override pin > selection [⑤-C, feature-flagged] >
612
+ // cwd-hint > none) — centralizes what used to be the inline `PROJECT_ID
613
+ // || getEffectiveProjectId()` fallback into a single, pure, testable
614
+ // source of truth, and drives the mandatory pin-surfacing segment below.
615
+ const projectBinding = resolveSessionProjectBinding({
616
+ envOverride: PROJECT_ID,
617
+ selection,
618
+ cwdHintProjectId: getEffectiveProjectId(),
619
+ });
620
+ const projectBindingSegment = formatProjectBindingSegment(projectBinding);
621
+
622
+ // RFC-040 TD-206 (S372): feed the precedence-resolved binding back into
623
+ // the api-client's internal scoping so getActiveWorkflow()/getNextAction()
624
+ // (moved below, AFTER this point — see comment there) query under the
625
+ // SAME projectId the [ORCHESTRATOR-DATA] line above surfaces, instead of
626
+ // whatever cwd-hint ensureProjectIdResolved happened to resolve during the
627
+ // getToken() bootstrap call above. Closes the S371 Case-2 gap (selection
628
+ // displayed correctly but workflow queried under the cwd project).
629
+ // Safe no-op when source is 'env-override' (binding.projectId already
630
+ // equals api-client's own PROJECT_ID, seeded from the same
631
+ // ORCHESTRATOR_PROJECT_ID env var at import time) or 'cwd-hint'
632
+ // (binding.projectId already equals getEffectiveProjectId()).
633
+ setEffectiveProjectId(projectBinding.projectId);
634
+
635
+ // RFC-040 TD-204 ⑤-C, phase 1: when NO binding resolved at all AND the
636
+ // flag is on, prompt the operator to select a project. Computed ONCE and
637
+ // folded as extra lines into whichever outputContext() call below fires
638
+ // — never a SECOND outputContext call, so exactly one [ORCHESTRATOR-DATA]
639
+ // block is ever emitted (double-load regression guard).
640
+ const projectSelectionPrompt =
641
+ PROJECT_SELECTION_ENABLED && projectBinding.source === 'none'
642
+ ? await buildProjectSelectionPrompt()
643
+ : '';
644
+
645
+ // RFC-040 Operator Multi-Project — Slice 2C (C9), S358, TASK-C9-03: allocate
646
+ // a globally-numbered session (fail-open — API down never blocks the hook).
647
+ // RFC-040 Slice 5 (C5), S358/S361 / ⑥-c: allocation receives
648
+ // projectBinding.projectId — the same precedence-resolved id surfaced in
649
+ // the ORCHESTRATOR-DATA line below (AC-C5-4, single source of truth).
650
+ const sessionAllocation = await allocateSessionNumber(projectBinding.projectId);
651
+ const sessionNumberSegment = formatSessionNumberSegment(sessionAllocation);
652
+
653
+ // RFC-040 TD-206 (S372): getActiveWorkflow() now runs AFTER
654
+ // setEffectiveProjectId() above (moved from its original position, which
655
+ // ran BEFORE binding resolution — session-start.ts:584 pre-TD-206), so
656
+ // its X-Project-ID header carries the precedence-resolved
657
+ // binding.projectId (selection winner when present), not the raw
658
+ // cwd-resolved value it used to see (S371 Case-2 finding).
659
+ const workflow = await getActiveWorkflow();
660
+
422
661
  if (!workflow) {
423
662
  log(HOOK, 'No active workflow, injecting greeting data');
424
663
  outputContext(
425
664
  'SessionStart',
426
665
  [
427
- `[ORCHESTRATOR-DATA] version=${backlog.version}${sessionSegment} | workflow=none${tierSegment} | next=${backlog.next}`,
666
+ `[ORCHESTRATOR-DATA] version=${backlog.version}${sessionSegment} | workflow=none${tierSegment}${sessionNumberSegment}${projectBindingSegment} | next=${backlog.next}`,
428
667
  backlog.lastSession ? `[LAST-SESSION] ${backlog.lastSession}` : '',
429
668
  backlog.status ? `[PENDENTE] ${backlog.status}` : '',
430
669
  `[ORCHESTRATOR] Greet the user via AskUserQuestion using the data above (CLAUDE.md Rule #1).`,
@@ -433,6 +672,7 @@ async function main(): Promise<void> {
433
672
  capabilityContext,
434
673
  graphContext,
435
674
  sprintInboxContext,
675
+ projectSelectionPrompt,
436
676
  ]
437
677
  .filter(Boolean)
438
678
  .join('\n')
@@ -451,12 +691,13 @@ async function main(): Promise<void> {
451
691
  outputContext(
452
692
  'SessionStart',
453
693
  [
454
- `[ORCHESTRATOR-DATA] version=${backlog.version}${sessionSegment} | workflow=${workflow.id} | type=${workflow.type} | phase=${workflow.currentPhase} | status=${workflow.status}${actionHint}${tierSegment}`,
694
+ `[ORCHESTRATOR-DATA] version=${backlog.version}${sessionSegment} | workflow=${workflow.id} | type=${workflow.type} | phase=${workflow.currentPhase} | status=${workflow.status}${actionHint}${tierSegment}${sessionNumberSegment}${projectBindingSegment}`,
455
695
  `[ORCHESTRATOR] Active workflow detected. Greet the user via AskUserQuestion showing workflow state (CLAUDE.md Rule #1).`,
456
696
  'Do NOT invoke /orchestrator skill for the greeting — use AskUserQuestion directly.',
457
697
  'Invoke /orchestrator skill when the user chooses to continue the workflow.',
458
698
  capabilityContext,
459
699
  graphContext,
700
+ projectSelectionPrompt,
460
701
  ]
461
702
  .filter(Boolean)
462
703
  .join('\n')
@@ -0,0 +1,59 @@
1
+ import { createRequire as __createRequireShim } from 'node:module'; const require = __createRequireShim(import.meta.url);
2
+
3
+ // hooks/sprint-registry.ts
4
+ import { mkdirSync, writeFileSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ function log(msg) {
7
+ process.stderr.write(`[sprint-registry] ${msg}
8
+ `);
9
+ }
10
+ async function readStdin() {
11
+ const chunks = [];
12
+ for await (const chunk of process.stdin) {
13
+ chunks.push(chunk);
14
+ }
15
+ const raw = Buffer.concat(chunks).toString("utf-8").trim();
16
+ if (!raw) return {};
17
+ try {
18
+ return JSON.parse(raw);
19
+ } catch {
20
+ return {};
21
+ }
22
+ }
23
+ async function main() {
24
+ const stdin = await readStdin();
25
+ const sessionId = stdin.session_id ?? void 0;
26
+ const sprintId = process.env.SPRINT_ID;
27
+ const paneRole = process.env.PANE_ROLE;
28
+ if (!sprintId || !paneRole) {
29
+ log(`Skipping: SPRINT_ID=${sprintId ?? "unset"} PANE_ROLE=${paneRole ?? "unset"}`);
30
+ process.exit(0);
31
+ }
32
+ const registryDir = `/tmp/${sprintId}/registry`;
33
+ const entryPath = join(registryDir, `${paneRole}.json`);
34
+ const workflowId = process.env.WORKFLOW_ID;
35
+ const entry = {
36
+ role: paneRole,
37
+ sprintId,
38
+ pid: process.pid,
39
+ registeredAt: (/* @__PURE__ */ new Date()).toISOString(),
40
+ worktreePath: process.cwd()
41
+ };
42
+ if (sessionId) {
43
+ entry.sessionId = sessionId;
44
+ }
45
+ if (workflowId) {
46
+ entry.workflowId = workflowId;
47
+ }
48
+ try {
49
+ mkdirSync(registryDir, { recursive: true });
50
+ writeFileSync(entryPath, JSON.stringify(entry, null, 2), "utf-8");
51
+ log(
52
+ `Registered ${paneRole} for sprint ${sprintId} (pid=${process.pid}, session=${sessionId ?? "unknown"})`
53
+ );
54
+ } catch (err) {
55
+ log(`Registration failed: ${err}`);
56
+ }
57
+ process.exit(0);
58
+ }
59
+ main().catch(() => process.exit(0));