principles-disciple 1.104.0 → 1.104.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/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/hooks/after-tool-call-helpers.ts +79 -89
- package/src/hooks/after-tool-call-types.ts +2 -8
- package/src/hooks/raw-observation-adapter.ts +231 -0
- package/src/hooks/raw-observation-types.ts +77 -0
- package/src/hooks/triage-adapter.ts +59 -52
- package/src/hooks/trigger-cooldown-tracker.ts +82 -0
- package/templates/langs/en/principles/THINKING_OS.md +12 -0
- package/templates/langs/zh/principles/THINKING_OS.md +12 -0
- package/tests/hooks/pain.test.ts +20 -14
- package/tests/hooks/raw-observation-adapter.test.ts +312 -0
- package/tests/hooks/single-gate-pain-admission.test.ts +258 -0
- package/tests/integration/auto-entry-gate.test.ts +13 -5
|
@@ -28,81 +28,54 @@ import {
|
|
|
28
28
|
// ── Source Kind Resolution ───────────────────────────────────────────────────
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
* Map
|
|
31
|
+
* Map RawObservation to SourceKind.
|
|
32
32
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* - failureSource: 'dispatch_error' vs 'tool_failure'
|
|
36
|
-
* - isRisky + score: only used for rulehost_block upgrade, not for kind resolution
|
|
33
|
+
* This is the unified entry point for source-kind classification.
|
|
34
|
+
* It replaces the scattered resolveSourceKindFrom* functions.
|
|
37
35
|
*/
|
|
38
|
-
export
|
|
39
|
-
toolName: string | undefined,
|
|
40
|
-
failureSource: 'tool_failure' | 'dispatch_error',
|
|
41
|
-
provenance?: 'openclaw_context_bound' | 'owner_reported_no_host_trace' | 'automatic_hook',
|
|
42
|
-
): SourceKind {
|
|
43
|
-
// Manual pain via agent tool call
|
|
44
|
-
if (toolName === 'pain' || toolName === 'skill:pain') {
|
|
45
|
-
return provenance === 'openclaw_context_bound' ? 'agent_on_owner_request' : 'owner_reported';
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Dispatch errors (tool not found, unknown tool)
|
|
49
|
-
if (failureSource === 'dispatch_error') {
|
|
50
|
-
return 'dispatch_error';
|
|
51
|
-
}
|
|
36
|
+
export { resolveSourceKind } from './raw-observation-adapter.js';
|
|
52
37
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Map after_tool_call hook context to SourceKind.
|
|
40
|
+
*
|
|
41
|
+
* @deprecated Use resolveSourceKind directly with RawObservation.
|
|
42
|
+
*/
|
|
43
|
+
export { resolveSourceKindFromToolFailure } from './raw-observation-adapter.js';
|
|
56
44
|
|
|
57
45
|
/**
|
|
58
46
|
* Map empathy/semantic detection context to SourceKind.
|
|
59
47
|
*
|
|
60
|
-
*
|
|
61
|
-
* - 'llm_paralysis' → llm_paralysis
|
|
62
|
-
* - 'llm_*' (detection rule) → semantic
|
|
63
|
-
* - 'user_empathy' or empathy keyword match → empathy_inferred
|
|
64
|
-
* - GFI threshold crossed → gfi_threshold
|
|
48
|
+
* @deprecated Use resolveSourceKind directly with RawObservation.
|
|
65
49
|
*/
|
|
66
|
-
export
|
|
67
|
-
detectionSource: string,
|
|
68
|
-
isGfiTriggered: boolean,
|
|
69
|
-
): SourceKind {
|
|
70
|
-
if (isGfiTriggered) return 'gfi_threshold';
|
|
71
|
-
if (detectionSource === 'llm_paralysis') return 'llm_paralysis';
|
|
72
|
-
if (detectionSource.startsWith('llm_')) return 'semantic';
|
|
73
|
-
if (detectionSource === 'user_empathy') return 'empathy_inferred';
|
|
74
|
-
return 'unknown';
|
|
75
|
-
}
|
|
50
|
+
export { resolveSourceKindFromLlmDetection } from './raw-observation-adapter.js';
|
|
76
51
|
|
|
77
52
|
/**
|
|
78
53
|
* Map gate-block context to SourceKind.
|
|
54
|
+
*
|
|
55
|
+
* @deprecated Use resolveSourceKind directly with RawObservation.
|
|
79
56
|
*/
|
|
80
|
-
export
|
|
81
|
-
return 'rulehost_block';
|
|
82
|
-
}
|
|
57
|
+
export { resolveSourceKindFromGateBlock } from './raw-observation-adapter.js';
|
|
83
58
|
|
|
84
59
|
/**
|
|
85
60
|
* Map /pd-pain command to SourceKind.
|
|
61
|
+
*
|
|
62
|
+
* @deprecated Use resolveSourceKind directly with RawObservation.
|
|
86
63
|
*/
|
|
87
|
-
export
|
|
88
|
-
return 'owner_reported';
|
|
89
|
-
}
|
|
64
|
+
export { resolveSourceKindFromCommand } from './raw-observation-adapter.js';
|
|
90
65
|
|
|
91
66
|
/**
|
|
92
67
|
* Map provider/rate-limit failure to SourceKind.
|
|
68
|
+
*
|
|
69
|
+
* @deprecated Use resolveSourceKind directly with RawObservation.
|
|
93
70
|
*/
|
|
94
|
-
export
|
|
95
|
-
isRateLimit: boolean,
|
|
96
|
-
): SourceKind {
|
|
97
|
-
return isRateLimit ? 'rate_limit' : 'provider_failure';
|
|
98
|
-
}
|
|
71
|
+
export { resolveSourceKindFromProvider } from './raw-observation-adapter.js';
|
|
99
72
|
|
|
100
73
|
/**
|
|
101
74
|
* Map subagent error to SourceKind.
|
|
75
|
+
*
|
|
76
|
+
* @deprecated Use resolveSourceKind directly with RawObservation.
|
|
102
77
|
*/
|
|
103
|
-
export
|
|
104
|
-
return 'subagent_error';
|
|
105
|
-
}
|
|
78
|
+
export { resolveSourceKindFromSubagent } from './raw-observation-adapter.js';
|
|
106
79
|
|
|
107
80
|
// ── Triage Evaluation ───────────────────────────────────────────────────────
|
|
108
81
|
|
|
@@ -123,6 +96,8 @@ export function evaluateEvidenceTriage(
|
|
|
123
96
|
options?: {
|
|
124
97
|
isUnsafeHighConfidence?: boolean;
|
|
125
98
|
provenance?: 'openclaw_context_bound' | 'owner_reported_no_host_trace' | 'automatic_hook';
|
|
99
|
+
consecutiveErrors?: number;
|
|
100
|
+
isRisky?: boolean;
|
|
126
101
|
},
|
|
127
102
|
): TriageResult {
|
|
128
103
|
const input: TriageInput = {
|
|
@@ -132,7 +107,39 @@ export function evaluateEvidenceTriage(
|
|
|
132
107
|
provenance: options?.provenance,
|
|
133
108
|
};
|
|
134
109
|
|
|
135
|
-
|
|
110
|
+
let result = evaluateTriage(input);
|
|
111
|
+
|
|
112
|
+
// PEAT-B1 upgrade logic: risky high-score overrides evidence_only
|
|
113
|
+
// Matches PainDiagnosticGate.risky_high_score: isRisky && score >= 70 → admit
|
|
114
|
+
if (
|
|
115
|
+
result.decision === 'evidence_only' &&
|
|
116
|
+
options?.isRisky === true &&
|
|
117
|
+
score >= 70
|
|
118
|
+
) {
|
|
119
|
+
result = {
|
|
120
|
+
...result,
|
|
121
|
+
decision: 'admit',
|
|
122
|
+
reason: 'Risky high-score operation overrides evidence-only decision. Immediate diagnosis required.',
|
|
123
|
+
nextAction: 'create_diagnostic_task',
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// PEAT-B1 upgrade logic: repeated failures override evidence_only
|
|
128
|
+
// Threshold: 4 consecutive failures (matches PainDiagnosticGate.repeatedFailure)
|
|
129
|
+
if (
|
|
130
|
+
result.decision === 'evidence_only' &&
|
|
131
|
+
options?.consecutiveErrors !== undefined &&
|
|
132
|
+
options.consecutiveErrors >= 4
|
|
133
|
+
) {
|
|
134
|
+
result = {
|
|
135
|
+
...result,
|
|
136
|
+
decision: 'admit',
|
|
137
|
+
reason: 'Repeated failures override evidence-only decision. Pattern suggests systemic issue requiring diagnosis.',
|
|
138
|
+
nextAction: 'create_diagnostic_task',
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return result;
|
|
136
143
|
}
|
|
137
144
|
|
|
138
145
|
// ── High-Confidence Unsafe Action Detection ──────────────────────────────────
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trigger Cooldown Tracker — PRI-363
|
|
3
|
+
*
|
|
4
|
+
* Manages cooldown state for trigger controller decisions.
|
|
5
|
+
*
|
|
6
|
+
* This is a plugin-layer concern because:
|
|
7
|
+
* - Core (trigger-controller) is stateless and pure
|
|
8
|
+
* - Cooldown state needs to persist across tool calls
|
|
9
|
+
* - The map is scoped to the plugin's lifecycle
|
|
10
|
+
*
|
|
11
|
+
* EP-05: Loop State Freshness — each check reads fresh state from the map.
|
|
12
|
+
* ERR-001: No `as` casts on map access.
|
|
13
|
+
* ERR-002: Every rejected decision includes reason + nextAction.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const DEFAULT_COOLDOWN_MS = 15 * 60 * 1000; // 15 minutes
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Episode key format: sessionId:source:errorHash
|
|
20
|
+
*/
|
|
21
|
+
function buildEpisodeKey(
|
|
22
|
+
source: string,
|
|
23
|
+
sessionId: string | undefined,
|
|
24
|
+
errorHash: string | undefined,
|
|
25
|
+
): string {
|
|
26
|
+
const sid = sessionId || 'unknown';
|
|
27
|
+
const hash = errorHash || 'no-hash';
|
|
28
|
+
return `${sid}:${source}:${hash}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Check whether cooldown is currently active for a given episode.
|
|
33
|
+
*/
|
|
34
|
+
export function isCooldownActive(
|
|
35
|
+
source: string,
|
|
36
|
+
sessionId: string | undefined,
|
|
37
|
+
errorHash: string | undefined,
|
|
38
|
+
cooldownMap: ReadonlyMap<string, number>,
|
|
39
|
+
): boolean {
|
|
40
|
+
const episodeKey = buildEpisodeKey(source, sessionId, errorHash);
|
|
41
|
+
const lastDiagnosedAt = cooldownMap.get(episodeKey);
|
|
42
|
+
|
|
43
|
+
if (lastDiagnosedAt === undefined) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
return now - lastDiagnosedAt < DEFAULT_COOLDOWN_MS;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Mark an episode as diagnosed (set cooldown timestamp).
|
|
53
|
+
*/
|
|
54
|
+
export function markEpisodeAsDiagnosed(
|
|
55
|
+
source: string,
|
|
56
|
+
sessionId: string | undefined,
|
|
57
|
+
errorHash: string | undefined,
|
|
58
|
+
cooldownMap: Map<string, number>,
|
|
59
|
+
): void {
|
|
60
|
+
const episodeKey = buildEpisodeKey(source, sessionId, errorHash);
|
|
61
|
+
cooldownMap.set(episodeKey, Date.now());
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Clear all cooldown state (for tests).
|
|
66
|
+
*/
|
|
67
|
+
export function clearCooldownState(cooldownMap: Map<string, number>): void {
|
|
68
|
+
cooldownMap.clear();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get the cooldown timestamp for a given episode (for tests).
|
|
73
|
+
*/
|
|
74
|
+
export function getCooldownTimestamp(
|
|
75
|
+
source: string,
|
|
76
|
+
sessionId: string | undefined,
|
|
77
|
+
errorHash: string | undefined,
|
|
78
|
+
cooldownMap: ReadonlyMap<string, number>,
|
|
79
|
+
): number | undefined {
|
|
80
|
+
const episodeKey = buildEpisodeKey(source, sessionId, errorHash);
|
|
81
|
+
return cooldownMap.get(episodeKey);
|
|
82
|
+
}
|
|
@@ -62,4 +62,16 @@ LLMs are highly sensitive to XML tags; this structure is designed to boost instr
|
|
|
62
62
|
<must>Maintain extreme digital cleanliness. The project root is SACRED. Use strict `kebab-case` for all naming. Clean up all test scripts and debug artifacts after the task.</must>
|
|
63
63
|
<forbidden>Creating arbitrary temporary files (e.g., `test.txt`, `temp.md`, `debug.log`) in the project root directory.</forbidden>
|
|
64
64
|
</directive>
|
|
65
|
+
|
|
66
|
+
<directive id="T-09" name="DIVIDE_AND_CONQUER">
|
|
67
|
+
<trigger>When facing a complex task, multi-step change, or an operation that can be decomposed.</trigger>
|
|
68
|
+
<must>Break the task into smaller, manageable phases before execution. Execute one phase at a time and verify each phase's result before proceeding.</must>
|
|
69
|
+
<forbidden>Attempting to execute a large, complex change in a single step, or proceeding without a decomposition plan.</forbidden>
|
|
70
|
+
</directive>
|
|
71
|
+
|
|
72
|
+
<directive id="T-10" name="MEMORY_EXTERNALIZATION">
|
|
73
|
+
<trigger>When reaching a significant conclusion, making a decision, or planning next steps across sessions.</trigger>
|
|
74
|
+
<must>Write intermediate conclusions, decisions, and plans to persistent files (e.g., plan.md, scratchpad) so they survive context compression and session boundaries.</must>
|
|
75
|
+
<forbidden>Relying solely on conversation context to retain important state that will be lost when the context window shifts.</forbidden>
|
|
76
|
+
</directive>
|
|
65
77
|
</thinking_os_core_directives>
|
|
@@ -62,4 +62,16 @@
|
|
|
62
62
|
<must>保持极致的数字洁癖。项目根目录是神圣的。所有命名必须严格使用 `kebab-case`。任务结束后清理所有的测试脚本和 Debug 遗留物。</must>
|
|
63
63
|
<forbidden>在项目根目录下随意创建临时文件(如 `test.txt`、`temp.md`、`debug.log`)。</forbidden>
|
|
64
64
|
</directive>
|
|
65
|
+
|
|
66
|
+
<directive id="T-09" name="DIVIDE_AND_CONQUER">
|
|
67
|
+
<trigger>当面对复杂任务、多步骤变更或可分解的操作时。</trigger>
|
|
68
|
+
<must>在执行前将任务拆分为更小的、可管理的阶段。逐阶段执行,并在进入下一阶段前验证当前阶段的结果。</must>
|
|
69
|
+
<forbidden>试图在单一步骤中执行大型复杂变更,或在没有分解计划的情况下直接推进。</forbidden>
|
|
70
|
+
</directive>
|
|
71
|
+
|
|
72
|
+
<directive id="T-10" name="MEMORY_EXTERNALIZATION">
|
|
73
|
+
<trigger>当得出重要结论、做出决策或规划跨会话的后续步骤时。</trigger>
|
|
74
|
+
<must>将中间结论、决策和计划写入持久化文件(如 plan.md、scratchpad),使其在上下文压缩和会话边界后仍然可用。</must>
|
|
75
|
+
<forbidden>仅依赖对话上下文来保持重要状态,这些状态在上下文窗口切换时将丢失。</forbidden>
|
|
76
|
+
</directive>
|
|
65
77
|
</thinking_os_core_directives>
|
package/tests/hooks/pain.test.ts
CHANGED
|
@@ -7,7 +7,7 @@ import * as ioUtils from '../../src/utils/io.js';
|
|
|
7
7
|
import { WorkspaceContext } from '../../src/core/workspace-context.js';
|
|
8
8
|
import { EventLogService } from '../../src/core/event-log.js';
|
|
9
9
|
import { setInjectedProbationIds, clearSession } from '../../src/core/session-tracker.js';
|
|
10
|
-
import {
|
|
10
|
+
import { resetTriggerCooldownForTest } from '../../src/hooks/after-tool-call-helpers.js';
|
|
11
11
|
import { loadFeatureFlagFromConfig } from '../../src/core/pd-config-loader.js';
|
|
12
12
|
|
|
13
13
|
vi.mock('fs');
|
|
@@ -140,12 +140,12 @@ describe('Post-Write Checks & Pain Hook', () => {
|
|
|
140
140
|
mockEmitSync.mockReset();
|
|
141
141
|
mockRecordProbationFeedback.mockReset();
|
|
142
142
|
mockUpdatePrincipleValueMetrics.mockReset();
|
|
143
|
-
vi.spyOn(WorkspaceContext, '
|
|
143
|
+
vi.spyOn(WorkspaceContext, 'fromHookContextExplicit').mockReturnValue(mockWctx as any);
|
|
144
144
|
vi.spyOn(EventLogService, 'get').mockReturnValue(mockEventLog as any);
|
|
145
145
|
clearSession('s-success');
|
|
146
146
|
clearSession('s-low-value-failure');
|
|
147
147
|
clearSession('s-repeated-failure');
|
|
148
|
-
|
|
148
|
+
resetTriggerCooldownForTest();
|
|
149
149
|
});
|
|
150
150
|
|
|
151
151
|
afterEach(() => {
|
|
@@ -158,7 +158,7 @@ describe('Post-Write Checks & Pain Hook', () => {
|
|
|
158
158
|
handleAfterToolCall(mockEvent as any, mockCtx as any);
|
|
159
159
|
|
|
160
160
|
// Should still create context
|
|
161
|
-
expect(WorkspaceContext.
|
|
161
|
+
expect(WorkspaceContext.fromHookContextExplicit).toHaveBeenCalled();
|
|
162
162
|
expect(fs.writeFileSync).not.toHaveBeenCalled();
|
|
163
163
|
expect(mockEmitSync).not.toHaveBeenCalled();
|
|
164
164
|
});
|
|
@@ -178,7 +178,7 @@ describe('Post-Write Checks & Pain Hook', () => {
|
|
|
178
178
|
|
|
179
179
|
handleAfterToolCall(mockEvent as any, mockCtx as any, mockApi as any);
|
|
180
180
|
|
|
181
|
-
expect(WorkspaceContext.
|
|
181
|
+
expect(WorkspaceContext.fromHookContextExplicit).not.toHaveBeenCalled();
|
|
182
182
|
expect(mockEmitSync).not.toHaveBeenCalled();
|
|
183
183
|
});
|
|
184
184
|
|
|
@@ -221,6 +221,13 @@ describe('Post-Write Checks & Pain Hook', () => {
|
|
|
221
221
|
vi.mocked(ioUtils.isRisky).mockReturnValue(false);
|
|
222
222
|
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
223
223
|
|
|
224
|
+
// PRI-363: trigger controller requires consecutiveErrors >= 4 for upgrade
|
|
225
|
+
handleAfterToolCall(mockEvent as any, mockCtx as any);
|
|
226
|
+
expect(mockEmitSync).not.toHaveBeenCalled();
|
|
227
|
+
|
|
228
|
+
handleAfterToolCall(mockEvent as any, mockCtx as any);
|
|
229
|
+
expect(mockEmitSync).not.toHaveBeenCalled();
|
|
230
|
+
|
|
224
231
|
handleAfterToolCall(mockEvent as any, mockCtx as any);
|
|
225
232
|
expect(mockEmitSync).not.toHaveBeenCalled();
|
|
226
233
|
|
|
@@ -231,7 +238,6 @@ describe('Post-Write Checks & Pain Hook', () => {
|
|
|
231
238
|
data: expect.objectContaining({
|
|
232
239
|
painType: 'tool_failure',
|
|
233
240
|
source: 'write',
|
|
234
|
-
reason: expect.stringContaining('diagnosticGate=high_gfi'),
|
|
235
241
|
}),
|
|
236
242
|
}));
|
|
237
243
|
expect(mockWctx.trajectory.recordToolCall).toHaveBeenCalledWith(expect.objectContaining({
|
|
@@ -309,6 +315,8 @@ describe('Post-Write Checks & Pain Hook', () => {
|
|
|
309
315
|
contextTags: ['write'],
|
|
310
316
|
});
|
|
311
317
|
|
|
318
|
+
// PRI-363: risky high-score write triggers admission via trigger controller
|
|
319
|
+
// (isRisky=true + score >= 70 → risky_high_score upgrade → admit)
|
|
312
320
|
handleAfterToolCall(mockEvent as any, mockCtx as any);
|
|
313
321
|
|
|
314
322
|
expect(mockUpdatePrincipleValueMetrics).toHaveBeenCalledWith(
|
|
@@ -652,7 +660,7 @@ describe('PRI-326: evaluatePainAdmissionForToolCall', () => {
|
|
|
652
660
|
|
|
653
661
|
beforeEach(() => {
|
|
654
662
|
vi.clearAllMocks();
|
|
655
|
-
|
|
663
|
+
resetTriggerCooldownForTest();
|
|
656
664
|
vi.mocked(loadFeatureFlagFromConfig).mockReturnValue({ enabled: false, source: 'test' });
|
|
657
665
|
});
|
|
658
666
|
|
|
@@ -672,29 +680,27 @@ describe('PRI-326: evaluatePainAdmissionForToolCall', () => {
|
|
|
672
680
|
expect(result.stage).toBe('not_applicable');
|
|
673
681
|
});
|
|
674
682
|
|
|
675
|
-
it('returns
|
|
683
|
+
it('returns trigger_rejected when tool_failure triage rejects', () => {
|
|
676
684
|
vi.mocked(loadFeatureFlagFromConfig).mockReturnValue({ enabled: true, source: 'test' });
|
|
677
685
|
|
|
678
686
|
const result = evaluatePainAdmissionForToolCall(
|
|
679
687
|
{ toolName: 'write' } as any, baseObservation, baseOutcome, undefined, undefined, 's1', workspaceDir, mockConfig
|
|
680
688
|
);
|
|
681
|
-
expect(result.stage).toBe('
|
|
689
|
+
expect(result.stage).toBe('trigger_rejected');
|
|
682
690
|
expect(result.admitted).toBe(false);
|
|
683
691
|
expect(result.reason).toBeTruthy();
|
|
684
692
|
});
|
|
685
693
|
|
|
686
|
-
it('returns
|
|
694
|
+
it('returns trigger_admitted when consecutive errors exceed repeatedFailure threshold', () => {
|
|
687
695
|
vi.mocked(loadFeatureFlagFromConfig).mockReturnValue({ enabled: false, source: 'test' });
|
|
688
|
-
// consecutiveErrors=5 >= default repeatedFailure threshold of 4 →
|
|
696
|
+
// consecutiveErrors=5 >= default repeatedFailure threshold of 4 → trigger admits via repeated_failure
|
|
689
697
|
const highConsecutiveState = { currentGfi: 0, consecutiveErrors: 5, lastErrorHash: 'abc123' } as any;
|
|
690
698
|
|
|
691
699
|
const result = evaluatePainAdmissionForToolCall(
|
|
692
700
|
{ toolName: 'write' } as any, baseObservation, baseOutcome, highConsecutiveState, undefined, 's-gate-admitted-test', workspaceDir, mockConfig
|
|
693
701
|
);
|
|
694
|
-
expect(result.stage).toBe('
|
|
702
|
+
expect(result.stage).toBe('trigger_admitted');
|
|
695
703
|
expect(result.admitted).toBe(true);
|
|
696
|
-
expect(result.gateResult?.shouldDiagnose).toBe(true);
|
|
697
|
-
expect(result.gateResult?.reason).toBe('repeated_failure');
|
|
698
704
|
});
|
|
699
705
|
|
|
700
706
|
it('includes reason and detail in every decision', () => {
|