@orchestrator-claude/definitions 3.36.0 → 3.37.1

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 (69) hide show
  1. package/agents/adversarial-reviewer.md +6 -1
  2. package/agents/api-extractor.md +6 -0
  3. package/agents/azure-devops-engineer.md +1 -2
  4. package/agents/business-rule-miner.md +6 -0
  5. package/agents/cicd-platform-engineer.md +1 -2
  6. package/agents/ciclo-engineer.md +15 -2
  7. package/agents/code-archaeologist.md +6 -0
  8. package/agents/debug-sidecar.md +6 -0
  9. package/agents/doc-sidecar.md +6 -0
  10. package/agents/docs-guardian.md +9 -3
  11. package/agents/frontend-engineer.md +1 -2
  12. package/agents/{ai-architect.md → harness-architect.md} +26 -5
  13. package/agents/implementer.md +6 -1
  14. package/agents/legacy-discoverer.md +6 -0
  15. package/agents/legacy-synthesizer.md +6 -0
  16. package/agents/methodology-researcher.md +6 -1
  17. package/agents/operational-artifact-builder.md +6 -1
  18. package/agents/orchestrator.md +27 -87
  19. package/agents/planner.md +6 -1
  20. package/agents/presentation-engineer.md +5 -4
  21. package/agents/python-data-engineer.md +0 -1
  22. package/agents/researcher.md +6 -0
  23. package/agents/reviewer.md +6 -1
  24. package/agents/schema-extractor.md +6 -0
  25. package/agents/software-architect.md +30 -7
  26. package/agents/specifier.md +6 -1
  27. package/agents/task-generator.md +6 -1
  28. package/agents/test-sidecar.md +6 -0
  29. package/dist/scripts/generate-hooks-matcher.d.ts +106 -0
  30. package/dist/scripts/generate-hooks-matcher.d.ts.map +1 -0
  31. package/dist/scripts/generate-hooks-matcher.js +153 -0
  32. package/dist/scripts/generate-hooks-matcher.js.map +1 -0
  33. package/dist/scripts/generate-manifest.d.ts +22 -0
  34. package/dist/scripts/generate-manifest.d.ts.map +1 -1
  35. package/dist/scripts/generate-manifest.js +67 -2
  36. package/dist/scripts/generate-manifest.js.map +1 -1
  37. package/hooks/agent-git-guard.mjs +63 -11
  38. package/hooks/dangling-guard.mjs +42 -12
  39. package/hooks/gate-guardian.mjs +38 -8
  40. package/hooks/hooks.json +3 -9
  41. package/hooks/lib/api-client.ts +128 -9
  42. package/hooks/lib/capability-scanner.ts +391 -55
  43. package/hooks/lib/dispatch-marker.ts +214 -0
  44. package/hooks/lib/git-checkpoint.ts +34 -37
  45. package/hooks/lib/observations-helper.ts +14 -1
  46. package/hooks/mailbox-listener.mjs +69 -17
  47. package/hooks/playwright-output-guard.mjs +40 -10
  48. package/hooks/post-implement.mjs +38 -8
  49. package/hooks/pre-agent-dispatch.mjs +137 -16
  50. package/hooks/pre-agent-dispatch.ts +82 -15
  51. package/hooks/session-start.mjs +274 -103
  52. package/hooks/session-start.ts +250 -69
  53. package/hooks/subagent-start.mjs +7665 -22
  54. package/hooks/subagent-start.ts +367 -16
  55. package/hooks/subagent-stop.mjs +102 -62
  56. package/hooks/subagent-stop.ts +124 -28
  57. package/hooks/user-prompt.mjs +43 -13
  58. package/hooks/verification-before-completion.mjs +63 -11
  59. package/hooks/workflow-guard.mjs +45 -11
  60. package/hooks-settings-template.json +1 -7
  61. package/manifest.json +62 -62
  62. package/package.json +4 -2
  63. package/skills/checkpoint-protocol/SKILL.md +19 -4
  64. package/skills/ciclo-lane-dispatch/SKILL.md +24 -0
  65. package/skills/fase-1b-projecao/SKILL.md +146 -0
  66. package/skills/orchestrator/SKILL.md +11 -3
  67. package/skills/workflow-coordination/SKILL.md +3 -3
  68. package/hooks/post-compact.mjs +0 -7931
  69. package/hooks/post-compact.ts +0 -170
