@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,304 @@
1
+ /**
2
+ * `DecisionTraceHook` — emit a decision-trace JSON per phase.
3
+ *
4
+ * TypeScript twin of `work_engine/hooks/builtin/decision_trace.py` (ADR-200
5
+ * py2ts — work_engine.hooks.builtin subpackage). Implements the v1 envelope
6
+ * from `docs/contracts/decision-trace-v1.md`. Default-off; opt-in via
7
+ * `.agent-settings.yml` `decision_engine.surface_traces: true`.
8
+ *
9
+ * The hook is purely observational — it never mutates `DeliveryState`, never
10
+ * raises terminal errors. Stream / disk failures surface as {@link HookError}
11
+ * (non-fatal per the three-tier contract).
12
+ */
13
+ import * as fs from 'node:fs';
14
+ import * as path from 'node:path';
15
+
16
+ import {
17
+ derive_confidence_band,
18
+ derive_risk_class,
19
+ summarise_memory,
20
+ summarise_verify,
21
+ } from '../../scoring/decision_trace.js';
22
+ import type { HookContext } from '../context.js';
23
+ import { HookEvent } from '../events.js';
24
+ import { HookError } from '../exceptions.js';
25
+ import type { HookRegistry } from '../registry.js';
26
+
27
+ /** Arbitrary value, mirroring the Python `Any` fields. */
28
+ type Any = unknown;
29
+
30
+ export const SCHEMA_VERSION = 1;
31
+ const _MAX_MEMORY_IDS = 32;
32
+
33
+ /**
34
+ * Emit one decision-trace JSON file per dispatcher step.
35
+ *
36
+ * `output_dir` is an optional override for the trace destination. When
37
+ * `null` the hook writes alongside the WorkState file: if the state file
38
+ * sits under `agents/runtime/state/work/<id>/state.json` the trace lands at
39
+ * `agents/runtime/state/work/<id>/decision-trace-<phase>.json`; otherwise the
40
+ * trace lands next to the state file as `<stem>.decision-trace-<phase>.json`.
41
+ */
42
+ export class DecisionTraceHook {
43
+ private readonly _output_dir: string | null;
44
+ private _state_file: string | null = null;
45
+ private _step_started: Map<string, number> = new Map();
46
+
47
+ constructor(output_dir: string | null = null) {
48
+ this._output_dir = output_dir;
49
+ }
50
+
51
+ /** Register the trace callbacks on the lifecycle events used. */
52
+ register(registry: HookRegistry): void {
53
+ registry.register(HookEvent.BEFORE_LOAD, (ctx) => this._capture_state_file(ctx));
54
+ registry.register(HookEvent.AFTER_LOAD, (ctx) => this._capture_state_file(ctx));
55
+ registry.register(HookEvent.BEFORE_STEP, (ctx) => this._mark_step_start(ctx));
56
+ registry.register(HookEvent.AFTER_STEP, (ctx) => this._emit_trace(ctx));
57
+ }
58
+
59
+ // -- lifecycle callbacks ------------------------------------------
60
+
61
+ private _capture_state_file(ctx: HookContext): void {
62
+ if (ctx.state_file !== null && ctx.state_file !== undefined) {
63
+ this._state_file = String(ctx.state_file);
64
+ }
65
+ }
66
+
67
+ private _mark_step_start(ctx: HookContext): void {
68
+ if (ctx.step_name) {
69
+ this._step_started.set(ctx.step_name, _time());
70
+ }
71
+ }
72
+
73
+ private _emit_trace(ctx: HookContext): void {
74
+ if (!ctx.step_name) {
75
+ return;
76
+ }
77
+ let started: number;
78
+ if (this._step_started.has(ctx.step_name)) {
79
+ started = this._step_started.get(ctx.step_name) as number;
80
+ this._step_started.delete(ctx.step_name);
81
+ } else {
82
+ started = _time();
83
+ }
84
+ const envelope = this._build_envelope(ctx, started);
85
+ const target = this._target_path(ctx.step_name);
86
+ try {
87
+ fs.mkdirSync(path.dirname(target), { recursive: true });
88
+ fs.writeFileSync(target, _pyJsonDumpsIndent2(envelope) + '\n', { encoding: 'utf-8' });
89
+ } catch (exc) {
90
+ throw new HookError(`decision-trace write failed: ${_osErr(exc)}`);
91
+ }
92
+ }
93
+
94
+ // -- envelope construction ----------------------------------------
95
+
96
+ private _build_envelope(ctx: HookContext, started: number): Record<string, Any> {
97
+ const delivery = ctx.delivery;
98
+ const memory = summarise_memory(_getattr(delivery, 'memory', null), { limit: _MAX_MEMORY_IDS });
99
+ const verify = summarise_verify(_getattr(delivery, 'verify', null));
100
+ const ambiguity = _pyTruthy(_getattr(delivery, 'questions', null));
101
+ return {
102
+ schema_version: SCHEMA_VERSION,
103
+ work_id: this._work_id(),
104
+ phase: ctx.step_name,
105
+ started_at: _iso_utc(started),
106
+ ended_at: _iso_utc(_time()),
107
+ confidence_band: derive_confidence_band({
108
+ memory_hits: memory['hits'] as number,
109
+ verify_claims: verify['claims'] as number,
110
+ verify_first_try_passes: verify['first_try_passes'] as number,
111
+ ambiguity_flag: ambiguity,
112
+ }),
113
+ risk_class: derive_risk_class(_getattr(delivery, 'changes', null)),
114
+ rules: [],
115
+ memory,
116
+ verify,
117
+ };
118
+ }
119
+
120
+ // -- path helpers --------------------------------------------------
121
+
122
+ private _work_id(): string {
123
+ if (this._state_file === null) {
124
+ return 'unknown';
125
+ }
126
+ const parent = path.dirname(this._state_file);
127
+ const parentName = path.basename(parent);
128
+ const grandName = path.basename(path.dirname(parent));
129
+ if (parentName && grandName === 'work') {
130
+ return parentName;
131
+ }
132
+ return _stem(this._state_file);
133
+ }
134
+
135
+ private _target_path(phase: string): string {
136
+ const filename = `decision-trace-${phase}.json`;
137
+ if (this._output_dir !== null) {
138
+ return path.join(this._output_dir, filename);
139
+ }
140
+ if (this._state_file === null) {
141
+ return filename;
142
+ }
143
+ const parent = path.dirname(this._state_file);
144
+ const parentName = path.basename(parent);
145
+ const grandName = path.basename(path.dirname(parent));
146
+ if (parentName && grandName === 'work') {
147
+ return path.join(parent, filename);
148
+ }
149
+ return path.join(parent, `${_stem(this._state_file)}.${filename}`);
150
+ }
151
+ }
152
+
153
+ /** `time.time()` — epoch seconds (float). */
154
+ function _time(): number {
155
+ return Date.now() / 1000;
156
+ }
157
+
158
+ /** Python `Path.stem` — filename without its final suffix. */
159
+ function _stem(p: string): string {
160
+ const base = path.basename(p);
161
+ const dot = base.lastIndexOf('.');
162
+ // Python `.stem`: drop the final extension; a leading dot is not a suffix.
163
+ if (dot <= 0) {
164
+ return base;
165
+ }
166
+ return base.slice(0, dot);
167
+ }
168
+
169
+ /** `datetime.fromtimestamp(epoch, tz=utc).strftime("%Y-%m-%dT%H:%M:%SZ")`. */
170
+ function _iso_utc(epoch: number): string {
171
+ const d = new Date(epoch * 1000);
172
+ const pad = (n: number, w = 2): string => String(n).padStart(w, '0');
173
+ return (
174
+ `${pad(d.getUTCFullYear(), 4)}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}` +
175
+ `T${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}Z`
176
+ );
177
+ }
178
+
179
+ /** Render an `OSError`-like value the way `str(exc)` would for the message. */
180
+ function _osErr(exc: unknown): string {
181
+ if (exc instanceof Error) {
182
+ return exc.message;
183
+ }
184
+ return String(exc);
185
+ }
186
+
187
+ /** Python `getattr(obj, name, default)`. */
188
+ function _getattr(obj: Any, name: string, dflt: Any): Any {
189
+ if (obj !== null && typeof obj === 'object' && name in (obj as object)) {
190
+ return (obj as Record<string, Any>)[name];
191
+ }
192
+ return dflt;
193
+ }
194
+
195
+ /** Python `bool(x)` truthiness for the `questions` shapes seen here. */
196
+ function _pyTruthy(value: Any): boolean {
197
+ if (value === null || value === undefined || value === false) {
198
+ return false;
199
+ }
200
+ if (value === true) {
201
+ return true;
202
+ }
203
+ if (typeof value === 'number') {
204
+ return value !== 0;
205
+ }
206
+ if (typeof value === 'string') {
207
+ return value.length > 0;
208
+ }
209
+ if (Array.isArray(value)) {
210
+ return value.length > 0;
211
+ }
212
+ if (value instanceof Map || value instanceof Set) {
213
+ return value.size > 0;
214
+ }
215
+ if (typeof value === 'object') {
216
+ return Object.keys(value as Record<string, unknown>).length > 0;
217
+ }
218
+ return true;
219
+ }
220
+
221
+ /**
222
+ * Python `json.dumps(obj, indent=2, sort_keys=False)` (ensure_ascii=True).
223
+ * Insertion-ordered keys, 2-space indent, `\uXXXX`-escaped non-ASCII.
224
+ */
225
+ function _pyJsonDumpsIndent2(value: Any): string {
226
+ return _dumpsIndent(value, 0);
227
+ }
228
+
229
+ function _dumpsIndent(value: Any, depth: number): string {
230
+ const pad = ' '.repeat(2 * (depth + 1));
231
+ const closePad = ' '.repeat(2 * depth);
232
+ if (value === null || value === undefined) {
233
+ return 'null';
234
+ }
235
+ if (typeof value === 'boolean') {
236
+ return value ? 'true' : 'false';
237
+ }
238
+ if (typeof value === 'number') {
239
+ return _jsonNum(value);
240
+ }
241
+ if (typeof value === 'string') {
242
+ return _jsonStrAscii(value);
243
+ }
244
+ if (Array.isArray(value)) {
245
+ if (value.length === 0) {
246
+ return '[]';
247
+ }
248
+ const items = value.map((v) => pad + _dumpsIndent(v, depth + 1));
249
+ return `[\n${items.join(',\n')}\n${closePad}]`;
250
+ }
251
+ if (typeof value === 'object') {
252
+ const obj = value as Record<string, Any>;
253
+ const keys = Object.keys(obj);
254
+ if (keys.length === 0) {
255
+ return '{}';
256
+ }
257
+ const items = keys.map((k) => `${pad}${_jsonStrAscii(k)}: ${_dumpsIndent(obj[k], depth + 1)}`);
258
+ return `{\n${items.join(',\n')}\n${closePad}}`;
259
+ }
260
+ return _jsonStrAscii(String(value));
261
+ }
262
+
263
+ /** Python `json.dumps` number formatting (integers without `.0`). */
264
+ function _jsonNum(n: number): string {
265
+ if (Number.isInteger(n)) {
266
+ return String(n);
267
+ }
268
+ return String(n);
269
+ }
270
+
271
+ /** Python `json.dumps(s, ensure_ascii=True)` for a single string. */
272
+ function _jsonStrAscii(s: string): string {
273
+ let out = '"';
274
+ for (const ch of s) {
275
+ const code = ch.codePointAt(0) as number;
276
+ if (ch === '"') {
277
+ out += '\\"';
278
+ } else if (ch === '\\') {
279
+ out += '\\\\';
280
+ } else if (ch === '\n') {
281
+ out += '\\n';
282
+ } else if (ch === '\r') {
283
+ out += '\\r';
284
+ } else if (ch === '\t') {
285
+ out += '\\t';
286
+ } else if (ch === '\b') {
287
+ out += '\\b';
288
+ } else if (ch === '\f') {
289
+ out += '\\f';
290
+ } else if (code < 0x20) {
291
+ out += `\\u${code.toString(16).padStart(4, '0')}`;
292
+ } else if (code < 0x7f) {
293
+ out += ch;
294
+ } else if (code <= 0xffff) {
295
+ out += `\\u${code.toString(16).padStart(4, '0')}`;
296
+ } else {
297
+ const c = code - 0x10000;
298
+ const hi = 0xd800 + (c >> 10);
299
+ const lo = 0xdc00 + (c & 0x3ff);
300
+ out += `\\u${hi.toString(16).padStart(4, '0')}\\u${lo.toString(16).padStart(4, '0')}`;
301
+ }
302
+ }
303
+ return `${out}"`;
304
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * `DirectiveSetGuardHook` — catch CLI / state directive-set drift.
3
+ *
4
+ * TypeScript twin of `work_engine/hooks/builtin/directive_set_guard.py`
5
+ * (ADR-200 py2ts — work_engine.hooks.builtin subpackage). Fires on
6
+ * `HookEvent.BEFORE_DISPATCH`. Compares the resolved `set_name` against the
7
+ * `directive_set` field on the persisted `WorkState`. Mismatch →
8
+ * {@link HookError} (non-fatal: the runner warns), so a flow that silently
9
+ * re-dispatches under a different set surfaces the drift before any step runs.
10
+ *
11
+ * The guard is read-only. It does not rewrite `state.directive_set`.
12
+ */
13
+ import type { HookContext } from '../context.js';
14
+ import { HookEvent } from '../events.js';
15
+ import { HookError } from '../exceptions.js';
16
+ import type { HookRegistry } from '../registry.js';
17
+
18
+ /** Arbitrary value, mirroring the Python `Any` fields. */
19
+ type Any = unknown;
20
+
21
+ /** Asserts `set_name` matches `state.directive_set` on dispatch. */
22
+ export class DirectiveSetGuardHook {
23
+ /** Register on `HookEvent.BEFORE_DISPATCH`. */
24
+ register(registry: HookRegistry): void {
25
+ registry.register(HookEvent.BEFORE_DISPATCH, (ctx) => this._guard(ctx));
26
+ }
27
+
28
+ private _guard(ctx: HookContext): void {
29
+ const set_name = ctx.set_name;
30
+ const work = ctx.work;
31
+ if (set_name === null || set_name === undefined || work === null || work === undefined) {
32
+ // `before_dispatch` always carries both refs per the context
33
+ // surface; missing means a hook-bug, not drift.
34
+ throw new HookError(
35
+ 'directive-set guard: missing set_name or work on ' +
36
+ `before_dispatch (set_name=${_pyRepr(set_name)}, work=${_pyRepr(work)})`,
37
+ );
38
+ }
39
+
40
+ const persisted = _getattr(work, 'directive_set', null);
41
+ if (persisted === null || persisted === undefined) {
42
+ // Legacy v0 envelopes have no `directive_set` field; the guard is
43
+ // a no-op for those — nothing to compare.
44
+ return;
45
+ }
46
+
47
+ if (persisted !== set_name) {
48
+ throw new HookError(
49
+ 'directive-set drift: CLI resolved ' +
50
+ `${_pyRepr(set_name)} but state carries ${_pyRepr(persisted)}`,
51
+ );
52
+ }
53
+ }
54
+ }
55
+
56
+ /** Python `getattr(obj, name, default)`. */
57
+ function _getattr(obj: Any, name: string, dflt: Any): Any {
58
+ if (obj !== null && typeof obj === 'object' && name in (obj as object)) {
59
+ return (obj as Record<string, Any>)[name];
60
+ }
61
+ return dflt;
62
+ }
63
+
64
+ /**
65
+ * Python `repr(x)` for the scalar shapes the guard formats. `None` →
66
+ * `None`; `str` → single-quoted (switching to double quotes only when the
67
+ * string contains a single quote but no double quote, matching CPython).
68
+ */
69
+ function _pyRepr(value: Any): string {
70
+ if (value === null || value === undefined) {
71
+ return 'None';
72
+ }
73
+ if (value === true) {
74
+ return 'True';
75
+ }
76
+ if (value === false) {
77
+ return 'False';
78
+ }
79
+ if (typeof value === 'string') {
80
+ return _reprStr(value);
81
+ }
82
+ return String(value);
83
+ }
84
+
85
+ function _reprStr(s: string): string {
86
+ const hasSingle = s.includes("'");
87
+ const hasDouble = s.includes('"');
88
+ const useDouble = hasSingle && !hasDouble;
89
+ const quote = useDouble ? '"' : "'";
90
+ let out = quote;
91
+ for (const ch of s) {
92
+ const code = ch.codePointAt(0) as number;
93
+ if (ch === '\\') {
94
+ out += '\\\\';
95
+ } else if (ch === quote) {
96
+ out += `\\${quote}`;
97
+ } else if (ch === '\n') {
98
+ out += '\\n';
99
+ } else if (ch === '\r') {
100
+ out += '\\r';
101
+ } else if (ch === '\t') {
102
+ out += '\\t';
103
+ } else if (code < 0x20 || code === 0x7f) {
104
+ out += `\\x${code.toString(16).padStart(2, '0')}`;
105
+ } else {
106
+ out += ch;
107
+ }
108
+ }
109
+ return out + quote;
110
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * `HaltSurfaceAuditHook` — defense-in-depth around halt surfaces.
3
+ *
4
+ * TypeScript twin of `work_engine/hooks/builtin/halt_surface_audit.py`
5
+ * (ADR-200 py2ts — work_engine.hooks.builtin subpackage). Fires on `on_halt`
6
+ * and re-asserts that every halt carries a non-empty user-facing surface,
7
+ * mirroring the dispatcher's `_validate_step_result`.
8
+ *
9
+ * Pure observability: emits {@link HookError} (non-fatal) when the surface is
10
+ * empty. The runner converts it to a warning so the violation is visible.
11
+ */
12
+ import type { HookContext } from '../context.js';
13
+ import { HookEvent } from '../events.js';
14
+ import { HookError } from '../exceptions.js';
15
+ import type { HookRegistry } from '../registry.js';
16
+
17
+ /** Arbitrary value, mirroring the Python `Any` fields. */
18
+ type Any = unknown;
19
+
20
+ /** Asserts that every halt carries a non-empty user-facing surface. */
21
+ export class HaltSurfaceAuditHook {
22
+ /** Register on `HookEvent.ON_HALT` only. */
23
+ register(registry: HookRegistry): void {
24
+ registry.register(HookEvent.ON_HALT, (ctx) => this._audit(ctx));
25
+ }
26
+
27
+ private _audit(ctx: HookContext): void {
28
+ const result = ctx.result;
29
+ if (result === null || result === undefined) {
30
+ // Hook-driven halts go through `_hook_halt_blocked` and may not
31
+ // carry a `StepResult` — the surface lives on `state.questions`
32
+ // instead. Audit that fallback too.
33
+ const questions = _getattr(ctx.delivery, 'questions', null);
34
+ if (!_pyTruthy(questions)) {
35
+ throw new HookError(
36
+ `halt at step ${_pyRepr(ctx.step_name)} surfaced no questions ` +
37
+ '(hook-driven halt with empty state.questions)',
38
+ );
39
+ }
40
+ return;
41
+ }
42
+
43
+ const questions = _getattr(result, 'questions', null);
44
+ if (!_pyTruthy(questions)) {
45
+ throw new HookError(
46
+ `halt at step ${_pyRepr(ctx.step_name)} surfaced no questions ` +
47
+ '(StepResult.questions empty); the user has nothing to act on',
48
+ );
49
+ }
50
+ }
51
+ }
52
+
53
+ /** Python `getattr(obj, name, default)`. */
54
+ function _getattr(obj: Any, name: string, dflt: Any): Any {
55
+ if (obj !== null && typeof obj === 'object' && name in (obj as object)) {
56
+ return (obj as Record<string, Any>)[name];
57
+ }
58
+ return dflt;
59
+ }
60
+
61
+ /** Python `bool(x)` truthiness for the `questions` shapes seen here. */
62
+ function _pyTruthy(value: Any): boolean {
63
+ if (value === null || value === undefined || value === false) {
64
+ return false;
65
+ }
66
+ if (value === true) {
67
+ return true;
68
+ }
69
+ if (typeof value === 'number') {
70
+ return value !== 0;
71
+ }
72
+ if (typeof value === 'string') {
73
+ return value.length > 0;
74
+ }
75
+ if (Array.isArray(value)) {
76
+ return value.length > 0;
77
+ }
78
+ if (value instanceof Map || value instanceof Set) {
79
+ return value.size > 0;
80
+ }
81
+ if (typeof value === 'object') {
82
+ return Object.keys(value as Record<string, unknown>).length > 0;
83
+ }
84
+ return true;
85
+ }
86
+
87
+ /** Python `repr(x)` for `step_name` (a string or `None`). */
88
+ function _pyRepr(value: Any): string {
89
+ if (value === null || value === undefined) {
90
+ return 'None';
91
+ }
92
+ if (typeof value === 'string') {
93
+ const hasSingle = value.includes("'");
94
+ const hasDouble = value.includes('"');
95
+ const quote = hasSingle && !hasDouble ? '"' : "'";
96
+ let out = quote;
97
+ for (const ch of value) {
98
+ const code = ch.codePointAt(0) as number;
99
+ if (ch === '\\') {
100
+ out += '\\\\';
101
+ } else if (ch === quote) {
102
+ out += `\\${quote}`;
103
+ } else if (ch === '\n') {
104
+ out += '\\n';
105
+ } else if (ch === '\r') {
106
+ out += '\\r';
107
+ } else if (ch === '\t') {
108
+ out += '\\t';
109
+ } else if (code < 0x20 || code === 0x7f) {
110
+ out += `\\x${code.toString(16).padStart(2, '0')}`;
111
+ } else {
112
+ out += ch;
113
+ }
114
+ }
115
+ return out + quote;
116
+ }
117
+ return String(value);
118
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Concrete observability hooks shipped with the engine.
3
+ *
4
+ * TypeScript twin of `work_engine/hooks/builtin/__init__.py` (ADR-200 py2ts —
5
+ * work_engine.hooks.builtin subpackage). Phase 4 hooks: low-risk, default-off,
6
+ * observe-only. Each hook exposes a `register(registry)` method so the
7
+ * registry stays the single source of truth for event → callback wiring.
8
+ */
9
+ export { ChatHistoryAppendHook } from './chat_history_append.js';
10
+ export { ChatHistoryHaltAppendHook } from './chat_history_halt_append.js';
11
+ export { DecisionGateHook, build_decision_gate_hook } from './decision_gate.js';
12
+ export { DecisionTraceHook } from './decision_trace.js';
13
+ export { DirectiveSetGuardHook } from './directive_set_guard.js';
14
+ export { HaltSurfaceAuditHook } from './halt_surface_audit.js';
15
+ export { MemoryVisibilityHook } from './memory_visibility.js';
16
+ export { StateShapeValidationHook } from './state_shape_validation.js';
17
+ export { TraceHook } from './trace.js';