@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +6 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +1 -1
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +1 -1
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +1 -1
- package/dist/mcp/registry-manifest.json +1 -1
- package/package.json +4 -1
- package/src/agent-src/README.md +64 -0
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +497 -0
- package/src/agent-src/scripts/scan-seeder-violations.php +145 -0
- package/src/agent-src/scripts/update_roadmap_progress.ts +1087 -0
- package/src/agent-src/templates/AGENTS.md +34 -0
- package/src/agent-src/templates/scripts/README.md +195 -0
- package/src/agent-src/templates/scripts/check_memory.ts +662 -0
- package/src/agent-src/templates/scripts/check_memory_proposal.ts +370 -0
- package/src/agent-src/templates/scripts/historical-bug-patterns.example.yml +84 -0
- package/src/agent-src/templates/scripts/implement_ticket/__main__.ts +27 -0
- package/src/agent-src/templates/scripts/memory_hash.ts +355 -0
- package/src/agent-src/templates/scripts/memory_lookup.ts +1089 -0
- package/src/agent-src/templates/scripts/memory_report.ts +868 -0
- package/src/agent-src/templates/scripts/memory_signal.ts +444 -0
- package/src/agent-src/templates/scripts/memory_status.ts +215 -0
- package/src/agent-src/templates/scripts/ownership-map.example.yml +87 -0
- package/src/agent-src/templates/scripts/pr-risk-config.example.yml +76 -0
- package/src/agent-src/templates/scripts/pr_review_routing.ts +544 -0
- package/src/agent-src/templates/scripts/pr_risk_review.ts +0 -0
- package/src/agent-src/templates/scripts/telemetry/aggregator.ts +0 -0
- package/src/agent-src/templates/scripts/telemetry/boundary.ts +164 -0
- package/src/agent-src/templates/scripts/telemetry/engagement.ts +479 -0
- package/src/agent-src/templates/scripts/telemetry/report_renderer.ts +394 -0
- package/src/agent-src/templates/scripts/telemetry/settings.ts +210 -0
- package/src/agent-src/templates/scripts/telemetry_record.ts +276 -0
- package/src/agent-src/templates/scripts/telemetry_report.ts +211 -0
- package/src/agent-src/templates/scripts/telemetry_status.ts +333 -0
- package/src/agent-src/templates/scripts/tier_usage_report.ts +619 -0
- package/src/agent-src/templates/scripts/work_engine/__main__.ts +14 -0
- package/src/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +1261 -0
- package/src/agent-src/templates/scripts/work_engine/_lib/user_global_paths.ts +329 -0
- package/src/agent-src/templates/scripts/work_engine/cli.ts +206 -0
- package/src/agent-src/templates/scripts/work_engine/cli_args.ts +249 -0
- package/src/agent-src/templates/scripts/work_engine/delivery_state.ts +225 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/analyze.ts +125 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/implement.ts +189 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/index.ts +94 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/memory.ts +193 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/plan.ts +267 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/refine.ts +518 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/report.ts +379 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +268 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +258 -0
- package/src/agent-src/templates/scripts/work_engine/directives/index.ts +32 -0
- package/src/agent-src/templates/scripts/work_engine/directives/mixed/contract.ts +243 -0
- package/src/agent-src/templates/scripts/work_engine/directives/mixed/index.ts +108 -0
- package/src/agent-src/templates/scripts/work_engine/directives/mixed/stitch.ts +259 -0
- package/src/agent-src/templates/scripts/work_engine/directives/mixed/ui.ts +216 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/_passthrough.ts +40 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +241 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +216 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +506 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +325 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/index.ts +102 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +462 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/review.ts +474 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +352 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/_skipped.ts +33 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/apply.ts +213 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/index.ts +111 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/refine.ts +126 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/report.ts +112 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/test.ts +164 -0
- package/src/agent-src/templates/scripts/work_engine/dispatcher.ts +515 -0
- package/src/agent-src/templates/scripts/work_engine/emitters.ts +119 -0
- package/src/agent-src/templates/scripts/work_engine/errors.ts +24 -0
- package/src/agent-src/templates/scripts/work_engine/hook_bootstrap.ts +104 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/_chat_history_base.ts +189 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_append.ts +41 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_halt_append.ts +89 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/decision_gate.ts +193 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/decision_trace.ts +304 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/directive_set_guard.ts +110 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/halt_surface_audit.ts +118 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/index.ts +17 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/memory_visibility.ts +161 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/state_shape_validation.ts +45 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/trace.ts +134 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/context.ts +94 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/events.ts +58 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/exceptions.ts +85 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/index.ts +27 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/registry.ts +66 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/runner.ts +90 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/settings.ts +260 -0
- package/src/agent-src/templates/scripts/work_engine/input_builders.ts +260 -0
- package/src/agent-src/templates/scripts/work_engine/intent/classify.ts +466 -0
- package/src/agent-src/templates/scripts/work_engine/migration/v0_to_v1.ts +531 -0
- package/src/agent-src/templates/scripts/work_engine/orchestration.ts +366 -0
- package/src/agent-src/templates/scripts/work_engine/persona_policy.ts +97 -0
- package/src/agent-src/templates/scripts/work_engine/resolvers/diff.ts +135 -0
- package/src/agent-src/templates/scripts/work_engine/resolvers/file.ts +175 -0
- package/src/agent-src/templates/scripts/work_engine/resolvers/prompt.ts +115 -0
- package/src/agent-src/templates/scripts/work_engine/scoring/confidence.ts +415 -0
- package/src/agent-src/templates/scripts/work_engine/scoring/decision_engine.ts +466 -0
- package/src/agent-src/templates/scripts/work_engine/scoring/decision_trace.ts +298 -0
- package/src/agent-src/templates/scripts/work_engine/scoring/memory_visibility.ts +444 -0
- package/src/agent-src/templates/scripts/work_engine/stack/detect.ts +252 -0
- package/src/agent-src/templates/scripts/work_engine/stack/runner.ts +745 -0
- package/src/agent-src/templates/scripts/work_engine/state.ts +1151 -0
- package/src/agent-src/templates/scripts/work_engine/state_io.ts +413 -0
- package/src/install/atomic.ts +111 -0
- package/src/install/conflict.ts +226 -0
- package/src/install/detect.ts +280 -0
- package/src/install/paths.ts +90 -0
- package/src/install/plan.ts +205 -0
- package/src/install/preflight.ts +210 -0
- package/src/install/selectedTools.ts +52 -0
- package/src/install/toolDetection.ts +120 -0
- package/src/install/txlog.ts +161 -0
- package/src/install/types.ts +175 -0
- package/src/install/wizard-plan.ts +216 -0
- package/src/scripts/prepack-check.mjs +108 -5
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `MemoryVisibilityHook` — emit the visibility line on save.
|
|
3
|
+
*
|
|
4
|
+
* TypeScript twin of `work_engine/hooks/builtin/memory_visibility.py`
|
|
5
|
+
* (ADR-200 py2ts — work_engine.hooks.builtin subpackage). Implements the
|
|
6
|
+
* producer side of `docs/contracts/memory-visibility-v1.md`: derive
|
|
7
|
+
* `asks/hits/ids` from `state.memory` and thread the rendered line into
|
|
8
|
+
* `state.report`.
|
|
9
|
+
*
|
|
10
|
+
* Fires on `before_save`. Default-off; opt-in via `.agent-settings.yml`. The
|
|
11
|
+
* hook is purely observational: failures surface as {@link HookError}
|
|
12
|
+
* (non-fatal per the three-tier contract).
|
|
13
|
+
*/
|
|
14
|
+
import { summarise_memory, summarise_verify } from '../../scoring/decision_trace.js';
|
|
15
|
+
import {
|
|
16
|
+
DEFAULT_ASKED_TYPES,
|
|
17
|
+
compute_affected,
|
|
18
|
+
format_changed_decisions_block,
|
|
19
|
+
format_line,
|
|
20
|
+
should_emit,
|
|
21
|
+
summarise_visibility,
|
|
22
|
+
} from '../../scoring/memory_visibility.js';
|
|
23
|
+
import type { HookContext } from '../context.js';
|
|
24
|
+
import { HookEvent } from '../events.js';
|
|
25
|
+
import { HookError } from '../exceptions.js';
|
|
26
|
+
import type { HookRegistry } from '../registry.js';
|
|
27
|
+
|
|
28
|
+
/** Arbitrary value, mirroring the Python `Any` fields. */
|
|
29
|
+
type Any = unknown;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Thread the `🧠 Memory: <hits>/<asks> · ids=[…]` line into the report.
|
|
33
|
+
*
|
|
34
|
+
* `memory_cadence` is the `memory.cadence` cadence key. `visibility_off`
|
|
35
|
+
* mirrors `memory.visibility: off`. `asked_types` overrides the list of
|
|
36
|
+
* memory types treated as `asks` in the visibility line.
|
|
37
|
+
*/
|
|
38
|
+
export class MemoryVisibilityHook {
|
|
39
|
+
private readonly _memory_cadence: string;
|
|
40
|
+
private readonly _visibility_off: boolean;
|
|
41
|
+
private readonly _asked_types: readonly string[];
|
|
42
|
+
|
|
43
|
+
constructor(
|
|
44
|
+
options: {
|
|
45
|
+
memory_cadence?: string;
|
|
46
|
+
visibility_off?: boolean;
|
|
47
|
+
asked_types?: Iterable<string> | null;
|
|
48
|
+
} = {},
|
|
49
|
+
) {
|
|
50
|
+
this._memory_cadence = options.memory_cadence ?? 'always';
|
|
51
|
+
this._visibility_off = options.visibility_off ?? false;
|
|
52
|
+
this._asked_types =
|
|
53
|
+
options.asked_types !== undefined && options.asked_types !== null
|
|
54
|
+
? [...options.asked_types]
|
|
55
|
+
: DEFAULT_ASKED_TYPES;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Register the visibility-line emitter on `before_save`. */
|
|
59
|
+
register(registry: HookRegistry): void {
|
|
60
|
+
registry.register(HookEvent.BEFORE_SAVE, (ctx) => this._on_before_save(ctx));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private _on_before_save(ctx: HookContext): void {
|
|
64
|
+
const work = ctx.work;
|
|
65
|
+
if (work === null || work === undefined) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const memory = _getattr(work, 'memory', null);
|
|
69
|
+
const summary = summarise_visibility(memory, { asked_types: this._asked_types });
|
|
70
|
+
if (
|
|
71
|
+
!should_emit(summary, {
|
|
72
|
+
memory_cadence: this._memory_cadence,
|
|
73
|
+
visibility_off: this._visibility_off,
|
|
74
|
+
})
|
|
75
|
+
) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const affected = this._derive_affected(work, memory);
|
|
79
|
+
const line = format_line(summary, { affected });
|
|
80
|
+
if (!line) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const block = format_changed_decisions_block(
|
|
84
|
+
(summary['ids'] as string[]) || [],
|
|
85
|
+
affected,
|
|
86
|
+
);
|
|
87
|
+
const existing = (_getattr(work, 'report', '') as string) || '';
|
|
88
|
+
const rendered = block === null ? line : `${line}\n\n${block}`;
|
|
89
|
+
if (existing.includes(line) && (block === null || existing.includes(block))) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const sep = existing ? '\n\n' : '';
|
|
93
|
+
try {
|
|
94
|
+
(work as Record<string, Any>)['report'] = `${existing}${sep}${rendered}`;
|
|
95
|
+
} catch (_exc) {
|
|
96
|
+
throw new HookError('memory-visibility: state.report not writable');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Compute the closed-list `affected` keys for this work step. Returns
|
|
102
|
+
* `null` when memory was not consulted (hits == 0).
|
|
103
|
+
*/
|
|
104
|
+
private _derive_affected(work: Any, memory: Any): string[] | null {
|
|
105
|
+
const memory_summary = summarise_memory(memory);
|
|
106
|
+
const verify_summary = summarise_verify(_getattr(work, 'verify', null));
|
|
107
|
+
const ambiguity = _pyTruthy(_getattr(work, 'questions', null));
|
|
108
|
+
return compute_affected({
|
|
109
|
+
memory_hits: memory_summary['hits'] as number,
|
|
110
|
+
verify_claims: verify_summary['claims'] as number,
|
|
111
|
+
verify_first_try_passes: verify_summary['first_try_passes'] as number,
|
|
112
|
+
ambiguity_flag: ambiguity,
|
|
113
|
+
changes: _getattr(work, 'changes', null),
|
|
114
|
+
applied_rules: _getattr(work, 'applied_rules', null) as string[] | null,
|
|
115
|
+
test_plan: _getattr(work, 'test_plan', null) as string[] | null,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Convenience helper: render the line directly from a memory list. Used by
|
|
122
|
+
* external callers that have a `memory` list but no {@link HookContext}.
|
|
123
|
+
* Returns `null` when `asks == 0`.
|
|
124
|
+
*/
|
|
125
|
+
export function derive_visibility(memory: Any): string | null {
|
|
126
|
+
return format_line(summarise_visibility(memory));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Python `getattr(obj, name, default)`. */
|
|
130
|
+
function _getattr(obj: Any, name: string, dflt: Any): Any {
|
|
131
|
+
if (obj !== null && typeof obj === 'object' && name in (obj as object)) {
|
|
132
|
+
return (obj as Record<string, Any>)[name];
|
|
133
|
+
}
|
|
134
|
+
return dflt;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Python `bool(x)` truthiness for the `questions` shapes seen here. */
|
|
138
|
+
function _pyTruthy(value: Any): boolean {
|
|
139
|
+
if (value === null || value === undefined || value === false) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
if (value === true) {
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
if (typeof value === 'number') {
|
|
146
|
+
return value !== 0;
|
|
147
|
+
}
|
|
148
|
+
if (typeof value === 'string') {
|
|
149
|
+
return value.length > 0;
|
|
150
|
+
}
|
|
151
|
+
if (Array.isArray(value)) {
|
|
152
|
+
return value.length > 0;
|
|
153
|
+
}
|
|
154
|
+
if (value instanceof Map || value instanceof Set) {
|
|
155
|
+
return value.size > 0;
|
|
156
|
+
}
|
|
157
|
+
if (typeof value === 'object') {
|
|
158
|
+
return Object.keys(value as Record<string, unknown>).length > 0;
|
|
159
|
+
}
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `StateShapeValidationHook` — round-trip the v1 envelope on load and save.
|
|
3
|
+
*
|
|
4
|
+
* TypeScript twin of `work_engine/hooks/builtin/state_shape_validation.py`
|
|
5
|
+
* (ADR-200 py2ts — work_engine.hooks.builtin subpackage). Fires on
|
|
6
|
+
* `AFTER_LOAD` and `BEFORE_SAVE`. For each event, serialises the live
|
|
7
|
+
* `WorkState` through `state.to_dict` and re-validates via `state.from_dict`.
|
|
8
|
+
* A `SchemaError` from either side is reported as a {@link HookError} so the
|
|
9
|
+
* runner warns and continues — observability, not a gate.
|
|
10
|
+
*/
|
|
11
|
+
import { SchemaError, type WorkState, from_dict, to_dict } from '../../state.js';
|
|
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
|
+
/** Round-trips the loaded `WorkState` against the v1 schema. */
|
|
18
|
+
export class StateShapeValidationHook {
|
|
19
|
+
/** Register on AFTER_LOAD and BEFORE_SAVE. */
|
|
20
|
+
register(registry: HookRegistry): void {
|
|
21
|
+
registry.register(HookEvent.AFTER_LOAD, (ctx) => this._validate(ctx));
|
|
22
|
+
registry.register(HookEvent.BEFORE_SAVE, (ctx) => this._validate(ctx));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private _validate(ctx: HookContext): void {
|
|
26
|
+
const work = ctx.work;
|
|
27
|
+
if (work === null || work === undefined) {
|
|
28
|
+
// Should not happen on AFTER_LOAD/BEFORE_SAVE; treat as a
|
|
29
|
+
// hook-side bug rather than swallow silently.
|
|
30
|
+
throw new HookError(
|
|
31
|
+
'state-shape validation: HookContext.work is None at ' +
|
|
32
|
+
`event for state_file=${ctx.state_file === null ? 'None' : ctx.state_file}`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
from_dict(to_dict(work as WorkState));
|
|
38
|
+
} catch (exc) {
|
|
39
|
+
if (exc instanceof SchemaError) {
|
|
40
|
+
throw new HookError(`state-shape validation failed: ${exc.message}`);
|
|
41
|
+
}
|
|
42
|
+
throw exc;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `TraceHook` — emit one stderr line per hook event.
|
|
3
|
+
*
|
|
4
|
+
* TypeScript twin of `work_engine/hooks/builtin/trace.py` (ADR-200 py2ts —
|
|
5
|
+
* work_engine.hooks.builtin subpackage). Registers on every `HookEvent`;
|
|
6
|
+
* output goes to a configurable stream (default `process.stderr`) so tests can
|
|
7
|
+
* capture it.
|
|
8
|
+
*
|
|
9
|
+
* Pure observability — never mutates context, never halts. A misbehaving sink
|
|
10
|
+
* raises {@link HookError}, which the runner swallows with a warning.
|
|
11
|
+
*/
|
|
12
|
+
import type { HookEvent } from '../events.js';
|
|
13
|
+
import { HOOK_EVENTS } from '../events.js';
|
|
14
|
+
import type { HookContext } from '../context.js';
|
|
15
|
+
import { HookError } from '../exceptions.js';
|
|
16
|
+
import type { HookCallback, HookRegistry } from '../registry.js';
|
|
17
|
+
|
|
18
|
+
/** Arbitrary value, mirroring the Python `Any` fields. */
|
|
19
|
+
type Any = unknown;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Minimal write+flush sink, mirroring Python's `IO[str]`. `process.stderr`
|
|
23
|
+
* satisfies it; tests pass a string-collector.
|
|
24
|
+
*/
|
|
25
|
+
export interface TextStream {
|
|
26
|
+
write(s: string): unknown;
|
|
27
|
+
flush?(): void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Stderr-trace hook for every lifecycle event. */
|
|
31
|
+
export class TraceHook {
|
|
32
|
+
private readonly _stream: TextStream;
|
|
33
|
+
private readonly _prefix: string;
|
|
34
|
+
|
|
35
|
+
constructor(stream: TextStream | null = null, prefix = '[hook]') {
|
|
36
|
+
this._stream = stream !== null ? stream : process.stderr;
|
|
37
|
+
this._prefix = prefix;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Register the trace callback for every `HookEvent`. */
|
|
41
|
+
register(registry: HookRegistry): void {
|
|
42
|
+
for (const event of HOOK_EVENTS) {
|
|
43
|
+
registry.register(event, this._make_callback(event));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private _make_callback(event: HookEvent): HookCallback {
|
|
48
|
+
const _cb = (ctx: HookContext): void => {
|
|
49
|
+
try {
|
|
50
|
+
const line = this._format(event, ctx);
|
|
51
|
+
this._stream.write(line + '\n');
|
|
52
|
+
if (typeof this._stream.flush === 'function') {
|
|
53
|
+
this._stream.flush();
|
|
54
|
+
}
|
|
55
|
+
} catch (exc) {
|
|
56
|
+
if (exc instanceof HookError) {
|
|
57
|
+
throw exc;
|
|
58
|
+
}
|
|
59
|
+
// Mirror Python `except (OSError, ValueError)`: wrap stream
|
|
60
|
+
// failures as a non-fatal HookError; re-raise anything else.
|
|
61
|
+
throw new HookError(`trace stream unavailable: ${_errStr(exc)}`);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
return _cb;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Build a one-line trace record.
|
|
69
|
+
*
|
|
70
|
+
* Format: `[hook] event=<name> step=<step> set=<set> outcome=<o>`.
|
|
71
|
+
* Missing fields are skipped so the line stays short on events that only
|
|
72
|
+
* carry a subset of the context.
|
|
73
|
+
*/
|
|
74
|
+
private _format(event: HookEvent, ctx: HookContext): string {
|
|
75
|
+
const parts: string[] = [this._prefix, `event=${event}`];
|
|
76
|
+
if (ctx.step_name) {
|
|
77
|
+
parts.push(`step=${ctx.step_name}`);
|
|
78
|
+
}
|
|
79
|
+
if (ctx.set_name) {
|
|
80
|
+
parts.push(`set=${ctx.set_name}`);
|
|
81
|
+
}
|
|
82
|
+
if (ctx.result !== null && ctx.result !== undefined) {
|
|
83
|
+
const outcome = _getattr(ctx.result, 'outcome', null);
|
|
84
|
+
if (outcome !== null && outcome !== undefined) {
|
|
85
|
+
parts.push(`outcome=${_valueOf(outcome)}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (ctx.final !== null && ctx.final !== undefined) {
|
|
89
|
+
parts.push(`final=${_valueOf(ctx.final)}`);
|
|
90
|
+
}
|
|
91
|
+
if (ctx.halting) {
|
|
92
|
+
parts.push(`halting=${ctx.halting}`);
|
|
93
|
+
}
|
|
94
|
+
if (ctx.exception !== null && ctx.exception !== undefined) {
|
|
95
|
+
parts.push(`exception=${_typeName(ctx.exception)}`);
|
|
96
|
+
}
|
|
97
|
+
return parts.join(' ');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Python `getattr(obj, name, default)`. */
|
|
102
|
+
function _getattr(obj: Any, name: string, dflt: Any): Any {
|
|
103
|
+
if (obj !== null && typeof obj === 'object' && name in (obj as object)) {
|
|
104
|
+
return (obj as Record<string, Any>)[name];
|
|
105
|
+
}
|
|
106
|
+
return dflt;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Python `getattr(x, "value", x)` — enum value if present, else the value. */
|
|
110
|
+
function _valueOf(x: Any): string {
|
|
111
|
+
if (x !== null && typeof x === 'object' && 'value' in (x as object)) {
|
|
112
|
+
return String((x as Record<string, Any>)['value']);
|
|
113
|
+
}
|
|
114
|
+
return String(x);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Python `type(exc).__name__`. */
|
|
118
|
+
function _typeName(exc: Any): string {
|
|
119
|
+
if (exc instanceof Error) {
|
|
120
|
+
return exc.constructor.name;
|
|
121
|
+
}
|
|
122
|
+
if (exc !== null && exc !== undefined && typeof exc === 'object') {
|
|
123
|
+
return (exc as { constructor?: { name?: string } }).constructor?.name ?? 'object';
|
|
124
|
+
}
|
|
125
|
+
return typeof exc;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** `str(exc)` for the wrapped-sink-failure message. */
|
|
129
|
+
function _errStr(exc: unknown): string {
|
|
130
|
+
if (exc instanceof Error) {
|
|
131
|
+
return exc.message;
|
|
132
|
+
}
|
|
133
|
+
return String(exc);
|
|
134
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `HookContext` — payload carried into every hook callback.
|
|
3
|
+
*
|
|
4
|
+
* TypeScript twin of `work_engine/hooks/context.py` (ADR-200 py2ts —
|
|
5
|
+
* work_engine.hooks subpackage). One class for both layers. Most fields are
|
|
6
|
+
* `null` for any given event; the per-event subset is documented below and
|
|
7
|
+
* locked by the roadmap's hook event surface table. Hooks must tolerate
|
|
8
|
+
* missing fields gracefully.
|
|
9
|
+
*
|
|
10
|
+
* Per-event subset (mirrors the roadmap):
|
|
11
|
+
*
|
|
12
|
+
* Dispatcher layer (`delivery` is set; `work` is `null`):
|
|
13
|
+
* - `before_step` → `step_name`, `delivery`
|
|
14
|
+
* - `after_step` → `step_name`, `delivery`, `result`
|
|
15
|
+
* - `on_halt` → `step_name`, `delivery`, `result`
|
|
16
|
+
* - `on_error` → `step_name`, `delivery`, `exception`
|
|
17
|
+
*
|
|
18
|
+
* CLI layer (`work` is set; `delivery` may be set after load):
|
|
19
|
+
* - `before_load` → `state_file`, `args`
|
|
20
|
+
* - `after_load` → `state_file`, `work`, `fmt`
|
|
21
|
+
* - `before_dispatch` → `work`, `delivery`, `set_name`
|
|
22
|
+
* - `after_dispatch` → `work`, `delivery`, `final`, `halting`
|
|
23
|
+
* - `before_save` → `work`, `delivery`, `fmt`
|
|
24
|
+
* - `after_save` → `work`, `state_file`, `fmt`
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** Arbitrary value, mirroring the Python `Any` fields. */
|
|
28
|
+
export type Any = unknown;
|
|
29
|
+
|
|
30
|
+
/** Per-event payload passed to every hook callback (matches the dataclass). */
|
|
31
|
+
export interface HookContextInit {
|
|
32
|
+
// Dispatcher-layer refs.
|
|
33
|
+
step_name?: string | null;
|
|
34
|
+
delivery?: Any; // DeliveryState
|
|
35
|
+
result?: Any; // StepResult
|
|
36
|
+
exception?: Any; // BaseException
|
|
37
|
+
|
|
38
|
+
// CLI-layer refs.
|
|
39
|
+
work?: Any; // WorkState
|
|
40
|
+
state_file?: string | null;
|
|
41
|
+
fmt?: string | null;
|
|
42
|
+
set_name?: string | null;
|
|
43
|
+
final?: Any; // Outcome
|
|
44
|
+
halting?: string | null;
|
|
45
|
+
args?: Any; // argparse.Namespace
|
|
46
|
+
|
|
47
|
+
// Escape hatch for hook-specific state.
|
|
48
|
+
extra?: Record<string, Any>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Per-event payload passed to every hook callback.
|
|
53
|
+
*
|
|
54
|
+
* Fields are intentionally optional — the runner does not validate which
|
|
55
|
+
* ones are populated for a given event. The contract is enforced by the
|
|
56
|
+
* call sites, not by the class.
|
|
57
|
+
*
|
|
58
|
+
* `extra` exists as an escape hatch for hook-specific state that does not
|
|
59
|
+
* warrant a dedicated field.
|
|
60
|
+
*/
|
|
61
|
+
export class HookContext {
|
|
62
|
+
// Dispatcher-layer refs.
|
|
63
|
+
step_name: string | null;
|
|
64
|
+
delivery: Any;
|
|
65
|
+
result: Any;
|
|
66
|
+
exception: Any;
|
|
67
|
+
|
|
68
|
+
// CLI-layer refs.
|
|
69
|
+
work: Any;
|
|
70
|
+
state_file: string | null;
|
|
71
|
+
fmt: string | null;
|
|
72
|
+
set_name: string | null;
|
|
73
|
+
final: Any;
|
|
74
|
+
halting: string | null;
|
|
75
|
+
args: Any;
|
|
76
|
+
|
|
77
|
+
// Escape hatch for hook-specific state.
|
|
78
|
+
extra: Record<string, Any>;
|
|
79
|
+
|
|
80
|
+
constructor(init: HookContextInit = {}) {
|
|
81
|
+
this.step_name = init.step_name ?? null;
|
|
82
|
+
this.delivery = init.delivery ?? null;
|
|
83
|
+
this.result = init.result ?? null;
|
|
84
|
+
this.exception = init.exception ?? null;
|
|
85
|
+
this.work = init.work ?? null;
|
|
86
|
+
this.state_file = init.state_file ?? null;
|
|
87
|
+
this.fmt = init.fmt ?? null;
|
|
88
|
+
this.set_name = init.set_name ?? null;
|
|
89
|
+
this.final = init.final ?? null;
|
|
90
|
+
this.halting = init.halting ?? null;
|
|
91
|
+
this.args = init.args ?? null;
|
|
92
|
+
this.extra = init.extra ?? {};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook event surface for `work_engine`.
|
|
3
|
+
*
|
|
4
|
+
* TypeScript twin of `work_engine/hooks/events.py` (ADR-200 py2ts —
|
|
5
|
+
* work_engine.hooks subpackage). Mirrors the Python `str`-Enum 1:1: each
|
|
6
|
+
* value is the event name verbatim, so round-trips stay trivial for
|
|
7
|
+
* telemetry and JSON tracing.
|
|
8
|
+
*
|
|
9
|
+
* Ten events split across two layers per
|
|
10
|
+
* `agents/roadmaps/road-to-work-engine-hooks.md` (locked).
|
|
11
|
+
*
|
|
12
|
+
* Dispatcher-layer events fire from inside `dispatcher.dispatch()` and
|
|
13
|
+
* operate on `DeliveryState` (legacy, internal). CLI-layer events fire
|
|
14
|
+
* from `cli.main()` and operate on `WorkState` (v1 envelope) plus
|
|
15
|
+
* auxiliary refs (`state_file`, `fmt`, `args`).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Lifecycle events emitted by the work engine.
|
|
20
|
+
*
|
|
21
|
+
* Modelled as a const object + value-union so it behaves like the Python
|
|
22
|
+
* `str`-Enum: members compare equal to their string value, and the value
|
|
23
|
+
* IS the event name. `HookEvent.BEFORE_STEP === 'before_step'`.
|
|
24
|
+
*/
|
|
25
|
+
export const HookEvent = {
|
|
26
|
+
// Dispatcher layer (DeliveryState).
|
|
27
|
+
BEFORE_STEP: 'before_step',
|
|
28
|
+
AFTER_STEP: 'after_step',
|
|
29
|
+
ON_HALT: 'on_halt',
|
|
30
|
+
ON_ERROR: 'on_error',
|
|
31
|
+
|
|
32
|
+
// CLI layer (WorkState).
|
|
33
|
+
BEFORE_LOAD: 'before_load',
|
|
34
|
+
AFTER_LOAD: 'after_load',
|
|
35
|
+
BEFORE_DISPATCH: 'before_dispatch',
|
|
36
|
+
AFTER_DISPATCH: 'after_dispatch',
|
|
37
|
+
BEFORE_SAVE: 'before_save',
|
|
38
|
+
AFTER_SAVE: 'after_save',
|
|
39
|
+
} as const;
|
|
40
|
+
|
|
41
|
+
export type HookEvent = (typeof HookEvent)[keyof typeof HookEvent];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Iteration order over every event, mirroring Python `for event in HookEvent`
|
|
45
|
+
* (declaration order). Used by {@link TraceHook} to register on all events.
|
|
46
|
+
*/
|
|
47
|
+
export const HOOK_EVENTS: readonly HookEvent[] = [
|
|
48
|
+
HookEvent.BEFORE_STEP,
|
|
49
|
+
HookEvent.AFTER_STEP,
|
|
50
|
+
HookEvent.ON_HALT,
|
|
51
|
+
HookEvent.ON_ERROR,
|
|
52
|
+
HookEvent.BEFORE_LOAD,
|
|
53
|
+
HookEvent.AFTER_LOAD,
|
|
54
|
+
HookEvent.BEFORE_DISPATCH,
|
|
55
|
+
HookEvent.AFTER_DISPATCH,
|
|
56
|
+
HookEvent.BEFORE_SAVE,
|
|
57
|
+
HookEvent.AFTER_SAVE,
|
|
58
|
+
];
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook control-flow signals.
|
|
3
|
+
*
|
|
4
|
+
* TypeScript twin of `work_engine/hooks/exceptions.py` (ADR-200 py2ts —
|
|
5
|
+
* work_engine.hooks subpackage). Class names stay 1:1 with the Python module.
|
|
6
|
+
*
|
|
7
|
+
* Three-tier error contract (locked by roadmap P1):
|
|
8
|
+
*
|
|
9
|
+
* - `HookError` — non-fatal. Hook implementation failed; the runner
|
|
10
|
+
* catches it, warns, and continues with the next callback for the same
|
|
11
|
+
* event. Work proceeds.
|
|
12
|
+
* - `HookHalt` — fatal-controlled. Hook demands a clean stop (canonical
|
|
13
|
+
* example: chat-history `turn-check` foreign session). The runner
|
|
14
|
+
* catches it and **returns** it to the caller, who decides how to
|
|
15
|
+
* surface it (engine halt, CLI exit code 2 + readable surface). Not
|
|
16
|
+
* re-raised through the dispatch loop.
|
|
17
|
+
* - any other `Error` — fatal-uncontrolled. Treated as a bug in the
|
|
18
|
+
* hook. The runner lets it propagate verbatim; dispatch unwinds.
|
|
19
|
+
*
|
|
20
|
+
* Both signals share a private `_HookSignal` base so the runner can
|
|
21
|
+
* distinguish hook-originated control flow from genuine bugs.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Internal marker for hook-originated control flow.
|
|
26
|
+
*
|
|
27
|
+
* Not part of the public API. The runner uses `instanceof` checks
|
|
28
|
+
* against the concrete subclasses below; the base exists only so a
|
|
29
|
+
* single `instanceof _HookSignal` would cover both signals if a future
|
|
30
|
+
* refactor needs it.
|
|
31
|
+
*/
|
|
32
|
+
export class _HookSignal extends Error {
|
|
33
|
+
constructor(message?: string) {
|
|
34
|
+
super(message);
|
|
35
|
+
Object.setPrototypeOf(this, _HookSignal.prototype);
|
|
36
|
+
this.name = '_HookSignal';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Non-fatal hook failure.
|
|
42
|
+
*
|
|
43
|
+
* Raised when a hook callback fails in a way the *engine* should ignore.
|
|
44
|
+
* The runner catches it, emits a warning with the message, and moves
|
|
45
|
+
* on to the next callback registered for the event.
|
|
46
|
+
*
|
|
47
|
+
* Use this for transient or non-critical hook failures (telemetry
|
|
48
|
+
* sinks, optional reporters). Do **not** use it to signal "stop the
|
|
49
|
+
* engine" — that is what {@link HookHalt} is for.
|
|
50
|
+
*/
|
|
51
|
+
export class HookError extends _HookSignal {
|
|
52
|
+
constructor(message?: string) {
|
|
53
|
+
super(message);
|
|
54
|
+
Object.setPrototypeOf(this, HookError.prototype);
|
|
55
|
+
this.name = 'HookError';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Fatal-controlled stop requested by a hook.
|
|
61
|
+
*
|
|
62
|
+
* Hooks raise this when execution must not continue. The runner catches
|
|
63
|
+
* it and returns it to the caller; the caller turns it into the
|
|
64
|
+
* appropriate halt surface (dispatcher `Outcome.BLOCKED`, CLI exit 2).
|
|
65
|
+
*
|
|
66
|
+
* `surface` is a list of pre-formatted numbered options per the
|
|
67
|
+
* `user-interaction` rule (one entry per line). Callers must not
|
|
68
|
+
* reformat — surface is rendered verbatim.
|
|
69
|
+
*
|
|
70
|
+
* `reason` is a short machine-readable code (e.g. `"foreign"`,
|
|
71
|
+
* `"missing"`, `"validation_failed"`) for logging and tests; it is not
|
|
72
|
+
* shown to the user.
|
|
73
|
+
*/
|
|
74
|
+
export class HookHalt extends _HookSignal {
|
|
75
|
+
readonly reason: string;
|
|
76
|
+
readonly surface: string[];
|
|
77
|
+
|
|
78
|
+
constructor(reason: string, surface: string[] | null = null) {
|
|
79
|
+
super(reason);
|
|
80
|
+
Object.setPrototypeOf(this, HookHalt.prototype);
|
|
81
|
+
this.name = 'HookHalt';
|
|
82
|
+
this.reason = reason;
|
|
83
|
+
this.surface = surface ? [...surface] : [];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `work_engine.hooks` — cross-cutting lifecycle hooks for the engine.
|
|
3
|
+
*
|
|
4
|
+
* TypeScript twin of `work_engine/hooks/__init__.py` (ADR-200 py2ts —
|
|
5
|
+
* work_engine.hooks subpackage). Public surface:
|
|
6
|
+
*
|
|
7
|
+
* - `HookEvent` — ten lifecycle events, two layers.
|
|
8
|
+
* - `HookContext` — per-event payload.
|
|
9
|
+
* - `HookError` / `HookHalt` — three-tier error contract.
|
|
10
|
+
* - `HookRegistry` — insertion-ordered event → callbacks map.
|
|
11
|
+
* - `HookRunner` — single emit point, owns the error contract.
|
|
12
|
+
*/
|
|
13
|
+
export {
|
|
14
|
+
ChatHistoryAppendHook,
|
|
15
|
+
ChatHistoryHaltAppendHook,
|
|
16
|
+
DecisionTraceHook,
|
|
17
|
+
DirectiveSetGuardHook,
|
|
18
|
+
HaltSurfaceAuditHook,
|
|
19
|
+
MemoryVisibilityHook,
|
|
20
|
+
StateShapeValidationHook,
|
|
21
|
+
TraceHook,
|
|
22
|
+
} from './builtin/index.js';
|
|
23
|
+
export { HookContext } from './context.js';
|
|
24
|
+
export { HookEvent } from './events.js';
|
|
25
|
+
export { HookError, HookHalt } from './exceptions.js';
|
|
26
|
+
export { type HookCallback, HookRegistry } from './registry.js';
|
|
27
|
+
export { HookRunner } from './runner.js';
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `HookRegistry` — insertion-ordered map from event to callbacks.
|
|
3
|
+
*
|
|
4
|
+
* TypeScript twin of `work_engine/hooks/registry.py` (ADR-200 py2ts —
|
|
5
|
+
* work_engine.hooks subpackage). Phase 1 ships insertion-ordered
|
|
6
|
+
* registration only.
|
|
7
|
+
*
|
|
8
|
+
* The registry is a plain container. It does not invoke callbacks, does
|
|
9
|
+
* not catch exceptions, and does not know about the error contract; that
|
|
10
|
+
* responsibility lives in {@link HookRunner}.
|
|
11
|
+
*/
|
|
12
|
+
import type { HookContext } from './context.js';
|
|
13
|
+
import type { HookEvent } from './events.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A hook callback. Returns `void` on success, throws `HookError` or
|
|
17
|
+
* `HookHalt` to signal control flow per `exceptions.ts`.
|
|
18
|
+
*/
|
|
19
|
+
export type HookCallback = (ctx: HookContext) => void;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Insertion-ordered registry of hook callbacks per event.
|
|
23
|
+
*
|
|
24
|
+
* Single instance per CLI invocation. Built once and shared with
|
|
25
|
+
* `dispatch()` so dispatcher events and CLI events are routed through the
|
|
26
|
+
* same callback set.
|
|
27
|
+
*/
|
|
28
|
+
export class HookRegistry {
|
|
29
|
+
// Map preserves insertion order, mirroring Python's dict ordering.
|
|
30
|
+
private _hooks: Map<HookEvent, HookCallback[]> = new Map();
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Register `callback` for `event`.
|
|
34
|
+
*
|
|
35
|
+
* Multiple callbacks for the same event are allowed; they fire in
|
|
36
|
+
* registration order.
|
|
37
|
+
*/
|
|
38
|
+
register(event: HookEvent, callback: HookCallback): void {
|
|
39
|
+
let list = this._hooks.get(event);
|
|
40
|
+
if (list === undefined) {
|
|
41
|
+
list = [];
|
|
42
|
+
this._hooks.set(event, list);
|
|
43
|
+
}
|
|
44
|
+
list.push(callback);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Return callbacks registered for `event` in insertion order.
|
|
49
|
+
*
|
|
50
|
+
* Returns an empty array when no callbacks are registered — the runner
|
|
51
|
+
* uses this to short-circuit a no-op fast path.
|
|
52
|
+
*/
|
|
53
|
+
for_event(event: HookEvent): HookCallback[] {
|
|
54
|
+
const list = this._hooks.get(event);
|
|
55
|
+
return list === undefined ? [] : [...list];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Iterate over events that have at least one callback.
|
|
60
|
+
*
|
|
61
|
+
* Diagnostics-only; not used on the hot path.
|
|
62
|
+
*/
|
|
63
|
+
events(): IterableIterator<HookEvent> {
|
|
64
|
+
return this._hooks.keys();
|
|
65
|
+
}
|
|
66
|
+
}
|