@@ -10,6 +10,7 @@ import { fileURLToPath } from 'node:url';
10
10
  import { parse as parseYaml } from 'yaml';
11
11
  import {
12
12
  log,
13
+ readStdin,
13
14
  getActiveWorkflow,
14
15
  getNextAction,
15
16
  outputContext,
@@ -24,14 +25,16 @@ import {
24
25
  import {
25
26
  needsRescan,
26
27
  buildCapabilityIndex,
27
- writeCapabilitiesToVault,
28
- readCapabilitiesFromVault,
28
+ resolveCapabilityCachePath,
29
+ writeCapabilityCache,
30
+ readCapabilityCache,
29
31
  scanAgentDefinitions,
30
32
  formatCapabilityMatrix,
31
33
  readMemoryIndex,
32
34
  filterRelevantMemories,
33
35
  formatMemoryContext,
34
36
  } from './lib/capability-scanner.js';
37
+ import type { MemoryIndexDiagnostic } from './lib/capability-scanner.js';
35
38
  import { readSessionSelection } from './lib/session-selection.js';
36
39
 
37
40
  const HOOK = 'SESSION-START';
@@ -63,29 +66,115 @@ const PROJECT_SELECTION_ENABLED = process.env.ORCHESTRATOR_ENABLE_PROJECT_SELECT
63
66
  // that is exactly the "no binding" state phase-1 prompts for).
64
67
  const SESSION_SELECTION_STATE_DIR = join(HOME, '.orchestrator', 'session-selection');
65
68
  const AGENTS_DIR = PROJECT_ROOT ? join(PROJECT_ROOT, '.claude', 'agents') : null;
69
+ // READ-ONLY from here on (TD-235 half B / S445 VETOR 1): the vault config is a
70
+ // git-tracked governance document. The scan cache moved out of it and into the
71
+ // gitignored state dir — see capability-scanner.ts "Cache Persistence".
66
72
  const VAULT_CONFIG_PATH = PROJECT_ROOT
67
73
  ? join(PROJECT_ROOT, 'project-guidelines', '.vault-config.yaml')
68
74
  : null;
75
+ // Same null-when-unregistered-cwd contract as its siblings (RFC-040 C5).
76
+ const CAPABILITY_CACHE_PATH = PROJECT_ROOT ? resolveCapabilityCachePath(PROJECT_ROOT) : null;
69
77
  const PROFILES_PATH = PROJECT_ROOT ? join(PROJECT_ROOT, '.claude', 'plugin-profiles.yaml') : null;
70
78
  const MEMORY_INDEX_PATH = PROJECT_ROOT
71
79
  ? join(PROJECT_ROOT, '.claude', 'agent-memory', 'orchestrator', 'memory-index.yaml')
72
80
  : null;
73
81
 
74
82
  /**
75
- * Read session_id from SessionStart stdin payload.
76
- * Claude Code sends JSON with session_id on SessionStart.
83
+ * How the harness entered this session. `compact` means context was
84
+ * compacted MID-TASK — a recovery, not a start (SPEC-S454 §12).
77
85
  */
78
- function readSessionId(): string | null {
86
+ interface SessionStartPayload {
87
+ sessionId: string | null;
88
+ source: string | null;
89
+ }
90
+
91
+ /**
92
+ * Read the SessionStart stdin payload — BOTH fields, in ONE read.
93
+ *
94
+ * SPEC-S455 RF-001/ADR-001: drains `process.stdin` to EOF via `readStdin()`
95
+ * (the same async primitive 13 of the other 14 hooks in this house already
96
+ * use successfully — `lib/api-client.ts#readStdin`), not
97
+ * `readFileSync('/dev/stdin')` (the old, synchronous mechanism — confirmed by
98
+ * runtime measurement, S455, to never deliver a real payload under the
99
+ * Claude Code harness's spawn: every `Claude session:` log line in this
100
+ * repo's history before this fix was a test fixture, zero were real UUIDs).
101
+ *
102
+ * ★ `process.stdin` is a stream, drained by the first `for await` inside
103
+ * `readStdin()`. A second read resolves to `{}` (readStdin's own
104
+ * empty/malformed contract), so a second call here would yield
105
+ * `source: null` on every real invocation, permanently pinning the hook to
106
+ * the boot branch while every mock-based test still passed — the same class
107
+ * of trap as the old mechanism, now manifested asynchronously instead of
108
+ * synchronously. That failure is silent by construction, which is why this
109
+ * function returns the whole payload and is called exactly once (see its
110
+ * single call site in `main()`). Do not add a second reader.
111
+ * Regression-pinned by tests/hooks/session-start-source-branch.test.ts, whose
112
+ * stdin mock models real drain semantics and counts the reads, and by
113
+ * tests/integration/api/helpers/spawn-session-start.test.ts, which drives a
114
+ * real OS pipe against the compiled bundle.
115
+ *
116
+ * ADR-005 (SPEC-S455 planner finding): the outer try/catch is deliberately
117
+ * KEPT around `readStdin()`, not removed — `readStdin()`'s own try/catch only
118
+ * protects `JSON.parse`, never a `for await` that itself throws/rejects (e.g.
119
+ * `process.stdin` unavailable/non-iterable under an unusual host such as a
120
+ * Vitest worker thread). Fail-open (RNF-002) must hold against ANY read
121
+ * failure, not just a malformed payload.
122
+ */
123
+ async function readSessionStartPayload(): Promise<SessionStartPayload> {
79
124
  try {
80
- const raw = readFileSync('/dev/stdin', 'utf-8').trim();
81
- if (!raw) return null;
82
- const payload = JSON.parse(raw) as Record<string, unknown>;
83
- return (payload.session_id as string) ?? null;
84
- } catch {
85
- return null;
125
+ const payload = await readStdin();
126
+ // SPEC-S455 RF-003: unconditional, every invocation — this is the line
127
+ // that would have made the old defect diagnosable from day one (model
128
+ // already validated by dangling-guard.ts:312). Limitation, documented
129
+ // here and in the spec: readStdin() collapses "stdin empty" and "JSON
130
+ // malformed" into the same {} — this instrumentation cannot and does not
131
+ // distinguish the two (out of scope, would require changing the shared
132
+ // helper, which 13 other hooks depend on).
133
+ log(HOOK, `stdin keys: [${Object.keys(payload).join(', ')}]`);
134
+ if (!payload.session_id && !payload.source) {
135
+ log(
136
+ HOOK,
137
+ `[WARN] stdin payload has neither session_id nor source — read likely empty or unparseable (readStdin() collapses both cases to {}); raw keys=[${Object.keys(payload).join(', ')}]`
138
+ );
139
+ }
140
+ return {
141
+ sessionId: (payload.session_id as string) ?? null,
142
+ source: (payload.source as string) ?? null,
143
+ };
144
+ } catch (err) {
145
+ // Anomaly readStdin() itself doesn't swallow (its own try/catch only
146
+ // covers JSON.parse) — e.g. a rejecting/throwing `for await`. Fail-open
147
+ // per RNF-002/ADR-005, but leave a trace instead of the old silence.
148
+ log(HOOK, `[WARN] readStdin() itself threw (not just empty/malformed stdin) — ${String(err)}`);
149
+ return { sessionId: null, source: null };
86
150
  }
87
151
  }
88
152
 
153
+ /**
154
+ * Recovery is ONLY `compact`. Every other source — including absent, unknown
155
+ * and unparseable — is a boot, so any degradation lands on the pre-existing
156
+ * behaviour rather than on a new, less-tested path (RNF-002 fail-open).
157
+ */
158
+ function isRecoverySource(source: string | null): boolean {
159
+ return source === 'compact';
160
+ }
161
+
162
+ /**
163
+ * Injected INSTEAD of the greeting instructions when recovering.
164
+ *
165
+ * The hard guarantee against a mid-task greeting is the missing
166
+ * `[ORCHESTRATOR-DATA]` token, not this text: the Session Start Protocol keys
167
+ * its AskUserQuestion greeting off that literal token, so withholding it
168
+ * disarms the trigger without editing the identity file (SPEC-S454 D-1,
169
+ * decided by the user). This directive is the belt to that suspenders — it
170
+ * states the situation the model cannot otherwise infer, since from the
171
+ * inside a compaction is indistinguishable from a fresh session.
172
+ */
173
+ const RECOVERY_DIRECTIVE: readonly string[] = [
174
+ '[ORCHESTRATOR] Context was compacted mid-session. This is NOT a session start.',
175
+ 'Do NOT greet. Do NOT call AskUserQuestion. Resume the in-flight task.',
176
+ ];
177
+
89
178
  // ─── RFC-033 Phase 1: project-state.yaml reader ─────────────────────────────
90
179
 
91
180
  const PROJECT_STATE_PATH = PROJECT_ROOT
@@ -403,57 +492,118 @@ export async function buildProjectSelectionPrompt(): Promise<string> {
403
492
  }
404
493
 
405
494
  /**
406
- * Build [CAPABILITY-MATRIX] + [MEMORY-CONTEXT] lines for injection.
407
- * Fail-open: returns empty string on any error.
495
+ * Build the [CAPABILITY-MATRIX] line (rescan-or-cache + format).
496
+ * Split out of buildCapabilityMatrixContext (TD-249) so a failure here
497
+ * cannot discard a memory line already built by buildMemoryLine, or vice
498
+ * versa — the two used to share one try block and were all-or-nothing.
408
499
  */
409
- function buildCapabilityMatrixContext(): string {
410
- try {
411
- // RFC-040 C5: unregistered cwd — no vault/agents/memory-index to read, skip.
412
- if (
413
- VAULT_CONFIG_PATH === null ||
414
- AGENTS_DIR === null ||
415
- PROFILES_PATH === null ||
416
- MEMORY_INDEX_PATH === null
417
- ) {
418
- log(
419
- HOOK,
420
- 'buildCapabilityMatrixContext: PROJECT_ROOT unresolved (unregistered cwd) — skipping'
421
- );
422
- return '';
423
- }
424
- let index = readCapabilitiesFromVault(VAULT_CONFIG_PATH);
425
- const cachedMtime = index?.installedPluginsMtime ?? '';
426
-
427
- if (!index || needsRescan(INSTALLED_PLUGINS_PATH, cachedMtime)) {
428
- log(HOOK, 'Capability rescan triggered');
429
- index = buildCapabilityIndex({
430
- installedPluginsPath: INSTALLED_PLUGINS_PATH,
431
- agentsDir: AGENTS_DIR,
432
- vaultConfigPath: VAULT_CONFIG_PATH,
433
- profilesPath: PROFILES_PATH,
434
- });
435
- writeCapabilitiesToVault(VAULT_CONFIG_PATH, index);
436
- } else {
437
- // Agents are always scanned fresh (fast, <10ms)
438
- index.agents = scanAgentDefinitions(AGENTS_DIR);
439
- log(HOOK, 'Using cached capability index');
440
- }
500
+ function buildMatrixLine(
501
+ vaultConfigPath: string,
502
+ agentsDir: string,
503
+ profilesPath: string,
504
+ cachePath: string
505
+ ): string {
506
+ // A null cache (absent / corrupt) means RESCAN, never an empty matrix.
507
+ let index = readCapabilityCache(cachePath, vaultConfigPath);
508
+ const cachedMtime = index?.installedPluginsMtime ?? '';
509
+
510
+ if (!index || needsRescan(INSTALLED_PLUGINS_PATH, cachedMtime)) {
511
+ log(HOOK, 'Capability rescan triggered');
512
+ index = buildCapabilityIndex({
513
+ installedPluginsPath: INSTALLED_PLUGINS_PATH,
514
+ agentsDir,
515
+ vaultConfigPath,
516
+ profilesPath,
517
+ });
518
+ writeCapabilityCache(cachePath, index);
519
+ } else {
520
+ // Agents are always scanned fresh (fast, <10ms)
521
+ index.agents = scanAgentDefinitions(agentsDir);
522
+ log(HOOK, 'Using cached capability index');
523
+ }
441
524
 
442
- const matrixLine = formatCapabilityMatrix(index);
525
+ return formatCapabilityMatrix(index);
526
+ }
443
527
 
444
- // Memory context
445
- const memoryEntries = readMemoryIndex(MEMORY_INDEX_PATH);
446
- let memoryLine = '';
447
- if (memoryEntries) {
448
- const filtered = filterRelevantMemories(memoryEntries);
449
- memoryLine = formatMemoryContext(filtered);
450
- }
528
+ /**
529
+ * Build the [MEMORY-CONTEXT] line. Split out of buildCapabilityMatrixContext
530
+ * (TD-249) — see buildMatrixLine doc comment for why.
531
+ */
532
+ function buildMemoryLine(memoryIndexPath: string): string {
533
+ const diag: MemoryIndexDiagnostic = {};
534
+ const memoryEntries = readMemoryIndex(memoryIndexPath, diag);
535
+ if (diag.reason) log(HOOK, `Memory index: ${diag.reason}`);
536
+
537
+ // Índice AUSENTE segue silencioso — não ter memória ainda é legítimo, e é o
538
+ // estado da maior parte dos slugs. Índice que EXISTE e não carrega, NÃO: era
539
+ // indistinguível de "não há memória" (S480). E o log sozinho não bastaria —
540
+ // ele vai para um arquivo de debug que ninguém abre. A linha entra NA SESSÃO,
541
+ // que é onde a ausência precisa ser vista: mesma regra do monitor da Root,
542
+ // campo ausente nunca some da tela.
543
+ //
544
+ // A guarda é `diag.reason`, NÃO `length === 0`: lista legitimamente vazia
545
+ // segue pelo caminho original (filter -> format -> ''), porque não é o
546
+ // defeito e mudá-la quebrou um teste que já existia — ele mocka
547
+ // readMemoryIndex devolvendo [] e assere que o fluxo continua.
548
+ if (diag.reason && (!memoryEntries || memoryEntries.length === 0)) {
549
+ return `[MEMORY-CONTEXT] INDISPONÍVEL — nenhuma memória injetada. ${diag.reason}`;
550
+ }
551
+ if (!memoryEntries) return '';
552
+ const filtered = filterRelevantMemories(memoryEntries);
553
+ return formatMemoryContext(filtered, memoryEntries);
554
+ }
555
+
556
+ /**
557
+ * Build [CAPABILITY-MATRIX] + [MEMORY-CONTEXT] lines for injection.
558
+ * Fail-open: each half degrades INDEPENDENTLY (TD-249) — a throw building
559
+ * the matrix must not discard a working memory line, and a throw building
560
+ * the memory line must not discard a working matrix line. Previously both
561
+ * lived in one try block, so either failure returned '' and threw away
562
+ * whatever the other half had already produced.
563
+ * Exported for unit-testability (TD-249).
564
+ */
565
+ export function buildCapabilityMatrixContext(): string {
566
+ // RFC-040 C5: unregistered cwd — no vault/agents/memory-index to read, skip.
567
+ // (Single guard is correct here, NOT a repeat of the TD-249 bug: all five
568
+ // paths derive from the same PROJECT_ROOT, so they are null/non-null
569
+ // together — this is one precondition, not two independently-failing
570
+ // steps. CAPABILITY_CACHE_PATH joined them in S445; it is PROJECT_ROOT-
571
+ // derived too, unless ORCHESTRATOR_STATE_DIR overrides the state dir, in
572
+ // which case it is non-null whenever PROJECT_ROOT is.)
573
+ if (
574
+ VAULT_CONFIG_PATH === null ||
575
+ AGENTS_DIR === null ||
576
+ PROFILES_PATH === null ||
577
+ MEMORY_INDEX_PATH === null ||
578
+ CAPABILITY_CACHE_PATH === null
579
+ ) {
580
+ log(
581
+ HOOK,
582
+ 'buildCapabilityMatrixContext: PROJECT_ROOT unresolved (unregistered cwd) — skipping'
583
+ );
584
+ return '';
585
+ }
451
586
 
452
- return [matrixLine, memoryLine].filter(Boolean).join('\n');
587
+ let matrixLine = '';
588
+ try {
589
+ matrixLine = buildMatrixLine(
590
+ VAULT_CONFIG_PATH,
591
+ AGENTS_DIR,
592
+ PROFILES_PATH,
593
+ CAPABILITY_CACHE_PATH
594
+ );
453
595
  } catch (err) {
454
596
  log(HOOK, `Capability matrix failed: ${err}`);
455
- return '';
456
597
  }
598
+
599
+ let memoryLine = '';
600
+ try {
601
+ memoryLine = buildMemoryLine(MEMORY_INDEX_PATH);
602
+ } catch (err) {
603
+ log(HOOK, `Memory context failed: ${err}`);
604
+ }
605
+
606
+ return [matrixLine, memoryLine].filter(Boolean).join('\n');
457
607
  }
458
608
 
459
609
  /**
@@ -519,9 +669,14 @@ async function main(): Promise<void> {
519
669
  );
520
670
  }
521
671
 
522
- const sessionId = readSessionId();
672
+ // ONE read of stdin (readStdin() drains process.stdin to EOF), both fields
673
+ // derived from it — see readSessionStartPayload's doc comment for why a
674
+ // second read is fatal.
675
+ const { sessionId, source } = await readSessionStartPayload();
676
+ const isRecovery = isRecoverySource(source);
523
677
  const sessionSegment = sessionId ? ` | session=${sessionId}` : '';
524
678
  if (sessionId) log(HOOK, `Claude session: ${sessionId}`);
679
+ log(HOOK, `source=${source ?? 'unknown'} mode=${isRecovery ? 'recovery' : 'boot'}`);
525
680
 
526
681
  const backlog = getBacklogSummary();
527
682
  const tierInfo = await checkTier();
@@ -582,8 +737,15 @@ async function main(): Promise<void> {
582
737
  // folded as extra lines into whichever outputContext() call below fires
583
738
  // — never a SECOND outputContext call, so exactly one [ORCHESTRATOR-DATA]
584
739
  // block is ever emitted (double-load regression guard).
740
+ //
741
+ // SPEC-S454 RF-002: skipped entirely when recovering. A compaction does not
742
+ // change the project binding — if none resolved, the prompt already fired at
743
+ // boot — and re-prompting mid-task is both an AskUserQuestion (which the
744
+ // recovery directive just forbade) and the interruption this fix exists to
745
+ // avoid. Skipping also spares the API round-trip inside the recovery path
746
+ // (RNF-001: no new I/O).
585
747
  const projectSelectionPrompt =
586
- PROJECT_SELECTION_ENABLED && projectBinding.source === 'none'
748
+ PROJECT_SELECTION_ENABLED && !isRecovery && projectBinding.source === 'none'
587
749
  ? await buildProjectSelectionPrompt()
588
750
  : '';
589
751
 
@@ -604,16 +766,27 @@ async function main(): Promise<void> {
604
766
  const workflow = await getActiveWorkflow();
605
767
 
606
768
  if (!workflow) {
607
- log(HOOK, 'No active workflow, injecting greeting data');
769
+ log(HOOK, `No active workflow, injecting ${isRecovery ? 'recovery' : 'greeting'} data`);
770
+ // SPEC-S454 RF-002: identical STATE, different FRAME. Only the leading
771
+ // token and the instruction block differ between boot and recovery — the
772
+ // capability matrix, memory context and sprint inbox are exactly what a
773
+ // compaction destroys, so they ride both paths unchanged.
774
+ // [LAST-SESSION]/[PENDENTE] stay boot-only: they orient someone arriving,
775
+ // and mid-task they are noise that reads like a session opening.
776
+ const stateLine = `version=${backlog.version}${sessionSegment} | workflow=none${tierSegment}${sessionNumberSegment}${projectBindingSegment} | next=${backlog.next}`;
608
777
  outputContext(
609
778
  'SessionStart',
610
779
  [
611
- `[ORCHESTRATOR-DATA] version=${backlog.version}${sessionSegment} | workflow=none${tierSegment}${sessionNumberSegment}${projectBindingSegment} | next=${backlog.next}`,
612
- backlog.lastSession ? `[LAST-SESSION] ${backlog.lastSession}` : '',
613
- backlog.status ? `[PENDENTE] ${backlog.status}` : '',
614
- `[ORCHESTRATOR] Greet the user via AskUserQuestion using the data above (CLAUDE.md Rule #1).`,
615
- 'Do NOT invoke /orchestrator skill for the greeting — use AskUserQuestion directly.',
616
- 'Invoke /orchestrator skill only when the user chooses to start a workflow.',
780
+ `${isRecovery ? '[ORCHESTRATOR-RECOVERY]' : '[ORCHESTRATOR-DATA]'} ${stateLine}`,
781
+ ...(isRecovery
782
+ ? RECOVERY_DIRECTIVE
783
+ : [
784
+ backlog.lastSession ? `[LAST-SESSION] ${backlog.lastSession}` : '',
785
+ backlog.status ? `[PENDENTE] ${backlog.status}` : '',
786
+ `[ORCHESTRATOR] Greet the user via AskUserQuestion using the data above (orchestrator identity, MUST rule #1).`,
787
+ 'Do NOT invoke /orchestrator skill for the greeting — use AskUserQuestion directly.',
788
+ 'Invoke /orchestrator skill only when the user chooses to start a workflow.',
789
+ ]),
617
790
  capabilityContext,
618
791
  sprintInboxContext,
619
792
  projectSelectionPrompt,
@@ -631,14 +804,22 @@ async function main(): Promise<void> {
631
804
  ? ` | pending=${action.pendingAction.agent} (${action.pendingAction.status})`
632
805
  : '';
633
806
 
634
- log(HOOK, `Injecting workflow context (wf=${workflow.id} phase=${workflow.currentPhase})`);
807
+ log(
808
+ HOOK,
809
+ `Injecting workflow ${isRecovery ? 'recovery' : 'context'} (wf=${workflow.id} phase=${workflow.currentPhase})`
810
+ );
811
+ const stateLine = `version=${backlog.version}${sessionSegment} | workflow=${workflow.id} | type=${workflow.type} | phase=${workflow.currentPhase} | status=${workflow.status}${actionHint}${tierSegment}${sessionNumberSegment}${projectBindingSegment}`;
635
812
  outputContext(
636
813
  'SessionStart',
637
814
  [
638
- `[ORCHESTRATOR-DATA] version=${backlog.version}${sessionSegment} | workflow=${workflow.id} | type=${workflow.type} | phase=${workflow.currentPhase} | status=${workflow.status}${actionHint}${tierSegment}${sessionNumberSegment}${projectBindingSegment}`,
639
- `[ORCHESTRATOR] Active workflow detected. Greet the user via AskUserQuestion showing workflow state (CLAUDE.md Rule #1).`,
640
- 'Do NOT invoke /orchestrator skill for the greeting — use AskUserQuestion directly.',
641
- 'Invoke /orchestrator skill when the user chooses to continue the workflow.',
815
+ `${isRecovery ? '[ORCHESTRATOR-RECOVERY]' : '[ORCHESTRATOR-DATA]'} ${stateLine}`,
816
+ ...(isRecovery
817
+ ? RECOVERY_DIRECTIVE
818
+ : [
819
+ `[ORCHESTRATOR] Active workflow detected. Greet the user via AskUserQuestion showing workflow state (orchestrator identity, MUST rule #1).`,
820
+ 'Do NOT invoke /orchestrator skill for the greeting — use AskUserQuestion directly.',
821
+ 'Invoke /orchestrator skill when the user chooses to continue the workflow.',
822
+ ]),
642
823
  capabilityContext,
643
824
  projectSelectionPrompt,
644
825
  ]