@orchestrator-claude/definitions 3.37.0 → 3.37.2
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 +50 -21
- 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} +8 -3
- 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 +29 -15
- 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 +10 -5
- 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 +462 -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/lib/security-gate-core.ts +271 -0
- 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/security-gate-honest.mjs +302 -0
- package/hooks/security-gate-honest.ts +148 -0
- package/hooks/session-start.mjs +304 -106
- package/hooks/session-start.ts +317 -71
- package/hooks/subagent-start.mjs +7673 -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 +77 -61
- package/package.json +4 -2
- package/skills/checkpoint-protocol/SKILL.md +19 -4
- package/skills/ciclo-lane-dispatch/SKILL.md +117 -8
- package/skills/fase-1b-projecao/SKILL.md +150 -0
- package/skills/lane-1-entrevista/SKILL.md +173 -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,59 +492,172 @@ 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
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
+
}
|
|
524
|
+
|
|
525
|
+
return formatCapabilityMatrix(index);
|
|
526
|
+
}
|
|
441
527
|
|
|
442
|
-
|
|
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
|
+
}
|
|
443
555
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
556
|
+
/**
|
|
557
|
+
* RFC-040 C5: unregistered cwd — no vault/agents/memory-index to read, skip.
|
|
558
|
+
* (Single guard is correct here, NOT a repeat of the TD-249 bug: all five
|
|
559
|
+
* paths derive from the same PROJECT_ROOT, so they are null/non-null
|
|
560
|
+
* together — this is one precondition, not two independently-failing
|
|
561
|
+
* steps. CAPABILITY_CACHE_PATH joined them in S445; it is PROJECT_ROOT-
|
|
562
|
+
* derived too, unless ORCHESTRATOR_STATE_DIR overrides the state dir, in
|
|
563
|
+
* which case it is non-null whenever PROJECT_ROOT is.)
|
|
564
|
+
*/
|
|
565
|
+
interface VaultPaths {
|
|
566
|
+
vaultConfigPath: string;
|
|
567
|
+
agentsDir: string;
|
|
568
|
+
profilesPath: string;
|
|
569
|
+
memoryIndexPath: string;
|
|
570
|
+
capabilityCachePath: string;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function resolveVaultPaths(caller: string): VaultPaths | null {
|
|
574
|
+
if (
|
|
575
|
+
VAULT_CONFIG_PATH === null ||
|
|
576
|
+
AGENTS_DIR === null ||
|
|
577
|
+
PROFILES_PATH === null ||
|
|
578
|
+
MEMORY_INDEX_PATH === null ||
|
|
579
|
+
CAPABILITY_CACHE_PATH === null
|
|
580
|
+
) {
|
|
581
|
+
log(HOOK, `${caller}: PROJECT_ROOT unresolved (unregistered cwd) — skipping`);
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
584
|
+
return {
|
|
585
|
+
vaultConfigPath: VAULT_CONFIG_PATH,
|
|
586
|
+
agentsDir: AGENTS_DIR,
|
|
587
|
+
profilesPath: PROFILES_PATH,
|
|
588
|
+
memoryIndexPath: MEMORY_INDEX_PATH,
|
|
589
|
+
capabilityCachePath: CAPABILITY_CACHE_PATH,
|
|
590
|
+
};
|
|
591
|
+
}
|
|
451
592
|
|
|
452
|
-
|
|
593
|
+
/**
|
|
594
|
+
* Build the [CAPABILITY-MATRIX] line for injection. Fail-open (TD-249):
|
|
595
|
+
* a throw here returns '' and never touches the memory line, which is
|
|
596
|
+
* built by buildMemoryContext() on its own try block.
|
|
597
|
+
* Exported for unit-testability (TD-315).
|
|
598
|
+
*/
|
|
599
|
+
export function buildMatrixContext(): string {
|
|
600
|
+
const paths = resolveVaultPaths('buildMatrixContext');
|
|
601
|
+
if (!paths) return '';
|
|
602
|
+
try {
|
|
603
|
+
return buildMatrixLine(
|
|
604
|
+
paths.vaultConfigPath,
|
|
605
|
+
paths.agentsDir,
|
|
606
|
+
paths.profilesPath,
|
|
607
|
+
paths.capabilityCachePath
|
|
608
|
+
);
|
|
453
609
|
} catch (err) {
|
|
454
610
|
log(HOOK, `Capability matrix failed: ${err}`);
|
|
455
611
|
return '';
|
|
456
612
|
}
|
|
457
613
|
}
|
|
458
614
|
|
|
615
|
+
/**
|
|
616
|
+
* Build the [MEMORY-CONTEXT] line for injection. Fail-open (TD-249):
|
|
617
|
+
* a throw here returns '' and never touches the matrix line.
|
|
618
|
+
*
|
|
619
|
+
* TD-315: emitted SEPARATELY from the matrix — and BEFORE [LAST-SESSION] —
|
|
620
|
+
* so the memory is not the first block to die when the harness truncates
|
|
621
|
+
* additionalContext. Measured S513/S514: total 17190/17013 chars; with the
|
|
622
|
+
* old order ([ORCHESTRATOR-DATA] · [LAST-SESSION] · [PENDENTE] · greeting ·
|
|
623
|
+
* [CAPABILITY-MATRIX] · [MEMORY-CONTEXT]) the memory started at offset
|
|
624
|
+
* 10974/10551 and a 10000-char cut took ALL 20 memories while the
|
|
625
|
+
* narrative survived whole. Placed right after the state line (offset
|
|
626
|
+
* 4101 in S514, ending 10563), the same cut keeps 91% of it, and any cut
|
|
627
|
+
* >= 10563 keeps it whole — with nothing lost when nothing is cut. Capping
|
|
628
|
+
* [LAST-SESSION] instead would NOT save the memory (it would still end past
|
|
629
|
+
* 13000) and would mutilate the PM's narrative every session, cut or not.
|
|
630
|
+
* Exported for unit-testability (TD-315).
|
|
631
|
+
*/
|
|
632
|
+
export function buildMemoryContext(): string {
|
|
633
|
+
const paths = resolveVaultPaths('buildMemoryContext');
|
|
634
|
+
if (!paths) return '';
|
|
635
|
+
try {
|
|
636
|
+
return buildMemoryLine(paths.memoryIndexPath);
|
|
637
|
+
} catch (err) {
|
|
638
|
+
log(HOOK, `Memory context failed: ${err}`);
|
|
639
|
+
return '';
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Build [CAPABILITY-MATRIX] + [MEMORY-CONTEXT] lines, matrix first.
|
|
645
|
+
* Fail-open: each half degrades INDEPENDENTLY (TD-249) — a throw building
|
|
646
|
+
* the matrix must not discard a working memory line, and a throw building
|
|
647
|
+
* the memory line must not discard a working matrix line. Previously both
|
|
648
|
+
* lived in one try block, so either failure returned '' and threw away
|
|
649
|
+
* whatever the other half had already produced.
|
|
650
|
+
*
|
|
651
|
+
* TD-315: no longer what main() injects — main() now places the two halves
|
|
652
|
+
* at different offsets (memory before the narrative, matrix after the
|
|
653
|
+
* greeting). Kept as the composed form for the TD-249 partial-failure
|
|
654
|
+
* contract and any external caller.
|
|
655
|
+
* Exported for unit-testability (TD-249).
|
|
656
|
+
*/
|
|
657
|
+
export function buildCapabilityMatrixContext(): string {
|
|
658
|
+
return [buildMatrixContext(), buildMemoryContext()].filter(Boolean).join('\n');
|
|
659
|
+
}
|
|
660
|
+
|
|
459
661
|
/**
|
|
460
662
|
* Check sprint mailbox inbox for pending tasks (RFC-027 Sprint 4).
|
|
461
663
|
* Only runs when SPRINT_ID and PANE_ROLE are set (sprint pane sessions).
|
|
@@ -519,14 +721,22 @@ async function main(): Promise<void> {
|
|
|
519
721
|
);
|
|
520
722
|
}
|
|
521
723
|
|
|
522
|
-
|
|
724
|
+
// ONE read of stdin (readStdin() drains process.stdin to EOF), both fields
|
|
725
|
+
// derived from it — see readSessionStartPayload's doc comment for why a
|
|
726
|
+
// second read is fatal.
|
|
727
|
+
const { sessionId, source } = await readSessionStartPayload();
|
|
728
|
+
const isRecovery = isRecoverySource(source);
|
|
523
729
|
const sessionSegment = sessionId ? ` | session=${sessionId}` : '';
|
|
524
730
|
if (sessionId) log(HOOK, `Claude session: ${sessionId}`);
|
|
731
|
+
log(HOOK, `source=${source ?? 'unknown'} mode=${isRecovery ? 'recovery' : 'boot'}`);
|
|
525
732
|
|
|
526
733
|
const backlog = getBacklogSummary();
|
|
527
734
|
const tierInfo = await checkTier();
|
|
528
735
|
const tierSegment = formatTierSegment(tierInfo);
|
|
529
|
-
|
|
736
|
+
// TD-315: two separate blocks at two different offsets — see
|
|
737
|
+
// buildMemoryContext's doc comment for the measured reason.
|
|
738
|
+
const memoryContext = buildMemoryContext();
|
|
739
|
+
const matrixContext = buildMatrixContext();
|
|
530
740
|
|
|
531
741
|
// Sprint inbox check (RFC-027 S4 — only for sprint panes)
|
|
532
742
|
const sprintInboxContext = checkSprintInbox();
|
|
@@ -582,8 +792,15 @@ async function main(): Promise<void> {
|
|
|
582
792
|
// folded as extra lines into whichever outputContext() call below fires
|
|
583
793
|
// — never a SECOND outputContext call, so exactly one [ORCHESTRATOR-DATA]
|
|
584
794
|
// block is ever emitted (double-load regression guard).
|
|
795
|
+
//
|
|
796
|
+
// SPEC-S454 RF-002: skipped entirely when recovering. A compaction does not
|
|
797
|
+
// change the project binding — if none resolved, the prompt already fired at
|
|
798
|
+
// boot — and re-prompting mid-task is both an AskUserQuestion (which the
|
|
799
|
+
// recovery directive just forbade) and the interruption this fix exists to
|
|
800
|
+
// avoid. Skipping also spares the API round-trip inside the recovery path
|
|
801
|
+
// (RNF-001: no new I/O).
|
|
585
802
|
const projectSelectionPrompt =
|
|
586
|
-
PROJECT_SELECTION_ENABLED && projectBinding.source === 'none'
|
|
803
|
+
PROJECT_SELECTION_ENABLED && !isRecovery && projectBinding.source === 'none'
|
|
587
804
|
? await buildProjectSelectionPrompt()
|
|
588
805
|
: '';
|
|
589
806
|
|
|
@@ -604,17 +821,35 @@ async function main(): Promise<void> {
|
|
|
604
821
|
const workflow = await getActiveWorkflow();
|
|
605
822
|
|
|
606
823
|
if (!workflow) {
|
|
607
|
-
log(HOOK,
|
|
824
|
+
log(HOOK, `No active workflow, injecting ${isRecovery ? 'recovery' : 'greeting'} data`);
|
|
825
|
+
// SPEC-S454 RF-002: identical STATE, different FRAME. Only the leading
|
|
826
|
+
// token and the instruction block differ between boot and recovery — the
|
|
827
|
+
// capability matrix, memory context and sprint inbox are exactly what a
|
|
828
|
+
// compaction destroys, so they ride both paths unchanged.
|
|
829
|
+
// [LAST-SESSION]/[PENDENTE] stay boot-only: they orient someone arriving,
|
|
830
|
+
// and mid-task they are noise that reads like a session opening.
|
|
831
|
+
//
|
|
832
|
+
// TD-315 ORDER CONTRACT: the state line stays at offset 0 (the greeting
|
|
833
|
+
// keys off the leading token); [MEMORY-CONTEXT] comes IMMEDIATELY after
|
|
834
|
+
// it, before [LAST-SESSION] and everything else, so that a truncation of
|
|
835
|
+
// additionalContext takes narrative and routing hints before it takes
|
|
836
|
+
// memory. Pinned by tests/hooks/session-start-memory-before-narrative.test.ts.
|
|
837
|
+
const stateLine = `version=${backlog.version}${sessionSegment} | workflow=none${tierSegment}${sessionNumberSegment}${projectBindingSegment} | next=${backlog.next}`;
|
|
608
838
|
outputContext(
|
|
609
839
|
'SessionStart',
|
|
610
840
|
[
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
841
|
+
`${isRecovery ? '[ORCHESTRATOR-RECOVERY]' : '[ORCHESTRATOR-DATA]'} ${stateLine}`,
|
|
842
|
+
memoryContext,
|
|
843
|
+
...(isRecovery
|
|
844
|
+
? RECOVERY_DIRECTIVE
|
|
845
|
+
: [
|
|
846
|
+
backlog.lastSession ? `[LAST-SESSION] ${backlog.lastSession}` : '',
|
|
847
|
+
backlog.status ? `[PENDENTE] ${backlog.status}` : '',
|
|
848
|
+
`[ORCHESTRATOR] Greet the user via AskUserQuestion using the data above (orchestrator identity, MUST rule #1).`,
|
|
849
|
+
'Do NOT invoke /orchestrator skill for the greeting — use AskUserQuestion directly.',
|
|
850
|
+
'Invoke /orchestrator skill only when the user chooses to start a workflow.',
|
|
851
|
+
]),
|
|
852
|
+
matrixContext,
|
|
618
853
|
sprintInboxContext,
|
|
619
854
|
projectSelectionPrompt,
|
|
620
855
|
]
|
|
@@ -631,15 +866,26 @@ async function main(): Promise<void> {
|
|
|
631
866
|
? ` | pending=${action.pendingAction.agent} (${action.pendingAction.status})`
|
|
632
867
|
: '';
|
|
633
868
|
|
|
634
|
-
log(
|
|
869
|
+
log(
|
|
870
|
+
HOOK,
|
|
871
|
+
`Injecting workflow ${isRecovery ? 'recovery' : 'context'} (wf=${workflow.id} phase=${workflow.currentPhase})`
|
|
872
|
+
);
|
|
873
|
+
const stateLine = `version=${backlog.version}${sessionSegment} | workflow=${workflow.id} | type=${workflow.type} | phase=${workflow.currentPhase} | status=${workflow.status}${actionHint}${tierSegment}${sessionNumberSegment}${projectBindingSegment}`;
|
|
874
|
+
// TD-315: same order contract as the no-workflow path — memory right after
|
|
875
|
+
// the state line, matrix after the instruction block.
|
|
635
876
|
outputContext(
|
|
636
877
|
'SessionStart',
|
|
637
878
|
[
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
879
|
+
`${isRecovery ? '[ORCHESTRATOR-RECOVERY]' : '[ORCHESTRATOR-DATA]'} ${stateLine}`,
|
|
880
|
+
memoryContext,
|
|
881
|
+
...(isRecovery
|
|
882
|
+
? RECOVERY_DIRECTIVE
|
|
883
|
+
: [
|
|
884
|
+
`[ORCHESTRATOR] Active workflow detected. Greet the user via AskUserQuestion showing workflow state (orchestrator identity, MUST rule #1).`,
|
|
885
|
+
'Do NOT invoke /orchestrator skill for the greeting — use AskUserQuestion directly.',
|
|
886
|
+
'Invoke /orchestrator skill when the user chooses to continue the workflow.',
|
|
887
|
+
]),
|
|
888
|
+
matrixContext,
|
|
643
889
|
projectSelectionPrompt,
|
|
644
890
|
]
|
|
645
891
|
.filter(Boolean)
|