@event4u/agent-config 8.4.0 → 8.4.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 (126) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +6 -0
  3. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  4. package/dist/discovery/deprecation-report.md +1 -1
  5. package/dist/discovery/discovery-manifest.json +1 -1
  6. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  7. package/dist/discovery/discovery-manifest.summary.md +1 -1
  8. package/dist/discovery/orphan-report.md +1 -1
  9. package/dist/discovery/packs.json +1 -1
  10. package/dist/discovery/trust-report.md +1 -1
  11. package/dist/discovery/workspaces.json +1 -1
  12. package/dist/mcp/registry-manifest.json +1 -1
  13. package/package.json +4 -1
  14. package/src/agent-src/README.md +64 -0
  15. package/src/agent-src/scripts/archive_completed_roadmaps.ts +497 -0
  16. package/src/agent-src/scripts/scan-seeder-violations.php +145 -0
  17. package/src/agent-src/scripts/update_roadmap_progress.ts +1087 -0
  18. package/src/agent-src/templates/AGENTS.md +34 -0
  19. package/src/agent-src/templates/scripts/README.md +195 -0
  20. package/src/agent-src/templates/scripts/check_memory.ts +662 -0
  21. package/src/agent-src/templates/scripts/check_memory_proposal.ts +370 -0
  22. package/src/agent-src/templates/scripts/historical-bug-patterns.example.yml +84 -0
  23. package/src/agent-src/templates/scripts/implement_ticket/__main__.ts +27 -0
  24. package/src/agent-src/templates/scripts/memory_hash.ts +355 -0
  25. package/src/agent-src/templates/scripts/memory_lookup.ts +1089 -0
  26. package/src/agent-src/templates/scripts/memory_report.ts +868 -0
  27. package/src/agent-src/templates/scripts/memory_signal.ts +444 -0
  28. package/src/agent-src/templates/scripts/memory_status.ts +215 -0
  29. package/src/agent-src/templates/scripts/ownership-map.example.yml +87 -0
  30. package/src/agent-src/templates/scripts/pr-risk-config.example.yml +76 -0
  31. package/src/agent-src/templates/scripts/pr_review_routing.ts +544 -0
  32. package/src/agent-src/templates/scripts/pr_risk_review.ts +0 -0
  33. package/src/agent-src/templates/scripts/telemetry/aggregator.ts +0 -0
  34. package/src/agent-src/templates/scripts/telemetry/boundary.ts +164 -0
  35. package/src/agent-src/templates/scripts/telemetry/engagement.ts +479 -0
  36. package/src/agent-src/templates/scripts/telemetry/report_renderer.ts +394 -0
  37. package/src/agent-src/templates/scripts/telemetry/settings.ts +210 -0
  38. package/src/agent-src/templates/scripts/telemetry_record.ts +276 -0
  39. package/src/agent-src/templates/scripts/telemetry_report.ts +211 -0
  40. package/src/agent-src/templates/scripts/telemetry_status.ts +333 -0
  41. package/src/agent-src/templates/scripts/tier_usage_report.ts +619 -0
  42. package/src/agent-src/templates/scripts/work_engine/__main__.ts +14 -0
  43. package/src/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +1261 -0
  44. package/src/agent-src/templates/scripts/work_engine/_lib/user_global_paths.ts +329 -0
  45. package/src/agent-src/templates/scripts/work_engine/cli.ts +206 -0
  46. package/src/agent-src/templates/scripts/work_engine/cli_args.ts +249 -0
  47. package/src/agent-src/templates/scripts/work_engine/delivery_state.ts +225 -0
  48. package/src/agent-src/templates/scripts/work_engine/directives/backend/analyze.ts +125 -0
  49. package/src/agent-src/templates/scripts/work_engine/directives/backend/implement.ts +189 -0
  50. package/src/agent-src/templates/scripts/work_engine/directives/backend/index.ts +94 -0
  51. package/src/agent-src/templates/scripts/work_engine/directives/backend/memory.ts +193 -0
  52. package/src/agent-src/templates/scripts/work_engine/directives/backend/plan.ts +267 -0
  53. package/src/agent-src/templates/scripts/work_engine/directives/backend/refine.ts +518 -0
  54. package/src/agent-src/templates/scripts/work_engine/directives/backend/report.ts +379 -0
  55. package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +268 -0
  56. package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +258 -0
  57. package/src/agent-src/templates/scripts/work_engine/directives/index.ts +32 -0
  58. package/src/agent-src/templates/scripts/work_engine/directives/mixed/contract.ts +243 -0
  59. package/src/agent-src/templates/scripts/work_engine/directives/mixed/index.ts +108 -0
  60. package/src/agent-src/templates/scripts/work_engine/directives/mixed/stitch.ts +259 -0
  61. package/src/agent-src/templates/scripts/work_engine/directives/mixed/ui.ts +216 -0
  62. package/src/agent-src/templates/scripts/work_engine/directives/ui/_passthrough.ts +40 -0
  63. package/src/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +241 -0
  64. package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +216 -0
  65. package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +506 -0
  66. package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +325 -0
  67. package/src/agent-src/templates/scripts/work_engine/directives/ui/index.ts +102 -0
  68. package/src/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +462 -0
  69. package/src/agent-src/templates/scripts/work_engine/directives/ui/review.ts +474 -0
  70. package/src/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +352 -0
  71. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/_skipped.ts +33 -0
  72. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/apply.ts +213 -0
  73. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/index.ts +111 -0
  74. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/refine.ts +126 -0
  75. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/report.ts +112 -0
  76. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/test.ts +164 -0
  77. package/src/agent-src/templates/scripts/work_engine/dispatcher.ts +515 -0
  78. package/src/agent-src/templates/scripts/work_engine/emitters.ts +119 -0
  79. package/src/agent-src/templates/scripts/work_engine/errors.ts +24 -0
  80. package/src/agent-src/templates/scripts/work_engine/hook_bootstrap.ts +104 -0
  81. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/_chat_history_base.ts +189 -0
  82. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_append.ts +41 -0
  83. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_halt_append.ts +89 -0
  84. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/decision_gate.ts +193 -0
  85. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/decision_trace.ts +304 -0
  86. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/directive_set_guard.ts +110 -0
  87. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/halt_surface_audit.ts +118 -0
  88. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/index.ts +17 -0
  89. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/memory_visibility.ts +161 -0
  90. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/state_shape_validation.ts +45 -0
  91. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/trace.ts +134 -0
  92. package/src/agent-src/templates/scripts/work_engine/hooks/context.ts +94 -0
  93. package/src/agent-src/templates/scripts/work_engine/hooks/events.ts +58 -0
  94. package/src/agent-src/templates/scripts/work_engine/hooks/exceptions.ts +85 -0
  95. package/src/agent-src/templates/scripts/work_engine/hooks/index.ts +27 -0
  96. package/src/agent-src/templates/scripts/work_engine/hooks/registry.ts +66 -0
  97. package/src/agent-src/templates/scripts/work_engine/hooks/runner.ts +90 -0
  98. package/src/agent-src/templates/scripts/work_engine/hooks/settings.ts +260 -0
  99. package/src/agent-src/templates/scripts/work_engine/input_builders.ts +260 -0
  100. package/src/agent-src/templates/scripts/work_engine/intent/classify.ts +466 -0
  101. package/src/agent-src/templates/scripts/work_engine/migration/v0_to_v1.ts +531 -0
  102. package/src/agent-src/templates/scripts/work_engine/orchestration.ts +366 -0
  103. package/src/agent-src/templates/scripts/work_engine/persona_policy.ts +97 -0
  104. package/src/agent-src/templates/scripts/work_engine/resolvers/diff.ts +135 -0
  105. package/src/agent-src/templates/scripts/work_engine/resolvers/file.ts +175 -0
  106. package/src/agent-src/templates/scripts/work_engine/resolvers/prompt.ts +115 -0
  107. package/src/agent-src/templates/scripts/work_engine/scoring/confidence.ts +415 -0
  108. package/src/agent-src/templates/scripts/work_engine/scoring/decision_engine.ts +466 -0
  109. package/src/agent-src/templates/scripts/work_engine/scoring/decision_trace.ts +298 -0
  110. package/src/agent-src/templates/scripts/work_engine/scoring/memory_visibility.ts +444 -0
  111. package/src/agent-src/templates/scripts/work_engine/stack/detect.ts +252 -0
  112. package/src/agent-src/templates/scripts/work_engine/stack/runner.ts +745 -0
  113. package/src/agent-src/templates/scripts/work_engine/state.ts +1151 -0
  114. package/src/agent-src/templates/scripts/work_engine/state_io.ts +413 -0
  115. package/src/install/atomic.ts +111 -0
  116. package/src/install/conflict.ts +226 -0
  117. package/src/install/detect.ts +280 -0
  118. package/src/install/paths.ts +90 -0
  119. package/src/install/plan.ts +205 -0
  120. package/src/install/preflight.ts +210 -0
  121. package/src/install/selectedTools.ts +52 -0
  122. package/src/install/toolDetection.ts +120 -0
  123. package/src/install/txlog.ts +161 -0
  124. package/src/install/types.ts +175 -0
  125. package/src/install/wizard-plan.ts +216 -0
  126. package/src/scripts/prepack-check.mjs +108 -5
