@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,104 @@
1
+ /**
2
+ * Lifecycle-hook registry assembly for the CLI entry point.
3
+ *
4
+ * TypeScript twin of `work_engine/hook_bootstrap.py` (ADR-200 py2ts Phase 1 —
5
+ * work_engine TOP/integration layer). Public API names stay snake_case to
6
+ * mirror the Python module 1:1 (per ADR-200 — Python style is part of the
7
+ * contract).
8
+ *
9
+ * Extracted from `cli.py` in P2.3 of `road-to-post-pr29-optimize.md`. Owns
10
+ * nothing but `_build_hook_registry` and its chat-history helper.
11
+ */
12
+
13
+ import type { ParsedArgs } from './cli_args.js';
14
+ import { HookRegistry } from './hooks/index.js';
15
+ import {
16
+ ChatHistoryAppendHook,
17
+ ChatHistoryHaltAppendHook,
18
+ DecisionTraceHook,
19
+ DirectiveSetGuardHook,
20
+ HaltSurfaceAuditHook,
21
+ MemoryVisibilityHook,
22
+ StateShapeValidationHook,
23
+ TraceHook,
24
+ build_decision_gate_hook,
25
+ } from './hooks/builtin/index.js';
26
+ import type { HookSettings} from './hooks/settings.js';
27
+ import { load_hook_settings } from './hooks/settings.js';
28
+
29
+ /**
30
+ * Build the CLI-side {@link HookRegistry} for one `main()` run.
31
+ *
32
+ * Reads `hooks.*` from `.agent-settings.yml` and registers the enabled hooks.
33
+ * The master switch `hooks.enabled` defaults to `false` when the block (or the
34
+ * file) is missing — the registry stays empty and golden replay flows are
35
+ * byte-stable.
36
+ *
37
+ * `--no-hooks` on the CLI forces an empty registry regardless of settings,
38
+ * which is the explicit escape hatch golden-replay test harnesses can use.
39
+ */
40
+ export function _build_hook_registry(args: ParsedArgs): HookRegistry {
41
+ const registry = new HookRegistry();
42
+ if (_getattr(args, 'no_hooks', false)) {
43
+ return registry;
44
+ }
45
+
46
+ const settings_path = _getattr(args, 'hooks_config', null) as string | null;
47
+ const settings = load_hook_settings(settings_path);
48
+ if (!settings.enabled) {
49
+ return registry;
50
+ }
51
+
52
+ if (settings.trace) {
53
+ new TraceHook().register(registry);
54
+ }
55
+ if (settings.halt_surface_audit) {
56
+ new HaltSurfaceAuditHook().register(registry);
57
+ }
58
+ if (settings.state_shape_validation) {
59
+ new StateShapeValidationHook().register(registry);
60
+ }
61
+ if (settings.directive_set_guard) {
62
+ new DirectiveSetGuardHook().register(registry);
63
+ }
64
+ if (settings.decision_trace) {
65
+ new DecisionTraceHook().register(registry);
66
+ }
67
+ const gate_hook = build_decision_gate_hook(settings.decision_engine);
68
+ if (gate_hook !== null) {
69
+ gate_hook.register(registry);
70
+ }
71
+ if (settings.memory_visibility) {
72
+ new MemoryVisibilityHook({
73
+ memory_cadence: settings.memory_cadence,
74
+ visibility_off: settings.memory_visibility_off,
75
+ }).register(registry);
76
+ }
77
+ if (settings.chat_history_enabled) {
78
+ _register_chat_history_hooks(registry, settings);
79
+ }
80
+
81
+ return registry;
82
+ }
83
+
84
+ /**
85
+ * Register the structural chat-history hooks bound to the configured script.
86
+ *
87
+ * Hook-only contract (post road-to-chat-history-hook-only): only the append +
88
+ * halt-append hooks remain; cooperative `turn-check` / `heartbeat` hooks were
89
+ * removed when the cooperative always-rules were retired.
90
+ */
91
+ export function _register_chat_history_hooks(registry: HookRegistry, settings: HookSettings): void {
92
+ const script = settings.chat_history_script;
93
+ new ChatHistoryAppendHook(script).register(registry);
94
+ new ChatHistoryHaltAppendHook(script).register(registry);
95
+ }
96
+
97
+ /** Python `getattr(obj, attr, default)` for plain object attribute access. */
98
+ function _getattr(obj: unknown, attr: string, dflt: unknown): unknown {
99
+ if (obj !== null && typeof obj === 'object' && attr in (obj as Record<string, unknown>)) {
100
+ const v = (obj as Record<string, unknown>)[attr];
101
+ return v === undefined ? dflt : v;
102
+ }
103
+ return dflt;
104
+ }
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Shared plumbing for chat-history hooks.
3
+ *
4
+ * TypeScript twin of `work_engine/hooks/builtin/_chat_history_base.py`
5
+ * (ADR-200 py2ts — work_engine.hooks.builtin subpackage). Subprocess-driven
6
+ * so the work-engine package stays decoupled from `scripts/chat_history.py`'s
7
+ * internals. The `runner` injection point is the test seam — production passes
8
+ * the default runner, tests pass a fake.
9
+ */
10
+ import { spawnSync, type SpawnSyncReturns } from 'node:child_process';
11
+ import * as fs from 'node:fs';
12
+ import * as path from 'node:path';
13
+ import * as process from 'node:process';
14
+ import { fileURLToPath } from 'node:url';
15
+
16
+ /**
17
+ * Subset of Python's `subprocess.CompletedProcess[str]` the hooks read:
18
+ * the return code plus the captured streams.
19
+ */
20
+ export interface CompletedProcess {
21
+ returncode: number;
22
+ stdout: string;
23
+ stderr: string;
24
+ }
25
+
26
+ /** Callable that runs a subprocess. Production default: {@link _default_runner}. */
27
+ export type ProcessRunner = (cmd: string[]) => CompletedProcess;
28
+
29
+ export const EXIT_OK = 0;
30
+ export const EXIT_MISSING = 10;
31
+ export const EXIT_FOREIGN = 11;
32
+ export const EXIT_RETURNING = 12;
33
+
34
+ export function _default_runner(cmd: string[]): CompletedProcess {
35
+ const [program, ...args] = cmd;
36
+ const proc: SpawnSyncReturns<string> = spawnSync(program as string, args, {
37
+ encoding: 'utf8',
38
+ });
39
+ return {
40
+ // `null` status (signal/spawn failure) maps to a non-OK code.
41
+ returncode: proc.status === null ? -1 : proc.status,
42
+ stdout: proc.stdout ?? '',
43
+ stderr: proc.stderr ?? '',
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Resolve the invocation for the chat-history script as a TypeScript twin.
49
+ *
50
+ * The chat-history script ships as a `.ts` file run through `tsx` — no
51
+ * python3 dependency. A `.py` `script_path` (the historical default /
52
+ * config value) is mapped to its `.ts` sibling. The `tsx` binary is found
53
+ * by walking up from the script's directory to a `node_modules/.bin/tsx`,
54
+ * falling back to `npx tsx`. Mirrors `dispatch_hook.ts::_resolve_tsx_invocation`
55
+ * and `run.ts::resolveTsxInvocation`.
56
+ */
57
+ export function _resolve_chat_history_invocation(script_path: string): string[] {
58
+ const tsScript = script_path.replace(/\.py$/, '.ts');
59
+ const binName = process.platform === 'win32' ? 'tsx.cmd' : 'tsx';
60
+ const walkUp = (start: string): string | null => {
61
+ let dir = start;
62
+ for (;;) {
63
+ const candidate = path.join(dir, 'node_modules', '.bin', binName);
64
+ try {
65
+ if (fs.statSync(candidate).isFile()) {
66
+ return candidate;
67
+ }
68
+ } catch {
69
+ // not here — keep walking up.
70
+ }
71
+ const parent = path.dirname(dir);
72
+ if (parent === dir) {
73
+ return null;
74
+ }
75
+ dir = parent;
76
+ }
77
+ };
78
+ // Script's tree first, then THIS module's package tree (the package
79
+ // ships tsx as a runtime dependency). `npx tsx` stays last-resort only:
80
+ // it runs against the consumer's npm config and fails hard on e.g.
81
+ // devEngines pins (the 8.1.0 EBADDEVENGINES regression).
82
+ const tsx =
83
+ walkUp(path.dirname(path.resolve(tsScript))) ??
84
+ walkUp(path.dirname(fileURLToPath(import.meta.url)));
85
+ if (tsx !== null) {
86
+ return [tsx, tsScript];
87
+ }
88
+ return ['npx', 'tsx', tsScript];
89
+ }
90
+
91
+ /**
92
+ * Shared plumbing — script path and runner.
93
+ *
94
+ * Schema v4 derives session attribution from the platform `session_id`,
95
+ * not from a derived first-user-msg. work-engine internal hooks have no
96
+ * platform session in scope, so they omit `--session-id` and entries land
97
+ * in the `<unknown>` session bucket.
98
+ */
99
+ export class _ChatHistoryHookBase {
100
+ readonly script_path: string;
101
+ private readonly _runner: ProcessRunner;
102
+
103
+ constructor(script_path: string, options: { runner?: ProcessRunner | null } = {}) {
104
+ this.script_path = script_path;
105
+ this._runner = options.runner ?? _default_runner;
106
+ }
107
+
108
+ protected _invoke(...args: string[]): CompletedProcess {
109
+ // Run the chat-history `.ts` script through `tsx` — no python3
110
+ // dependency. `script_path` (a `.py` default / config value) maps to
111
+ // its `.ts` sibling, prefixed by the resolved `tsx` binary.
112
+ const cmd = [..._resolve_chat_history_invocation(this.script_path), ...args];
113
+ return this._runner(cmd);
114
+ }
115
+ }
116
+
117
+ /** Python `getattr(obj, name, default)` for a duck-typed object. */
118
+ export function _getattr(obj: unknown, name: string, dflt: unknown): unknown {
119
+ if (obj !== null && typeof obj === 'object' && name in (obj as object)) {
120
+ return (obj as Record<string, unknown>)[name];
121
+ }
122
+ return dflt;
123
+ }
124
+
125
+ /**
126
+ * Python `json.dumps(obj)` with default separators (`", "`, `": "`) and
127
+ * `ensure_ascii=True`. Covers the simple `{"step": str}` /
128
+ * `{"step": str, "questions": [str, …]}` payloads the chat-history hooks
129
+ * build: strings, numbers, booleans, null, arrays, and plain objects in
130
+ * insertion order.
131
+ */
132
+ export function _pyJsonDumps(value: unknown): string {
133
+ if (value === null || value === undefined) {
134
+ return 'null';
135
+ }
136
+ if (typeof value === 'boolean') {
137
+ return value ? 'true' : 'false';
138
+ }
139
+ if (typeof value === 'number') {
140
+ return String(value);
141
+ }
142
+ if (typeof value === 'string') {
143
+ return _jsonStrAscii(value);
144
+ }
145
+ if (Array.isArray(value)) {
146
+ return `[${value.map((v) => _pyJsonDumps(v)).join(', ')}]`;
147
+ }
148
+ if (typeof value === 'object') {
149
+ const obj = value as Record<string, unknown>;
150
+ const items = Object.keys(obj).map((k) => `${_jsonStrAscii(k)}: ${_pyJsonDumps(obj[k])}`);
151
+ return `{${items.join(', ')}}`;
152
+ }
153
+ return _jsonStrAscii(String(value));
154
+ }
155
+
156
+ /** Python `json.dumps(s, ensure_ascii=True)` for a single string. */
157
+ function _jsonStrAscii(s: string): string {
158
+ let out = '"';
159
+ for (const ch of s) {
160
+ const code = ch.codePointAt(0) as number;
161
+ if (ch === '"') {
162
+ out += '\\"';
163
+ } else if (ch === '\\') {
164
+ out += '\\\\';
165
+ } else if (ch === '\n') {
166
+ out += '\\n';
167
+ } else if (ch === '\r') {
168
+ out += '\\r';
169
+ } else if (ch === '\t') {
170
+ out += '\\t';
171
+ } else if (ch === '\b') {
172
+ out += '\\b';
173
+ } else if (ch === '\f') {
174
+ out += '\\f';
175
+ } else if (code < 0x20) {
176
+ out += `\\u${code.toString(16).padStart(4, '0')}`;
177
+ } else if (code < 0x7f) {
178
+ out += ch;
179
+ } else if (code <= 0xffff) {
180
+ out += `\\u${code.toString(16).padStart(4, '0')}`;
181
+ } else {
182
+ const c = code - 0x10000;
183
+ const hi = 0xd800 + (c >> 10);
184
+ const lo = 0xdc00 + (c & 0x3ff);
185
+ out += `\\u${hi.toString(16).padStart(4, '0')}\\u${lo.toString(16).padStart(4, '0')}`;
186
+ }
187
+ }
188
+ return `${out}"`;
189
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * `ChatHistoryAppendHook` — phase-boundary persistence.
3
+ *
4
+ * TypeScript twin of `work_engine/hooks/builtin/chat_history_append.py`
5
+ * (ADR-200 py2ts — work_engine.hooks.builtin subpackage). Fires on
6
+ * `after_step`. Appends a `--type phase` entry whenever a step closed with
7
+ * `Outcome.SUCCESS`. Failures bubble up as {@link HookError} so the runner
8
+ * converts them to warnings — append errors must not break the main flow.
9
+ */
10
+ import { Outcome } from '../../delivery_state.js';
11
+ import type { HookContext } from '../context.js';
12
+ import { HookEvent } from '../events.js';
13
+ import { HookError } from '../exceptions.js';
14
+ import type { HookRegistry } from '../registry.js';
15
+ import { EXIT_OK, _ChatHistoryHookBase, _getattr, _pyJsonDumps } from './_chat_history_base.js';
16
+
17
+ /** Arbitrary value, mirroring the Python `Any` payload values. */
18
+ type Any = unknown;
19
+
20
+ /** Append a phase-boundary entry after every successful step. */
21
+ export class ChatHistoryAppendHook extends _ChatHistoryHookBase {
22
+ register(registry: HookRegistry): void {
23
+ registry.register(HookEvent.AFTER_STEP, (ctx) => this._on_after_step(ctx));
24
+ }
25
+
26
+ private _on_after_step(ctx: HookContext): void {
27
+ const result = ctx.result;
28
+ if (
29
+ result === null ||
30
+ result === undefined ||
31
+ _getattr(result, 'outcome', null) !== Outcome.SUCCESS
32
+ ) {
33
+ return;
34
+ }
35
+ const payload: Record<string, Any> = { step: ctx.step_name || '<unknown>' };
36
+ const proc = this._invoke('append', '--type', 'phase', '--json', _pyJsonDumps(payload));
37
+ if (proc.returncode !== EXIT_OK) {
38
+ throw new HookError(`chat-history append failed (exit ${proc.returncode})`);
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * `ChatHistoryHaltAppendHook` — capture halt surfaces in the log.
3
+ *
4
+ * TypeScript twin of `work_engine/hooks/builtin/chat_history_halt_append.py`
5
+ * (ADR-200 py2ts — work_engine.hooks.builtin subpackage). Fires on `on_halt`.
6
+ * Records a `--type decision` entry with the step name and any pending
7
+ * questions so a fresh chat can resume from the persisted log alone.
8
+ */
9
+ import type { HookContext } from '../context.js';
10
+ import { HookEvent } from '../events.js';
11
+ import { HookError } from '../exceptions.js';
12
+ import type { HookRegistry } from '../registry.js';
13
+ import { EXIT_OK, _ChatHistoryHookBase, _getattr, _pyJsonDumps } from './_chat_history_base.js';
14
+
15
+ /** Arbitrary value, mirroring the Python `Any` payload values. */
16
+ type Any = unknown;
17
+
18
+ /** Append a decision entry whenever a step halts. */
19
+ export class ChatHistoryHaltAppendHook extends _ChatHistoryHookBase {
20
+ register(registry: HookRegistry): void {
21
+ registry.register(HookEvent.ON_HALT, (ctx) => this._on_halt(ctx));
22
+ }
23
+
24
+ private _on_halt(ctx: HookContext): void {
25
+ let questions: string[] = [];
26
+ if (ctx.result !== null && ctx.result !== undefined) {
27
+ questions = _pyList(_pyOr(_getattr(ctx.result, 'questions', []), []));
28
+ }
29
+ if (questions.length === 0 && ctx.delivery !== null && ctx.delivery !== undefined) {
30
+ questions = _pyList(_pyOr(_getattr(ctx.delivery, 'questions', []), []));
31
+ }
32
+ const payload: Record<string, Any> = {
33
+ step: ctx.step_name || '<unknown>',
34
+ questions,
35
+ };
36
+ const proc = this._invoke('append', '--type', 'decision', '--json', _pyJsonDumps(payload));
37
+ if (proc.returncode !== EXIT_OK) {
38
+ throw new HookError(`chat-history halt-append failed (exit ${proc.returncode})`);
39
+ }
40
+ }
41
+ }
42
+
43
+ /** Python `a or b`. */
44
+ function _pyOr(a: Any, b: Any): Any {
45
+ return _pyTruthy(a) ? a : b;
46
+ }
47
+
48
+ /** Python `list(x)` for the iterable shapes `questions` can be. */
49
+ function _pyList(x: Any): string[] {
50
+ if (x === null || x === undefined) {
51
+ return [];
52
+ }
53
+ if (Array.isArray(x)) {
54
+ return [...x] as string[];
55
+ }
56
+ if (typeof x === 'string') {
57
+ return [...x];
58
+ }
59
+ if (typeof (x as { [Symbol.iterator]?: unknown })[Symbol.iterator] === 'function') {
60
+ return [...(x as Iterable<string>)];
61
+ }
62
+ return [];
63
+ }
64
+
65
+ /** Python `bool(x)` truthiness for the list / falsy shapes seen here. */
66
+ function _pyTruthy(value: Any): boolean {
67
+ if (value === null || value === undefined || value === false) {
68
+ return false;
69
+ }
70
+ if (value === true) {
71
+ return true;
72
+ }
73
+ if (typeof value === 'number') {
74
+ return value !== 0;
75
+ }
76
+ if (typeof value === 'string') {
77
+ return value.length > 0;
78
+ }
79
+ if (Array.isArray(value)) {
80
+ return value.length > 0;
81
+ }
82
+ if (value instanceof Map || value instanceof Set) {
83
+ return value.size > 0;
84
+ }
85
+ if (typeof value === 'object') {
86
+ return Object.keys(value as Record<string, unknown>).length > 0;
87
+ }
88
+ return true;
89
+ }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * `DecisionGateHook` — refuse to advance when an opt-in gate fires.
3
+ *
4
+ * TypeScript twin of `work_engine/hooks/builtin/decision_gate.py` (ADR-200
5
+ * py2ts — work_engine.hooks.builtin subpackage). Bridges
6
+ * `work_engine/scoring/decision_engine` into the dispatcher hook bus. Reads
7
+ * the gate config from {@link DecisionEngineSettings} and fires on
8
+ * `AFTER_STEP` only for the phase each gate owns.
9
+ *
10
+ * Three actions, mapped 1:1 from `evaluate_gates`:
11
+ *
12
+ * - `stop` → throw {@link HookHalt} with a numbered-option surface.
13
+ * - `warn` → throw {@link HookError} so the runner logs the reason.
14
+ * - `ask_timeout` → non-interactive context; apply `on_block_fallback` and
15
+ * re-resolve to `stop` or `warn`.
16
+ * - `ask` → interactive context; collapses to `stop` with the prompt
17
+ * surface for the CLI integration.
18
+ *
19
+ * Default-off: when `settings.decision_engine` is `null` or every gate is
20
+ * `off` the hook short-circuits without examining state.
21
+ */
22
+ import type {
23
+ GateDecision} from '../../scoring/decision_engine.js';
24
+ import {
25
+ DecisionEngineSettings,
26
+ evaluate_gates,
27
+ } from '../../scoring/decision_engine.js';
28
+ import {
29
+ derive_confidence_band,
30
+ derive_risk_class,
31
+ summarise_memory,
32
+ summarise_verify,
33
+ } from '../../scoring/decision_trace.js';
34
+ import type { HookContext } from '../context.js';
35
+ import { HookEvent } from '../events.js';
36
+ import { HookError, HookHalt } from '../exceptions.js';
37
+ import type { HookRegistry } from '../registry.js';
38
+
39
+ /** Arbitrary value, mirroring the Python `Any` fields. */
40
+ type Any = unknown;
41
+
42
+ const _BLOCK_REASON_PREFIX = 'decision_gate';
43
+
44
+ /**
45
+ * Evaluate decision-engine gates on every `AFTER_STEP`.
46
+ *
47
+ * The hook stores `settings` as a frozen reference; tests pass a fresh
48
+ * instance per scenario.
49
+ */
50
+ export class DecisionGateHook {
51
+ private readonly _settings: DecisionEngineSettings;
52
+
53
+ constructor(settings: DecisionEngineSettings) {
54
+ this._settings = settings;
55
+ }
56
+
57
+ /** Register the gate callback on `AFTER_STEP`. */
58
+ register(registry: HookRegistry): void {
59
+ registry.register(HookEvent.AFTER_STEP, (ctx) => this._evaluate(ctx));
60
+ }
61
+
62
+ // -- lifecycle callback ------------------------------------------
63
+
64
+ private _evaluate(ctx: HookContext): void {
65
+ if (!this._settings.any_gate_active) {
66
+ return;
67
+ }
68
+ const phase = ctx.step_name;
69
+ if (!phase) {
70
+ return;
71
+ }
72
+ const delivery = ctx.delivery;
73
+ const memory = summarise_memory(_getattr(delivery, 'memory', null));
74
+ const verify = summarise_verify(_getattr(delivery, 'verify', null));
75
+ const ambiguity = _pyTruthy(_getattr(delivery, 'questions', null));
76
+ const decision = evaluate_gates(this._settings, {
77
+ phase,
78
+ confidence_band: derive_confidence_band({
79
+ memory_hits: memory['hits'] as number,
80
+ verify_claims: verify['claims'] as number,
81
+ verify_first_try_passes: verify['first_try_passes'] as number,
82
+ ambiguity_flag: ambiguity,
83
+ }),
84
+ risk_class: derive_risk_class(_getattr(delivery, 'changes', null)),
85
+ memory_hits: memory['hits'] as number,
86
+ });
87
+ if (decision === null) {
88
+ return;
89
+ }
90
+ this._apply(decision);
91
+ }
92
+
93
+ // -- action dispatch ----------------------------------------------
94
+
95
+ private _apply(decision: GateDecision): void {
96
+ const action = decision.action;
97
+ if (action === 'warn') {
98
+ throw new HookError(DecisionGateHook._format_reason(decision));
99
+ }
100
+ if (action === 'ask_timeout') {
101
+ const fallback = this._settings.on_block_fallback;
102
+ if (fallback === 'warn') {
103
+ throw new HookError(
104
+ DecisionGateHook._format_reason(decision, 'ask_timeout'),
105
+ );
106
+ }
107
+ throw new HookHalt(
108
+ `${_BLOCK_REASON_PREFIX}:${decision.gate_id}:ask_timeout`,
109
+ DecisionGateHook._surface(decision, 'ask_timeout'),
110
+ );
111
+ }
112
+ throw new HookHalt(
113
+ `${_BLOCK_REASON_PREFIX}:${decision.gate_id}`,
114
+ DecisionGateHook._surface(decision),
115
+ );
116
+ }
117
+
118
+ // -- formatting helpers -------------------------------------------
119
+
120
+ private static _format_reason(decision: GateDecision, suffix = ''): string {
121
+ let tag = `${_BLOCK_REASON_PREFIX}:${decision.gate_id}`;
122
+ if (suffix) {
123
+ tag = `${tag}:${suffix}`;
124
+ }
125
+ return `${tag} — ${decision.reason}`;
126
+ }
127
+
128
+ private static _surface(decision: GateDecision, suffix = ''): string[] {
129
+ let header = `Decision-engine gate fired: ${decision.gate_id} (phase=${decision.phase})`;
130
+ if (suffix) {
131
+ header = `${header} [${suffix}]`;
132
+ }
133
+ return [
134
+ header,
135
+ `Reason: ${decision.reason}`,
136
+ '1) Address the gate condition and resume.',
137
+ '2) Lower the gate in `.agent-settings.yml` ' + '(`decision_engine` block) and resume.',
138
+ '3) Abort the run.',
139
+ ];
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Construct the hook from a {@link DecisionEngineSettings}-like object.
145
+ * Returns `null` when the config is absent or every gate is `off`; the
146
+ * bootstrap layer then skips registration entirely.
147
+ */
148
+ export function build_decision_gate_hook(settings: Any): DecisionGateHook | null {
149
+ if (settings === null || settings === undefined) {
150
+ return null;
151
+ }
152
+ if (!(settings instanceof DecisionEngineSettings)) {
153
+ return null;
154
+ }
155
+ if (!settings.any_gate_active) {
156
+ return null;
157
+ }
158
+ return new DecisionGateHook(settings);
159
+ }
160
+
161
+ /** Python `getattr(obj, name, default)`. */
162
+ function _getattr(obj: Any, name: string, dflt: Any): Any {
163
+ if (obj !== null && typeof obj === 'object' && name in (obj as object)) {
164
+ return (obj as Record<string, Any>)[name];
165
+ }
166
+ return dflt;
167
+ }
168
+
169
+ /** Python `bool(x)` truthiness for the shapes `questions` can take. */
170
+ function _pyTruthy(value: Any): boolean {
171
+ if (value === null || value === undefined || value === false) {
172
+ return false;
173
+ }
174
+ if (value === true) {
175
+ return true;
176
+ }
177
+ if (typeof value === 'number') {
178
+ return value !== 0;
179
+ }
180
+ if (typeof value === 'string') {
181
+ return value.length > 0;
182
+ }
183
+ if (Array.isArray(value)) {
184
+ return value.length > 0;
185
+ }
186
+ if (value instanceof Map || value instanceof Set) {
187
+ return value.size > 0;
188
+ }
189
+ if (typeof value === 'object') {
190
+ return Object.keys(value as Record<string, unknown>).length > 0;
191
+ }
192
+ return true;
193
+ }