@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,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `HookRunner` — single emit point for hook callbacks.
|
|
3
|
+
*
|
|
4
|
+
* TypeScript twin of `work_engine/hooks/runner.py` (ADR-200 py2ts —
|
|
5
|
+
* work_engine.hooks subpackage). Implements the three-tier error contract
|
|
6
|
+
* documented in `exceptions.ts`:
|
|
7
|
+
*
|
|
8
|
+
* - `HookError` from a callback → caught, a warning is emitted, the runner
|
|
9
|
+
* continues with the next callback for the same event. Returns `null`
|
|
10
|
+
* once the event is fully drained.
|
|
11
|
+
* - `HookHalt` from a callback → caught, **returned** to the caller with
|
|
12
|
+
* no further callbacks invoked for this event. The caller decides how to
|
|
13
|
+
* surface the halt. Never re-raised through the dispatch loop.
|
|
14
|
+
* - any other `Error` → propagates unchanged. Treated as a hook bug;
|
|
15
|
+
* dispatch unwinds.
|
|
16
|
+
*
|
|
17
|
+
* Python parity note (intentional, documented): the original emits the
|
|
18
|
+
* non-fatal warning via `warnings.warn`, whose surface format
|
|
19
|
+
* (`<file>:<line>: UserWarning: …`) and per-location de-duplication are
|
|
20
|
+
* interpreter-internal and not portable. This twin emits the same message
|
|
21
|
+
* to `process.stderr` through the `warn` seam. The observable contract —
|
|
22
|
+
* the HookError is swallowed and dispatch continues — is preserved; the
|
|
23
|
+
* exact stderr warning text is normalised in the golden harness.
|
|
24
|
+
*/
|
|
25
|
+
import type { HookContext } from './context.js';
|
|
26
|
+
import type { HookEvent } from './events.js';
|
|
27
|
+
import { HookError, HookHalt } from './exceptions.js';
|
|
28
|
+
import { HookRegistry } from './registry.js';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Emit hook events through a {@link HookRegistry}.
|
|
32
|
+
*
|
|
33
|
+
* Construct once per CLI invocation, share between the CLI and the
|
|
34
|
+
* dispatcher. `emit` is the only public method on the hot path.
|
|
35
|
+
*/
|
|
36
|
+
export class HookRunner {
|
|
37
|
+
private _registry: HookRegistry;
|
|
38
|
+
|
|
39
|
+
constructor(registry: HookRegistry | null = null) {
|
|
40
|
+
this._registry = registry !== null ? registry : new HookRegistry();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Return the underlying registry.
|
|
45
|
+
*
|
|
46
|
+
* Exposed so callers can register additional hooks after construction
|
|
47
|
+
* (e.g. in tests). Not used on the hot path.
|
|
48
|
+
*/
|
|
49
|
+
get registry(): HookRegistry {
|
|
50
|
+
return this._registry;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Fire all callbacks registered for `event`.
|
|
55
|
+
*
|
|
56
|
+
* Returns `null` when every callback completed (with or without a
|
|
57
|
+
* swallowed {@link HookError}). Returns the first {@link HookHalt}
|
|
58
|
+
* raised, after which no further callbacks are invoked for this event.
|
|
59
|
+
* Any other error propagates.
|
|
60
|
+
*/
|
|
61
|
+
emit(event: HookEvent, ctx: HookContext): HookHalt | null {
|
|
62
|
+
const callbacks = this._registry.for_event(event);
|
|
63
|
+
if (callbacks.length === 0) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
for (const callback of callbacks) {
|
|
67
|
+
try {
|
|
68
|
+
callback(ctx);
|
|
69
|
+
} catch (exc) {
|
|
70
|
+
if (exc instanceof HookHalt) {
|
|
71
|
+
return exc;
|
|
72
|
+
}
|
|
73
|
+
if (exc instanceof HookError) {
|
|
74
|
+
this.warn(`hook ${event} raised HookError: ${exc.message}`);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
throw exc;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Emit a non-fatal hook warning. Test seam — overridable. Mirrors the
|
|
85
|
+
* role of `warnings.warn` in the Python original.
|
|
86
|
+
*/
|
|
87
|
+
protected warn(message: string): void {
|
|
88
|
+
process.stderr.write(`${message}\n`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read `hooks.*` from `.agent-settings.yml` into {@link HookSettings}.
|
|
3
|
+
*
|
|
4
|
+
* TypeScript twin of `work_engine/hooks/settings.py` (ADR-200 py2ts —
|
|
5
|
+
* work_engine.hooks subpackage). Mirror of the chat-history settings pattern:
|
|
6
|
+
*
|
|
7
|
+
* - The YAML read goes through `work_engine/_lib/agent_settings.load_agent_settings`,
|
|
8
|
+
* which cascades the whitelisted DX-comfort keys from the user-global file.
|
|
9
|
+
* - Default-permissive: a missing file or missing `hooks:` block returns
|
|
10
|
+
* {@link HookSettings} with `enabled=false` — every hook off, every golden
|
|
11
|
+
* replay safe by construction.
|
|
12
|
+
* - Malformed YAML / unreadable file → defaults; degrade silently.
|
|
13
|
+
* - Chat-history hooks gate on **two** switches: `hooks.chat_history.enabled`
|
|
14
|
+
* AND the global `chat_history.enabled`. Either off → no chat-history hook.
|
|
15
|
+
*/
|
|
16
|
+
import { load_agent_settings, type SettingsDict } from '../_lib/agent_settings.js';
|
|
17
|
+
import {
|
|
18
|
+
DecisionEngineConfigError,
|
|
19
|
+
DecisionEngineSettings,
|
|
20
|
+
parse as _parse_decision_engine,
|
|
21
|
+
} from '../scoring/decision_engine.js';
|
|
22
|
+
|
|
23
|
+
export const DEFAULT_SETTINGS_FILE = '.agent-settings.yml';
|
|
24
|
+
export const DEFAULT_CHAT_HISTORY_SCRIPT = 'scripts/chat_history.py';
|
|
25
|
+
|
|
26
|
+
/** Arbitrary value, mirroring the Python `Any` fields. */
|
|
27
|
+
type Any = unknown;
|
|
28
|
+
|
|
29
|
+
/** Per-field init shape for {@link HookSettings} (mirrors the dataclass). */
|
|
30
|
+
export interface HookSettingsInit {
|
|
31
|
+
enabled?: boolean;
|
|
32
|
+
trace?: boolean;
|
|
33
|
+
halt_surface_audit?: boolean;
|
|
34
|
+
state_shape_validation?: boolean;
|
|
35
|
+
directive_set_guard?: boolean;
|
|
36
|
+
decision_trace?: boolean;
|
|
37
|
+
memory_visibility?: boolean;
|
|
38
|
+
memory_visibility_off?: boolean;
|
|
39
|
+
memory_cadence?: string;
|
|
40
|
+
chat_history_enabled?: boolean;
|
|
41
|
+
chat_history_script?: string;
|
|
42
|
+
decision_engine?: DecisionEngineSettings;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Resolved view of the `hooks:` block.
|
|
47
|
+
*
|
|
48
|
+
* `enabled` is the master switch. When `false` the registry stays empty
|
|
49
|
+
* regardless of the per-hook fields; this is the default when no settings
|
|
50
|
+
* file exists or no `hooks` block is declared, and it is what keeps
|
|
51
|
+
* golden-replay tests byte-stable.
|
|
52
|
+
*
|
|
53
|
+
* `decision_engine` carries the parsed gate config so {@link DecisionGateHook}
|
|
54
|
+
* can read it without re-parsing `.agent-settings.yml`.
|
|
55
|
+
*/
|
|
56
|
+
export class HookSettings {
|
|
57
|
+
readonly enabled: boolean;
|
|
58
|
+
readonly trace: boolean;
|
|
59
|
+
readonly halt_surface_audit: boolean;
|
|
60
|
+
readonly state_shape_validation: boolean;
|
|
61
|
+
readonly directive_set_guard: boolean;
|
|
62
|
+
readonly decision_trace: boolean;
|
|
63
|
+
readonly memory_visibility: boolean;
|
|
64
|
+
readonly memory_visibility_off: boolean;
|
|
65
|
+
readonly memory_cadence: string;
|
|
66
|
+
readonly chat_history_enabled: boolean;
|
|
67
|
+
readonly chat_history_script: string;
|
|
68
|
+
readonly decision_engine: DecisionEngineSettings;
|
|
69
|
+
|
|
70
|
+
constructor(init: HookSettingsInit = {}) {
|
|
71
|
+
this.enabled = init.enabled ?? false;
|
|
72
|
+
this.trace = init.trace ?? false;
|
|
73
|
+
this.halt_surface_audit = init.halt_surface_audit ?? false;
|
|
74
|
+
this.state_shape_validation = init.state_shape_validation ?? false;
|
|
75
|
+
this.directive_set_guard = init.directive_set_guard ?? false;
|
|
76
|
+
this.decision_trace = init.decision_trace ?? false;
|
|
77
|
+
this.memory_visibility = init.memory_visibility ?? false;
|
|
78
|
+
this.memory_visibility_off = init.memory_visibility_off ?? false;
|
|
79
|
+
this.memory_cadence = init.memory_cadence ?? 'always';
|
|
80
|
+
this.chat_history_enabled = init.chat_history_enabled ?? false;
|
|
81
|
+
this.chat_history_script = init.chat_history_script ?? DEFAULT_CHAT_HISTORY_SCRIPT;
|
|
82
|
+
this.decision_engine = init.decision_engine ?? new DecisionEngineSettings();
|
|
83
|
+
Object.freeze(this);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const _DEFAULT = new HookSettings();
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Return {@link HookSettings} hydrated from `.agent-settings.yml`.
|
|
91
|
+
*
|
|
92
|
+
* `settings_path` defaults to `./.agent-settings.yml` relative to the
|
|
93
|
+
* current working directory. `user_global_path` defaults to
|
|
94
|
+
* `~/.event4u/agent-config/agent-settings.yml` and only cascades the
|
|
95
|
+
* whitelisted DX-comfort keys when the project file omits them.
|
|
96
|
+
*/
|
|
97
|
+
export function load_hook_settings(
|
|
98
|
+
settings_path: string | null = null,
|
|
99
|
+
user_global_path: string | null = null,
|
|
100
|
+
): HookSettings {
|
|
101
|
+
const path = settings_path ? settings_path : DEFAULT_SETTINGS_FILE;
|
|
102
|
+
const raw = load_agent_settings({
|
|
103
|
+
project_path: path,
|
|
104
|
+
user_global_path,
|
|
105
|
+
});
|
|
106
|
+
if (!_pyTruthy(raw)) {
|
|
107
|
+
return _DEFAULT;
|
|
108
|
+
}
|
|
109
|
+
return _settings_from_raw(raw);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function _settings_from_raw(data: SettingsDict): HookSettings {
|
|
113
|
+
const hooks = data['hooks'];
|
|
114
|
+
if (!_isPlainDict(hooks)) {
|
|
115
|
+
return _DEFAULT;
|
|
116
|
+
}
|
|
117
|
+
const hooksDict = hooks as Record<string, Any>;
|
|
118
|
+
const enabled = _coerce_bool(hooksDict['enabled'], false);
|
|
119
|
+
|
|
120
|
+
const decision_engine_raw = data['decision_engine'];
|
|
121
|
+
let decision_engine_settings: DecisionEngineSettings;
|
|
122
|
+
try {
|
|
123
|
+
decision_engine_settings = _parse_decision_engine(decision_engine_raw);
|
|
124
|
+
} catch (exc) {
|
|
125
|
+
if (exc instanceof DecisionEngineConfigError) {
|
|
126
|
+
decision_engine_settings = new DecisionEngineSettings();
|
|
127
|
+
} else {
|
|
128
|
+
throw exc;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!enabled) {
|
|
133
|
+
return new HookSettings({
|
|
134
|
+
enabled: false,
|
|
135
|
+
decision_engine: decision_engine_settings,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const chat_section = hooksDict['chat_history'];
|
|
140
|
+
let chat_block_enabled: boolean;
|
|
141
|
+
let chat_script: string;
|
|
142
|
+
if (_isPlainDict(chat_section)) {
|
|
143
|
+
const cs = chat_section as Record<string, Any>;
|
|
144
|
+
chat_block_enabled = _coerce_bool(cs['enabled'], true);
|
|
145
|
+
chat_script = String(_pyOr(cs['script'], DEFAULT_CHAT_HISTORY_SCRIPT));
|
|
146
|
+
} else {
|
|
147
|
+
chat_block_enabled = true;
|
|
148
|
+
chat_script = DEFAULT_CHAT_HISTORY_SCRIPT;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const global_chat = data['chat_history'];
|
|
152
|
+
const global_chat_on =
|
|
153
|
+
_isPlainDict(global_chat) &&
|
|
154
|
+
_coerce_bool((global_chat as Record<string, Any>)['enabled'], false);
|
|
155
|
+
|
|
156
|
+
const decision_trace_on = decision_engine_settings.surface_traces;
|
|
157
|
+
|
|
158
|
+
const memory_section = data['memory'];
|
|
159
|
+
let visibility_off = false;
|
|
160
|
+
let memory_cadence = 'always';
|
|
161
|
+
if (_isPlainDict(memory_section)) {
|
|
162
|
+
const ms = memory_section as Record<string, Any>;
|
|
163
|
+
const rawVis = ms['visibility'];
|
|
164
|
+
if (typeof rawVis === 'string' && rawVis.trim().toLowerCase() === 'off') {
|
|
165
|
+
visibility_off = true;
|
|
166
|
+
} else if (typeof rawVis === 'boolean' && rawVis === false) {
|
|
167
|
+
visibility_off = true;
|
|
168
|
+
}
|
|
169
|
+
const cadence_raw = ms['cadence'];
|
|
170
|
+
if (cadence_raw !== null && cadence_raw !== undefined) {
|
|
171
|
+
memory_cadence = String(cadence_raw).trim().toLowerCase() || 'always';
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const memory_hooks = hooksDict['memory_visibility'];
|
|
176
|
+
let memory_visibility_on: boolean;
|
|
177
|
+
if (_isPlainDict(memory_hooks)) {
|
|
178
|
+
memory_visibility_on = _coerce_bool(
|
|
179
|
+
(memory_hooks as Record<string, Any>)['enabled'],
|
|
180
|
+
true,
|
|
181
|
+
);
|
|
182
|
+
} else {
|
|
183
|
+
memory_visibility_on = true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return new HookSettings({
|
|
187
|
+
enabled: true,
|
|
188
|
+
trace: _coerce_bool(hooksDict['trace'], false),
|
|
189
|
+
halt_surface_audit: _coerce_bool(hooksDict['halt_surface_audit'], true),
|
|
190
|
+
state_shape_validation: _coerce_bool(hooksDict['state_shape_validation'], true),
|
|
191
|
+
directive_set_guard: _coerce_bool(hooksDict['directive_set_guard'], true),
|
|
192
|
+
decision_trace: decision_trace_on,
|
|
193
|
+
memory_visibility: memory_visibility_on,
|
|
194
|
+
memory_visibility_off: visibility_off,
|
|
195
|
+
memory_cadence,
|
|
196
|
+
chat_history_enabled: chat_block_enabled && global_chat_on,
|
|
197
|
+
chat_history_script: chat_script,
|
|
198
|
+
decision_engine: decision_engine_settings,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function _coerce_bool(value: Any, dflt: boolean): boolean {
|
|
203
|
+
if (typeof value === 'boolean') {
|
|
204
|
+
return value;
|
|
205
|
+
}
|
|
206
|
+
if (value === null || value === undefined) {
|
|
207
|
+
return dflt;
|
|
208
|
+
}
|
|
209
|
+
if (typeof value === 'string') {
|
|
210
|
+
const s = value.trim().toLowerCase();
|
|
211
|
+
if (s === 'true' || s === 'yes' || s === 'on' || s === '1') {
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
if (s === 'false' || s === 'no' || s === 'off' || s === '0') {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return dflt;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ── Python-parity primitives ────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
/** Python `a or b` — returns `a` when truthy, else `b`. */
|
|
224
|
+
function _pyOr(a: Any, b: Any): Any {
|
|
225
|
+
return _pyTruthy(a) ? a : b;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Python `isinstance(x, dict)` — only a plain object (not array, not null). */
|
|
229
|
+
function _isPlainDict(value: Any): boolean {
|
|
230
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Python `bool(x)` truthiness: `None`/`undefined`, `False`, `0`, `""`,
|
|
235
|
+
* empty list/dict are falsy; everything else truthy.
|
|
236
|
+
*/
|
|
237
|
+
function _pyTruthy(value: Any): boolean {
|
|
238
|
+
if (value === null || value === undefined || value === false) {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
if (value === true) {
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
if (typeof value === 'number') {
|
|
245
|
+
return value !== 0;
|
|
246
|
+
}
|
|
247
|
+
if (typeof value === 'string') {
|
|
248
|
+
return value.length > 0;
|
|
249
|
+
}
|
|
250
|
+
if (Array.isArray(value)) {
|
|
251
|
+
return value.length > 0;
|
|
252
|
+
}
|
|
253
|
+
if (value instanceof Map || value instanceof Set) {
|
|
254
|
+
return value.size > 0;
|
|
255
|
+
}
|
|
256
|
+
if (typeof value === 'object') {
|
|
257
|
+
return Object.keys(value as Record<string, unknown>).length > 0;
|
|
258
|
+
}
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-based input builders and the load-or-build dispatch helper.
|
|
3
|
+
*
|
|
4
|
+
* TypeScript twin of `work_engine/input_builders.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 the
|
|
10
|
+
* CLI's "first run" path: when no state file exists, build a fresh `WorkState`
|
|
11
|
+
* from `--ticket-file`, `--prompt-file`, `--diff-file` or `--file-file`. Every
|
|
12
|
+
* builder is byte-identical in behaviour to the pre-split version.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import * as fs from 'node:fs';
|
|
16
|
+
|
|
17
|
+
import { _FMT_V0, _FMT_V1, type ParsedArgs } from './cli_args.js';
|
|
18
|
+
import { _CLIError } from './errors.js';
|
|
19
|
+
import { populate_routing } from './intent/classify.js';
|
|
20
|
+
import { DiffResolverError, build_envelope as _build_diff_envelope } from './resolvers/diff.js';
|
|
21
|
+
import { FileResolverError, build_envelope as _build_file_envelope } from './resolvers/file.js';
|
|
22
|
+
import { PromptResolverError, build_envelope as _build_prompt_envelope } from './resolvers/prompt.js';
|
|
23
|
+
import { Input, WorkState, type Dict } from './state.js';
|
|
24
|
+
import { _load, _maybe_raise_legacy_hint, _read_json } from './state_io.js';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Return the WorkState to dispatch against plus its wire format.
|
|
28
|
+
*
|
|
29
|
+
* Either loaded from `state_file` (format-preserving) or freshly built from
|
|
30
|
+
* `--ticket-file` (R1), `--prompt-file` (R2), `--diff-file` (R3) or
|
|
31
|
+
* `--file-file` (R3). Fresh ticket files default to v0 wire format so newly
|
|
32
|
+
* captured Goldens stay byte-equal with the pre-Phase-4 baseline; the prompt /
|
|
33
|
+
* diff / file paths emit v1 directly (v0 has no envelope concept for these
|
|
34
|
+
* kinds). v1 round-trips for state files already on disk in v1 shape.
|
|
35
|
+
*/
|
|
36
|
+
export function _load_or_build(state_file: string, args: ParsedArgs): [WorkState, string] {
|
|
37
|
+
if (_exists(state_file)) {
|
|
38
|
+
return _load(state_file);
|
|
39
|
+
}
|
|
40
|
+
_maybe_raise_legacy_hint(state_file);
|
|
41
|
+
const inputs: Array<[string, string | null]> = [
|
|
42
|
+
['--ticket-file', args.ticket_file],
|
|
43
|
+
['--prompt-file', args.prompt_file],
|
|
44
|
+
['--diff-file', args.diff_file],
|
|
45
|
+
['--file-file', args.file_file],
|
|
46
|
+
];
|
|
47
|
+
const supplied = inputs.filter(([, value]) => value !== null).map(([name]) => name);
|
|
48
|
+
if (supplied.length > 1) {
|
|
49
|
+
throw new _CLIError(
|
|
50
|
+
`${supplied.join(', ')} are mutually exclusive; pass exactly ` +
|
|
51
|
+
'one when building an initial state.',
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
if (supplied.length === 0) {
|
|
55
|
+
throw new _CLIError(
|
|
56
|
+
`No state file at ${state_file} and no --ticket-file, ` +
|
|
57
|
+
'--prompt-file, --diff-file, or --file-file given; cannot ' +
|
|
58
|
+
'build an initial state.',
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
if (args.prompt_file !== null) {
|
|
62
|
+
return [_build_from_prompt_file(args), _FMT_V1];
|
|
63
|
+
}
|
|
64
|
+
if (args.diff_file !== null) {
|
|
65
|
+
return [_build_from_diff_file(args), _FMT_V1];
|
|
66
|
+
}
|
|
67
|
+
if (args.file_file !== null) {
|
|
68
|
+
return [_build_from_file_file(args), _FMT_V1];
|
|
69
|
+
}
|
|
70
|
+
const ticket = _read_json(args.ticket_file as string);
|
|
71
|
+
if (!_isPlainDict(ticket)) {
|
|
72
|
+
throw new _CLIError(
|
|
73
|
+
`--ticket-file must carry a JSON object; got ${_pyTypeName(ticket)}.`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
const work = new WorkState({ input: new Input('ticket', ticket as Dict) });
|
|
77
|
+
if (args.persona) {
|
|
78
|
+
work.persona = args.persona;
|
|
79
|
+
}
|
|
80
|
+
populate_routing(work);
|
|
81
|
+
return [work, _FMT_V0];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Read `--prompt-file` as raw text and wrap it in a prompt envelope.
|
|
86
|
+
*
|
|
87
|
+
* The file is read verbatim (UTF-8) and handed to the prompt resolver, which
|
|
88
|
+
* validates non-emptiness and returns the canonical
|
|
89
|
+
* `Input(kind="prompt", data={raw, reconstructed_ac, assumptions})` envelope.
|
|
90
|
+
* Persona is honoured the same way as the ticket path.
|
|
91
|
+
*/
|
|
92
|
+
export function _build_from_prompt_file(args: ParsedArgs): WorkState {
|
|
93
|
+
let raw: string;
|
|
94
|
+
try {
|
|
95
|
+
raw = fs.readFileSync(args.prompt_file as string, 'utf-8');
|
|
96
|
+
} catch (exc) {
|
|
97
|
+
throw new _CLIError(`Cannot read ${args.prompt_file}: ${_osErrorText(exc, args.prompt_file as string)}`);
|
|
98
|
+
}
|
|
99
|
+
let envelope: Input;
|
|
100
|
+
try {
|
|
101
|
+
envelope = _build_prompt_envelope(raw);
|
|
102
|
+
} catch (exc) {
|
|
103
|
+
if (exc instanceof PromptResolverError) {
|
|
104
|
+
throw new _CLIError(`--prompt-file is not a valid prompt: ${exc.message}`);
|
|
105
|
+
}
|
|
106
|
+
throw exc;
|
|
107
|
+
}
|
|
108
|
+
const work = new WorkState({ input: envelope });
|
|
109
|
+
if (args.persona) {
|
|
110
|
+
work.persona = args.persona;
|
|
111
|
+
}
|
|
112
|
+
populate_routing(work);
|
|
113
|
+
return work;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Read `--diff-file` as raw text and wrap it in a diff envelope.
|
|
118
|
+
*
|
|
119
|
+
* The file is read verbatim (UTF-8) and handed to the diff resolver, which
|
|
120
|
+
* validates the unified-diff header heuristic and returns the canonical
|
|
121
|
+
* `Input(kind="diff", data={raw, reconstructed_ac, assumptions})` envelope.
|
|
122
|
+
* `populate_routing` then routes the envelope to the UI-improve directive set
|
|
123
|
+
* without running the prose classifier.
|
|
124
|
+
*/
|
|
125
|
+
export function _build_from_diff_file(args: ParsedArgs): WorkState {
|
|
126
|
+
let raw: string;
|
|
127
|
+
try {
|
|
128
|
+
raw = fs.readFileSync(args.diff_file as string, 'utf-8');
|
|
129
|
+
} catch (exc) {
|
|
130
|
+
throw new _CLIError(`Cannot read ${args.diff_file}: ${_osErrorText(exc, args.diff_file as string)}`);
|
|
131
|
+
}
|
|
132
|
+
let envelope: Input;
|
|
133
|
+
try {
|
|
134
|
+
envelope = _build_diff_envelope(raw);
|
|
135
|
+
} catch (exc) {
|
|
136
|
+
if (exc instanceof DiffResolverError) {
|
|
137
|
+
throw new _CLIError(`--diff-file is not a valid diff: ${exc.message}`);
|
|
138
|
+
}
|
|
139
|
+
throw exc;
|
|
140
|
+
}
|
|
141
|
+
const work = new WorkState({ input: envelope });
|
|
142
|
+
if (args.persona) {
|
|
143
|
+
work.persona = args.persona;
|
|
144
|
+
}
|
|
145
|
+
populate_routing(work);
|
|
146
|
+
return work;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Read `--file-file` as a single-line path and wrap it in a file envelope.
|
|
151
|
+
*
|
|
152
|
+
* The file is read verbatim (UTF-8); the first non-empty line is taken as the
|
|
153
|
+
* path reference and handed to the file resolver, which validates path shape
|
|
154
|
+
* (non-empty, NUL-free, not a URL) and returns the canonical
|
|
155
|
+
* `Input(kind="file", data={path, reconstructed_ac, assumptions})` envelope.
|
|
156
|
+
* Trailing whitespace and additional lines are ignored.
|
|
157
|
+
*/
|
|
158
|
+
export function _build_from_file_file(args: ParsedArgs): WorkState {
|
|
159
|
+
let raw: string;
|
|
160
|
+
try {
|
|
161
|
+
raw = fs.readFileSync(args.file_file as string, 'utf-8');
|
|
162
|
+
} catch (exc) {
|
|
163
|
+
throw new _CLIError(`Cannot read ${args.file_file}: ${_osErrorText(exc, args.file_file as string)}`);
|
|
164
|
+
}
|
|
165
|
+
// Python: `raw.strip().splitlines()[0] if raw.strip() else ""`.
|
|
166
|
+
const stripped = _pyStrip(raw);
|
|
167
|
+
const path_ = stripped ? _firstLine(stripped) : '';
|
|
168
|
+
let envelope: Input;
|
|
169
|
+
try {
|
|
170
|
+
envelope = _build_file_envelope(path_);
|
|
171
|
+
} catch (exc) {
|
|
172
|
+
if (exc instanceof FileResolverError) {
|
|
173
|
+
throw new _CLIError(`--file-file does not carry a valid path: ${exc.message}`);
|
|
174
|
+
}
|
|
175
|
+
throw exc;
|
|
176
|
+
}
|
|
177
|
+
const work = new WorkState({ input: envelope });
|
|
178
|
+
if (args.persona) {
|
|
179
|
+
work.persona = args.persona;
|
|
180
|
+
}
|
|
181
|
+
populate_routing(work);
|
|
182
|
+
return work;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── Python-parity helpers ────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
function _exists(p: string): boolean {
|
|
188
|
+
try {
|
|
189
|
+
fs.statSync(p);
|
|
190
|
+
return true;
|
|
191
|
+
} catch {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Python `isinstance(x, dict)` — plain object only (not array, not null). */
|
|
197
|
+
function _isPlainDict(value: unknown): boolean {
|
|
198
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Python `type(x).__name__` for the JSON value shapes the messages emit. */
|
|
202
|
+
function _pyTypeName(value: unknown): string {
|
|
203
|
+
if (value === null || value === undefined) {
|
|
204
|
+
return 'NoneType';
|
|
205
|
+
}
|
|
206
|
+
if (Array.isArray(value)) {
|
|
207
|
+
return 'list';
|
|
208
|
+
}
|
|
209
|
+
switch (typeof value) {
|
|
210
|
+
case 'string':
|
|
211
|
+
return 'str';
|
|
212
|
+
case 'boolean':
|
|
213
|
+
return 'bool';
|
|
214
|
+
case 'number':
|
|
215
|
+
return Number.isInteger(value as number) ? 'int' : 'float';
|
|
216
|
+
case 'object':
|
|
217
|
+
return 'dict';
|
|
218
|
+
default:
|
|
219
|
+
return typeof value;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Python `str.strip()` — strip leading/trailing whitespace. */
|
|
224
|
+
function _pyStrip(s: string): string {
|
|
225
|
+
// Python str.strip() with no arg removes ASCII + Unicode whitespace; JS \s
|
|
226
|
+
// covers the common set adequately for the single-line-path contract.
|
|
227
|
+
return s.replace(/^\s+/u, '').replace(/\s+$/u, '');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Python `str.splitlines()[0]` on an already-`strip()`ped string.
|
|
232
|
+
*
|
|
233
|
+
* The caller consumes only the first line; the stripped input has no leading
|
|
234
|
+
* boundary, so splitting on the universal-newline set and returning element 0
|
|
235
|
+
* is byte-equivalent to `splitlines()[0]` for the single-line-path contract.
|
|
236
|
+
*/
|
|
237
|
+
function _firstLine(s: string): string {
|
|
238
|
+
const m = s.split(/\r\n|[\n\r\v\f\x1c\x1d\x1e\x85\u2028\u2029]/u);
|
|
239
|
+
return m[0] ?? '';
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Approximate the text of Python's `OSError` for a failed read (mirrors
|
|
244
|
+
* `state_io.ts::_osErrorText`). The error *channel* (exit 2 via `_CLIError`)
|
|
245
|
+
* and the *prefix* ("Cannot read <path>: ") stay identical; only the trailing
|
|
246
|
+
* OS detail differs — tests normalise it.
|
|
247
|
+
*/
|
|
248
|
+
function _osErrorText(exc: unknown, p: string): string {
|
|
249
|
+
const e = exc as NodeJS.ErrnoException;
|
|
250
|
+
if (e && e.code === 'ENOENT') {
|
|
251
|
+
return `[Errno 2] No such file or directory: '${p}'`;
|
|
252
|
+
}
|
|
253
|
+
if (e && e.code === 'EISDIR') {
|
|
254
|
+
return `[Errno 21] Is a directory: '${p}'`;
|
|
255
|
+
}
|
|
256
|
+
if (e && e.code === 'EACCES') {
|
|
257
|
+
return `[Errno 13] Permission denied: '${p}'`;
|
|
258
|
+
}
|
|
259
|
+
return e && e.message ? e.message : String(exc);
|
|
260
|
+
}
|