@@ -0,0 +1,466 @@
1
+ /**
2
+ * Decision-engine gates — schema, validation, and per-phase evaluation.
3
+ *
4
+ * TypeScript twin of `work_engine/scoring/decision_engine.py` (ADR-200 py2ts
5
+ * Phase 1 — work_engine scoring subpackage). Public API names stay snake_case
6
+ * to mirror the Python module 1:1 (per ADR-200 — Python style is part of the
7
+ * contract).
8
+ *
9
+ * Reads the optional `decision_engine:` block from `.agent-settings.yml`.
10
+ * Absent block = current behaviour (observe-only, no gates fire).
11
+ *
12
+ * Schema (all keys optional; the parser rejects unknown keys hard):
13
+ *
14
+ * - `surface_traces` (bool, default `false`) — opt-in for
15
+ * `DecisionTraceHook`. Predates the gates; lives here so the
16
+ * `decision_engine:` block has one source-of-truth schema.
17
+ * - `min_confidence` (`low`/`medium`/`high`/`off`, default
18
+ * `off`) — confidence-band floor; Phase=Plan refuses to advance
19
+ * when the band is below.
20
+ * - `block_on_risk` (`low`/`medium`/`high`/`off`, default
21
+ * `off`) — risk-class ceiling; Phase=Implement refuses to advance
22
+ * when risk exceeds.
23
+ * - `require_memory_hits` (bool, default `false`) — Phase=Refine
24
+ * demands `memory_hits >= 1`.
25
+ * - `on_block` (`stop`/`ask`/`warn`, default `stop`) —
26
+ * what happens when a gate fires.
27
+ * - `ask_timeout_seconds` (int, default `30`) — timeout when
28
+ * `on_block=ask` runs in a non-interactive context (no TTY, or
29
+ * `CI=true`).
30
+ * - `on_block_fallback` (`stop`/`warn`, default `stop`) —
31
+ * resolution after `ask_timeout` elapses.
32
+ *
33
+ * Gate-conflict resolution (first match wins, only one gate fires per
34
+ * phase):
35
+ *
36
+ * 1. `block_on_risk` (highest impact)
37
+ * 2. `require_memory_hits`
38
+ * 3. `min_confidence` (lowest impact)
39
+ *
40
+ * See `docs/contracts/decision-engine-gates.md` for the full
41
+ * priority matrix.
42
+ */
43
+
44
+ /** Arbitrary JSON-ish value, mirroring the Python `Any` fields. */
45
+ export type Any = unknown;
46
+
47
+ export const ALLOWED_KEYS: ReadonlySet<string> = new Set([
48
+ 'surface_traces',
49
+ 'min_confidence',
50
+ 'block_on_risk',
51
+ 'require_memory_hits',
52
+ 'on_block',
53
+ 'ask_timeout_seconds',
54
+ 'on_block_fallback',
55
+ ]);
56
+
57
+ const _LEVEL_VALUES: ReadonlySet<string> = new Set(['low', 'medium', 'high', 'off']);
58
+ const _LEVEL_RANK: Record<string, number> = { low: 1, medium: 2, high: 3 };
59
+ const _ON_BLOCK_VALUES: ReadonlySet<string> = new Set(['stop', 'ask', 'warn']);
60
+ const _FALLBACK_VALUES: ReadonlySet<string> = new Set(['stop', 'warn']);
61
+
62
+ /**
63
+ * Conflict-resolution order. Highest-impact gate first; the first
64
+ * firing gate emits its reason and downstream gates are skipped.
65
+ */
66
+ export const GATE_PRIORITY: readonly string[] = [
67
+ 'block_on_risk',
68
+ 'require_memory_hits',
69
+ 'min_confidence',
70
+ ] as const;
71
+
72
+ const _PHASE_FOR_GATE: Record<string, string> = {
73
+ block_on_risk: 'implement',
74
+ require_memory_hits: 'refine',
75
+ min_confidence: 'plan',
76
+ };
77
+
78
+ /** Raised when the `decision_engine:` block is malformed. */
79
+ export class DecisionEngineConfigError extends Error {
80
+ constructor(message?: string) {
81
+ super(message);
82
+ Object.setPrototypeOf(this, DecisionEngineConfigError.prototype);
83
+ this.name = 'DecisionEngineConfigError';
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Resolved `decision_engine:` block. Frozen to keep gate evaluations
89
+ * replay-stable.
90
+ */
91
+ export class DecisionEngineSettings {
92
+ readonly surface_traces: boolean;
93
+ readonly min_confidence: string;
94
+ readonly block_on_risk: string;
95
+ readonly require_memory_hits: boolean;
96
+ readonly on_block: string;
97
+ readonly ask_timeout_seconds: number;
98
+ readonly on_block_fallback: string;
99
+
100
+ constructor(args: {
101
+ surface_traces?: boolean;
102
+ min_confidence?: string;
103
+ block_on_risk?: string;
104
+ require_memory_hits?: boolean;
105
+ on_block?: string;
106
+ ask_timeout_seconds?: number;
107
+ on_block_fallback?: string;
108
+ } = {}) {
109
+ this.surface_traces = args.surface_traces ?? false;
110
+ this.min_confidence = args.min_confidence ?? 'off';
111
+ this.block_on_risk = args.block_on_risk ?? 'off';
112
+ this.require_memory_hits = args.require_memory_hits ?? false;
113
+ this.on_block = args.on_block ?? 'stop';
114
+ this.ask_timeout_seconds = args.ask_timeout_seconds ?? 30;
115
+ this.on_block_fallback = args.on_block_fallback ?? 'stop';
116
+ Object.freeze(this);
117
+ }
118
+
119
+ /** True when at least one gate is enabled. */
120
+ get any_gate_active(): boolean {
121
+ return (
122
+ this.min_confidence !== 'off' ||
123
+ this.block_on_risk !== 'off' ||
124
+ this.require_memory_hits
125
+ );
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Outcome of one gate evaluation. `action` is the resolved
131
+ * response after applying `on_block` plus the non-TTY fallback.
132
+ */
133
+ export class GateDecision {
134
+ readonly gate_id: string;
135
+ readonly phase: string;
136
+ readonly reason: string;
137
+ readonly action: string; // "stop" | "warn" | "ask" | "ask_timeout"
138
+
139
+ constructor(args: { gate_id: string; phase: string; reason: string; action: string }) {
140
+ this.gate_id = args.gate_id;
141
+ this.phase = args.phase;
142
+ this.reason = args.reason;
143
+ this.action = args.action;
144
+ Object.freeze(this);
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Parse a `decision_engine` block into validated settings.
150
+ *
151
+ * Returns defaults when `data` is `null`/`undefined` (block absent) or an
152
+ * empty mapping. Raises {@link DecisionEngineConfigError} on unknown keys or
153
+ * invalid values.
154
+ */
155
+ export function parse(data: Any): DecisionEngineSettings {
156
+ if (data === null || data === undefined) {
157
+ return new DecisionEngineSettings();
158
+ }
159
+ if (!_isPlainDict(data)) {
160
+ throw new DecisionEngineConfigError(
161
+ 'decision_engine: must be a mapping, got ' + _pyTypeName(data),
162
+ );
163
+ }
164
+ const d = data as Record<string, unknown>;
165
+ const unknown: string[] = [];
166
+ for (const k of Object.keys(d)) {
167
+ if (!ALLOWED_KEYS.has(k)) {
168
+ unknown.push(k);
169
+ }
170
+ }
171
+ if (unknown.length > 0) {
172
+ throw new DecisionEngineConfigError(
173
+ 'decision_engine: unknown key(s): ' +
174
+ _sortedStr(unknown).join(', ') +
175
+ '. Allowed: ' +
176
+ _sortedStr([...ALLOWED_KEYS]).join(', '),
177
+ );
178
+ }
179
+ return new DecisionEngineSettings({
180
+ surface_traces: _coerce_bool(_get(d, 'surface_traces', undefined), false),
181
+ min_confidence: _coerce_level(_get(d, 'min_confidence', 'off'), 'min_confidence'),
182
+ block_on_risk: _coerce_level(_get(d, 'block_on_risk', 'off'), 'block_on_risk'),
183
+ require_memory_hits: _coerce_bool(_get(d, 'require_memory_hits', undefined), false),
184
+ on_block: _coerce_choice(_get(d, 'on_block', 'stop'), 'on_block', _ON_BLOCK_VALUES),
185
+ ask_timeout_seconds: _coerce_int(_get(d, 'ask_timeout_seconds', 30), 'ask_timeout_seconds'),
186
+ on_block_fallback: _coerce_choice(
187
+ _get(d, 'on_block_fallback', 'stop'),
188
+ 'on_block_fallback',
189
+ _FALLBACK_VALUES,
190
+ ),
191
+ });
192
+ }
193
+
194
+ function _coerce_bool(value: Any, dflt: boolean): boolean {
195
+ if (typeof value === 'boolean') {
196
+ return value;
197
+ }
198
+ if (value === null || value === undefined) {
199
+ return dflt;
200
+ }
201
+ if (typeof value === 'string') {
202
+ const s = value.trim().toLowerCase();
203
+ if (s === 'true' || s === 'yes' || s === 'on' || s === '1') {
204
+ return true;
205
+ }
206
+ if (s === 'false' || s === 'no' || s === 'off' || s === '0') {
207
+ return false;
208
+ }
209
+ }
210
+ throw new DecisionEngineConfigError(`decision_engine.${_pyRepr(value)}: expected bool`);
211
+ }
212
+
213
+ function _coerce_level(value: Any, key: string): string {
214
+ if (value === null || value === undefined) {
215
+ return 'off';
216
+ }
217
+ // YAML 1.1 parses unquoted `off` as boolean False; accept it as the off
218
+ // sentinel so writers don't have to quote. Boolean True stays rejected.
219
+ if (typeof value === 'boolean') {
220
+ if (value === false) {
221
+ return 'off';
222
+ }
223
+ throw new DecisionEngineConfigError(
224
+ `decision_engine.${key}: boolean True is not a valid level ` +
225
+ '(quote a string: low/medium/high/off)',
226
+ );
227
+ }
228
+ if (typeof value !== 'string') {
229
+ throw new DecisionEngineConfigError(
230
+ `decision_engine.${key}: expected string, got ` + _pyTypeName(value),
231
+ );
232
+ }
233
+ const s = value.trim().toLowerCase();
234
+ if (!_LEVEL_VALUES.has(s)) {
235
+ throw new DecisionEngineConfigError(
236
+ `decision_engine.${key}: invalid value ${_pyRepr(value)}. ` +
237
+ 'Allowed: ' +
238
+ _sortedStr([..._LEVEL_VALUES]).join(', '),
239
+ );
240
+ }
241
+ return s;
242
+ }
243
+
244
+ function _coerce_choice(value: Any, key: string, allowed: ReadonlySet<string>): string {
245
+ if (typeof value !== 'string') {
246
+ throw new DecisionEngineConfigError(
247
+ `decision_engine.${key}: expected string, got ` + _pyTypeName(value),
248
+ );
249
+ }
250
+ const s = value.trim().toLowerCase();
251
+ if (!allowed.has(s)) {
252
+ throw new DecisionEngineConfigError(
253
+ `decision_engine.${key}: invalid value ${_pyRepr(value)}. ` +
254
+ 'Allowed: ' +
255
+ _sortedStr([...allowed]).join(', '),
256
+ );
257
+ }
258
+ return s;
259
+ }
260
+
261
+ function _coerce_int(value: Any, key: string): number {
262
+ if (typeof value === 'boolean') {
263
+ throw new DecisionEngineConfigError(`decision_engine.${key}: expected int, got bool`);
264
+ }
265
+ if (typeof value === 'number' && Number.isInteger(value)) {
266
+ if (value < 0) {
267
+ throw new DecisionEngineConfigError(`decision_engine.${key}: must be >= 0`);
268
+ }
269
+ return value;
270
+ }
271
+ throw new DecisionEngineConfigError(
272
+ `decision_engine.${key}: expected int, got ` + _pyTypeName(value),
273
+ );
274
+ }
275
+
276
+ /**
277
+ * Evaluate gates for `phase`. Returns the first firing gate, or `null` when
278
+ * no gate fires.
279
+ *
280
+ * Conflict resolution follows {@link GATE_PRIORITY} — only the first matching
281
+ * gate's phase is considered. Each gate maps to exactly one phase via the
282
+ * internal phase map.
283
+ */
284
+ export function evaluate_gates(
285
+ settings: DecisionEngineSettings,
286
+ args: {
287
+ phase: string;
288
+ confidence_band: string | null;
289
+ risk_class: string | null;
290
+ memory_hits: number;
291
+ is_interactive?: (() => boolean) | null;
292
+ },
293
+ ): GateDecision | null {
294
+ const { phase, confidence_band, risk_class, memory_hits } = args;
295
+ const is_interactive = args.is_interactive ?? null;
296
+ if (!settings.any_gate_active) {
297
+ return null;
298
+ }
299
+ for (const gate_id of GATE_PRIORITY) {
300
+ if (_PHASE_FOR_GATE[gate_id] !== phase) {
301
+ continue;
302
+ }
303
+ const decision = _evaluate_single(gate_id, settings, {
304
+ confidence_band,
305
+ risk_class,
306
+ memory_hits,
307
+ });
308
+ if (decision !== null) {
309
+ const action = _resolve_action(settings, is_interactive);
310
+ return new GateDecision({
311
+ gate_id: decision.gate_id,
312
+ phase: decision.phase,
313
+ reason: decision.reason,
314
+ action,
315
+ });
316
+ }
317
+ }
318
+ return null;
319
+ }
320
+
321
+ function _evaluate_single(
322
+ gate_id: string,
323
+ settings: DecisionEngineSettings,
324
+ args: {
325
+ confidence_band: string | null;
326
+ risk_class: string | null;
327
+ memory_hits: number;
328
+ },
329
+ ): GateDecision | null {
330
+ const { confidence_band, risk_class, memory_hits } = args;
331
+ if (gate_id === 'min_confidence' && settings.min_confidence !== 'off') {
332
+ // min_confidence is validated to low/medium/high here, all in _LEVEL_RANK.
333
+ const floor = _LEVEL_RANK[settings.min_confidence] ?? 0;
334
+ const actual = _LEVEL_RANK[(confidence_band ?? '').toLowerCase()] ?? 0;
335
+ if (actual < floor) {
336
+ return new GateDecision({
337
+ gate_id,
338
+ phase: 'plan',
339
+ action: 'stop',
340
+ reason:
341
+ `confidence_band=${_pyRepr(confidence_band)} below floor ` +
342
+ `min_confidence=${_pyRepr(settings.min_confidence)}`,
343
+ });
344
+ }
345
+ } else if (gate_id === 'block_on_risk' && settings.block_on_risk !== 'off') {
346
+ // block_on_risk is validated to low/medium/high here, all in _LEVEL_RANK.
347
+ const ceiling = _LEVEL_RANK[settings.block_on_risk] ?? 0;
348
+ const actual = _LEVEL_RANK[(risk_class ?? '').toLowerCase()] ?? 0;
349
+ if (actual >= ceiling) {
350
+ return new GateDecision({
351
+ gate_id,
352
+ phase: 'implement',
353
+ action: 'stop',
354
+ reason:
355
+ `risk_class=${_pyRepr(risk_class)} at/above ceiling ` +
356
+ `block_on_risk=${_pyRepr(settings.block_on_risk)}`,
357
+ });
358
+ }
359
+ } else if (gate_id === 'require_memory_hits' && settings.require_memory_hits) {
360
+ if (memory_hits < 1) {
361
+ return new GateDecision({
362
+ gate_id,
363
+ phase: 'refine',
364
+ action: 'stop',
365
+ reason:
366
+ `memory_hits=${memory_hits} but ` +
367
+ 'require_memory_hits=true (need >= 1)',
368
+ });
369
+ }
370
+ }
371
+ return null;
372
+ }
373
+
374
+ /**
375
+ * Map `on_block` to an action, applying the non-TTY fallback.
376
+ *
377
+ * Non-interactive context = either `is_interactive()` returns False, or the
378
+ * `CI` env var is truthy. `on_block=ask` collapses to `ask_timeout` (consumer
379
+ * applies `on_block_fallback`).
380
+ */
381
+ function _resolve_action(
382
+ settings: DecisionEngineSettings,
383
+ is_interactive: (() => boolean) | null,
384
+ ): string {
385
+ if (settings.on_block === 'stop' || settings.on_block === 'warn') {
386
+ return settings.on_block;
387
+ }
388
+ const interactive =
389
+ is_interactive !== null ? is_interactive() : _default_is_interactive();
390
+ if (interactive) {
391
+ return 'ask';
392
+ }
393
+ return 'ask_timeout';
394
+ }
395
+
396
+ function _default_is_interactive(): boolean {
397
+ const ci = (process.env.CI ?? '').trim().toLowerCase();
398
+ if (ci === '1' || ci === 'true' || ci === 'yes') {
399
+ return false;
400
+ }
401
+ // Python: `sys.stdin.isatty() and sys.stdout.isatty()`.
402
+ return Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
403
+ }
404
+
405
+ // ── Python-parity primitives ────────────────────────────────────────────
406
+
407
+ /** `dict.get(key, default)` for a plain object. */
408
+ function _get(obj: Record<string, unknown>, key: string, dflt: unknown): unknown {
409
+ return key in obj ? obj[key] : dflt;
410
+ }
411
+
412
+ /** Python `isinstance(x, dict)` — plain object only (not array, not null). */
413
+ function _isPlainDict(value: unknown): boolean {
414
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
415
+ }
416
+
417
+ /**
418
+ * Python `type(x).__name__` for the error-message shapes this module emits:
419
+ * `list`, `str`, `int`, `float`, `bool`, `dict`, `NoneType`.
420
+ */
421
+ function _pyTypeName(value: unknown): string {
422
+ if (value === null || value === undefined) {
423
+ return 'NoneType';
424
+ }
425
+ if (Array.isArray(value)) {
426
+ return 'list';
427
+ }
428
+ switch (typeof value) {
429
+ case 'string':
430
+ return 'str';
431
+ case 'boolean':
432
+ return 'bool';
433
+ case 'number':
434
+ return Number.isInteger(value) ? 'int' : 'float';
435
+ case 'object':
436
+ return 'dict';
437
+ default:
438
+ return typeof value;
439
+ }
440
+ }
441
+
442
+ /**
443
+ * Python `repr(x)` for the scalar shapes the error messages interpolate via
444
+ * `{value!r}` — strings are single-quoted, `None` → `None`, booleans →
445
+ * `True`/`False`, numbers as-is.
446
+ */
447
+ function _pyRepr(value: unknown): string {
448
+ if (value === null || value === undefined) {
449
+ return 'None';
450
+ }
451
+ if (value === true) {
452
+ return 'True';
453
+ }
454
+ if (value === false) {
455
+ return 'False';
456
+ }
457
+ if (typeof value === 'string') {
458
+ return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
459
+ }
460
+ return String(value);
461
+ }
462
+
463
+ /** Python `sorted(list_of_str)` — code-point ascending. */
464
+ function _sortedStr(values: string[]): string[] {
465
+ return [...values].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
466
+ }