@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.
- package/agents/adversarial-reviewer.md +6 -1
- package/agents/api-extractor.md +6 -0
- package/agents/azure-devops-engineer.md +1 -2
- package/agents/business-rule-miner.md +6 -0
- package/agents/cicd-platform-engineer.md +1 -2
- package/agents/ciclo-engineer.md +15 -2
- package/agents/code-archaeologist.md +6 -0
- package/agents/debug-sidecar.md +6 -0
- package/agents/doc-sidecar.md +6 -0
- package/agents/docs-guardian.md +9 -3
- package/agents/frontend-engineer.md +1 -2
- package/agents/{ai-architect.md → harness-architect.md} +26 -5
- package/agents/implementer.md +6 -1
- package/agents/legacy-discoverer.md +6 -0
- package/agents/legacy-synthesizer.md +6 -0
- package/agents/methodology-researcher.md +6 -1
- package/agents/operational-artifact-builder.md +6 -1
- package/agents/orchestrator.md +27 -87
- package/agents/planner.md +6 -1
- package/agents/presentation-engineer.md +5 -4
- package/agents/python-data-engineer.md +0 -1
- package/agents/researcher.md +6 -0
- package/agents/reviewer.md +6 -1
- package/agents/schema-extractor.md +6 -0
- package/agents/software-architect.md +30 -7
- package/agents/specifier.md +6 -1
- package/agents/task-generator.md +6 -1
- package/agents/test-sidecar.md +6 -0
- package/dist/scripts/generate-hooks-matcher.d.ts +106 -0
- package/dist/scripts/generate-hooks-matcher.d.ts.map +1 -0
- package/dist/scripts/generate-hooks-matcher.js +153 -0
- package/dist/scripts/generate-hooks-matcher.js.map +1 -0
- package/dist/scripts/generate-manifest.d.ts +22 -0
- package/dist/scripts/generate-manifest.d.ts.map +1 -1
- package/dist/scripts/generate-manifest.js +67 -2
- package/dist/scripts/generate-manifest.js.map +1 -1
- package/hooks/agent-git-guard.mjs +63 -11
- package/hooks/dangling-guard.mjs +42 -12
- package/hooks/gate-guardian.mjs +38 -8
- package/hooks/hooks.json +3 -9
- package/hooks/lib/api-client.ts +128 -9
- package/hooks/lib/capability-scanner.ts +391 -55
- package/hooks/lib/dispatch-marker.ts +214 -0
- package/hooks/lib/git-checkpoint.ts +34 -37
- package/hooks/lib/observations-helper.ts +14 -1
- package/hooks/mailbox-listener.mjs +69 -17
- package/hooks/playwright-output-guard.mjs +40 -10
- package/hooks/post-implement.mjs +38 -8
- package/hooks/pre-agent-dispatch.mjs +137 -16
- package/hooks/pre-agent-dispatch.ts +82 -15
- package/hooks/session-start.mjs +274 -103
- package/hooks/session-start.ts +250 -69
- package/hooks/subagent-start.mjs +7665 -22
- package/hooks/subagent-start.ts +367 -16
- package/hooks/subagent-stop.mjs +102 -62
- package/hooks/subagent-stop.ts +124 -28
- package/hooks/user-prompt.mjs +43 -13
- package/hooks/verification-before-completion.mjs +63 -11
- package/hooks/workflow-guard.mjs +45 -11
- package/hooks-settings-template.json +1 -7
- package/manifest.json +62 -62
- package/package.json +4 -2
- package/skills/checkpoint-protocol/SKILL.md +19 -4
- package/skills/ciclo-lane-dispatch/SKILL.md +24 -0
- package/skills/fase-1b-projecao/SKILL.md +146 -0
- package/skills/orchestrator/SKILL.md +11 -3
- package/skills/workflow-coordination/SKILL.md +3 -3
- package/hooks/post-compact.mjs +0 -7931
- package/hooks/post-compact.ts +0 -170
package/hooks/session-start.ts
CHANGED
|
@@ -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
|
-
|
|
28
|
-
|
|
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
|
-
*
|
|
76
|
-
*
|
|
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
|
-
|
|
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
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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]
|
|
407
|
-
*
|
|
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
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
|
|
525
|
+
return formatCapabilityMatrix(index);
|
|
526
|
+
}
|
|
443
527
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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(
|
|
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
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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
|
]
|