@event4u/agent-config 8.3.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 (140) hide show
  1. package/.claude-plugin/marketplace.json +4 -426
  2. package/.claude-plugin/skills/install-agent-config/SKILL.md +32 -0
  3. package/CHANGELOG.md +25 -0
  4. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  5. package/dist/cli/registry.js +1 -0
  6. package/dist/cli/registry.js.map +1 -1
  7. package/dist/discovery/deprecation-report.md +1 -1
  8. package/dist/discovery/discovery-manifest.json +1 -1
  9. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  10. package/dist/discovery/discovery-manifest.summary.md +1 -1
  11. package/dist/discovery/orphan-report.md +1 -1
  12. package/dist/discovery/packs.json +1 -1
  13. package/dist/discovery/trust-report.md +1 -1
  14. package/dist/discovery/workspaces.json +1 -1
  15. package/dist/mcp/registry-manifest.json +1 -1
  16. package/docs/contracts/surface-matrix.md +103 -0
  17. package/package.json +4 -1
  18. package/src/agent-src/README.md +64 -0
  19. package/src/agent-src/scripts/archive_completed_roadmaps.ts +497 -0
  20. package/src/agent-src/scripts/scan-seeder-violations.php +145 -0
  21. package/src/agent-src/scripts/update_roadmap_progress.ts +1087 -0
  22. package/src/agent-src/templates/AGENTS.md +34 -0
  23. package/src/agent-src/templates/scripts/README.md +195 -0
  24. package/src/agent-src/templates/scripts/check_memory.ts +662 -0
  25. package/src/agent-src/templates/scripts/check_memory_proposal.ts +370 -0
  26. package/src/agent-src/templates/scripts/historical-bug-patterns.example.yml +84 -0
  27. package/src/agent-src/templates/scripts/implement_ticket/__main__.ts +27 -0
  28. package/src/agent-src/templates/scripts/memory_hash.ts +355 -0
  29. package/src/agent-src/templates/scripts/memory_lookup.ts +1089 -0
  30. package/src/agent-src/templates/scripts/memory_report.ts +868 -0
  31. package/src/agent-src/templates/scripts/memory_signal.ts +444 -0
  32. package/src/agent-src/templates/scripts/memory_status.ts +215 -0
  33. package/src/agent-src/templates/scripts/ownership-map.example.yml +87 -0
  34. package/src/agent-src/templates/scripts/pr-risk-config.example.yml +76 -0
  35. package/src/agent-src/templates/scripts/pr_review_routing.ts +544 -0
  36. package/src/agent-src/templates/scripts/pr_risk_review.ts +0 -0
  37. package/src/agent-src/templates/scripts/telemetry/aggregator.ts +0 -0
  38. package/src/agent-src/templates/scripts/telemetry/boundary.ts +164 -0
  39. package/src/agent-src/templates/scripts/telemetry/engagement.ts +479 -0
  40. package/src/agent-src/templates/scripts/telemetry/report_renderer.ts +394 -0
  41. package/src/agent-src/templates/scripts/telemetry/settings.ts +210 -0
  42. package/src/agent-src/templates/scripts/telemetry_record.ts +276 -0
  43. package/src/agent-src/templates/scripts/telemetry_report.ts +211 -0
  44. package/src/agent-src/templates/scripts/telemetry_status.ts +333 -0
  45. package/src/agent-src/templates/scripts/tier_usage_report.ts +619 -0
  46. package/src/agent-src/templates/scripts/work_engine/__main__.ts +14 -0
  47. package/src/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +1261 -0
  48. package/src/agent-src/templates/scripts/work_engine/_lib/user_global_paths.ts +329 -0
  49. package/src/agent-src/templates/scripts/work_engine/cli.ts +206 -0
  50. package/src/agent-src/templates/scripts/work_engine/cli_args.ts +249 -0
  51. package/src/agent-src/templates/scripts/work_engine/delivery_state.ts +225 -0
  52. package/src/agent-src/templates/scripts/work_engine/directives/backend/analyze.ts +125 -0
  53. package/src/agent-src/templates/scripts/work_engine/directives/backend/implement.ts +189 -0
  54. package/src/agent-src/templates/scripts/work_engine/directives/backend/index.ts +94 -0
  55. package/src/agent-src/templates/scripts/work_engine/directives/backend/memory.ts +193 -0
  56. package/src/agent-src/templates/scripts/work_engine/directives/backend/plan.ts +267 -0
  57. package/src/agent-src/templates/scripts/work_engine/directives/backend/refine.ts +518 -0
  58. package/src/agent-src/templates/scripts/work_engine/directives/backend/report.ts +379 -0
  59. package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +268 -0
  60. package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +258 -0
  61. package/src/agent-src/templates/scripts/work_engine/directives/index.ts +32 -0
  62. package/src/agent-src/templates/scripts/work_engine/directives/mixed/contract.ts +243 -0
  63. package/src/agent-src/templates/scripts/work_engine/directives/mixed/index.ts +108 -0
  64. package/src/agent-src/templates/scripts/work_engine/directives/mixed/stitch.ts +259 -0
  65. package/src/agent-src/templates/scripts/work_engine/directives/mixed/ui.ts +216 -0
  66. package/src/agent-src/templates/scripts/work_engine/directives/ui/_passthrough.ts +40 -0
  67. package/src/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +241 -0
  68. package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +216 -0
  69. package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +506 -0
  70. package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +325 -0
  71. package/src/agent-src/templates/scripts/work_engine/directives/ui/index.ts +102 -0
  72. package/src/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +462 -0
  73. package/src/agent-src/templates/scripts/work_engine/directives/ui/review.ts +474 -0
  74. package/src/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +352 -0
  75. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/_skipped.ts +33 -0
  76. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/apply.ts +213 -0
  77. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/index.ts +111 -0
  78. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/refine.ts +126 -0
  79. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/report.ts +112 -0
  80. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/test.ts +164 -0
  81. package/src/agent-src/templates/scripts/work_engine/dispatcher.ts +515 -0
  82. package/src/agent-src/templates/scripts/work_engine/emitters.ts +119 -0
  83. package/src/agent-src/templates/scripts/work_engine/errors.ts +24 -0
  84. package/src/agent-src/templates/scripts/work_engine/hook_bootstrap.ts +104 -0
  85. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/_chat_history_base.ts +189 -0
  86. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_append.ts +41 -0
  87. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_halt_append.ts +89 -0
  88. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/decision_gate.ts +193 -0
  89. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/decision_trace.ts +304 -0
  90. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/directive_set_guard.ts +110 -0
  91. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/halt_surface_audit.ts +118 -0
  92. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/index.ts +17 -0
  93. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/memory_visibility.ts +161 -0
  94. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/state_shape_validation.ts +45 -0
  95. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/trace.ts +134 -0
  96. package/src/agent-src/templates/scripts/work_engine/hooks/context.ts +94 -0
  97. package/src/agent-src/templates/scripts/work_engine/hooks/events.ts +58 -0
  98. package/src/agent-src/templates/scripts/work_engine/hooks/exceptions.ts +85 -0
  99. package/src/agent-src/templates/scripts/work_engine/hooks/index.ts +27 -0
  100. package/src/agent-src/templates/scripts/work_engine/hooks/registry.ts +66 -0
  101. package/src/agent-src/templates/scripts/work_engine/hooks/runner.ts +90 -0
  102. package/src/agent-src/templates/scripts/work_engine/hooks/settings.ts +260 -0
  103. package/src/agent-src/templates/scripts/work_engine/input_builders.ts +260 -0
  104. package/src/agent-src/templates/scripts/work_engine/intent/classify.ts +466 -0
  105. package/src/agent-src/templates/scripts/work_engine/migration/v0_to_v1.ts +531 -0
  106. package/src/agent-src/templates/scripts/work_engine/orchestration.ts +366 -0
  107. package/src/agent-src/templates/scripts/work_engine/persona_policy.ts +97 -0
  108. package/src/agent-src/templates/scripts/work_engine/resolvers/diff.ts +135 -0
  109. package/src/agent-src/templates/scripts/work_engine/resolvers/file.ts +175 -0
  110. package/src/agent-src/templates/scripts/work_engine/resolvers/prompt.ts +115 -0
  111. package/src/agent-src/templates/scripts/work_engine/scoring/confidence.ts +415 -0
  112. package/src/agent-src/templates/scripts/work_engine/scoring/decision_engine.ts +466 -0
  113. package/src/agent-src/templates/scripts/work_engine/scoring/decision_trace.ts +298 -0
  114. package/src/agent-src/templates/scripts/work_engine/scoring/memory_visibility.ts +444 -0
  115. package/src/agent-src/templates/scripts/work_engine/stack/detect.ts +252 -0
  116. package/src/agent-src/templates/scripts/work_engine/stack/runner.ts +745 -0
  117. package/src/agent-src/templates/scripts/work_engine/state.ts +1151 -0
  118. package/src/agent-src/templates/scripts/work_engine/state_io.ts +413 -0
  119. package/src/config/surface-matrix.yml +213 -0
  120. package/src/install/atomic.ts +111 -0
  121. package/src/install/conflict.ts +226 -0
  122. package/src/install/detect.ts +280 -0
  123. package/src/install/paths.ts +90 -0
  124. package/src/install/plan.ts +205 -0
  125. package/src/install/preflight.ts +210 -0
  126. package/src/install/selectedTools.ts +52 -0
  127. package/src/install/toolDetection.ts +120 -0
  128. package/src/install/txlog.ts +161 -0
  129. package/src/install/types.ts +175 -0
  130. package/src/install/wizard-plan.ts +216 -0
  131. package/src/scripts/_cli/cmd_converge.ts +399 -0
  132. package/src/scripts/_cli/cmd_doctor.ts +94 -0
  133. package/src/scripts/_cli/cmd_upgrade.ts +40 -16
  134. package/src/scripts/_dispatch.bash +13 -0
  135. package/src/scripts/condense.ts +67 -36
  136. package/src/scripts/hook_manifest.yaml +16 -7
  137. package/src/scripts/lint_marketplace.ts +54 -56
  138. package/src/scripts/lint_surface_matrix.ts +200 -0
  139. package/src/scripts/prepack-check.mjs +108 -5
  140. package/src/scripts/surface_probe_hook.ts +231 -0
@@ -0,0 +1,413 @@
1
+ /**
2
+ * State-file I/O helpers for the CLI entry point.
3
+ *
4
+ * TypeScript twin of `work_engine/state_io.py` (ADR-200 py2ts Phase 1 —
5
+ * work_engine foundation). Public API names stay snake_case to mirror the
6
+ * Python module 1:1 (per ADR-200 — Python style is part of the contract).
7
+ *
8
+ * Extracted from `cli.py` in P2.3 of `road-to-post-pr29-optimize.md`. Holds
9
+ * the format-preserving load/save pair, the v0 legacy serialiser, the JSON
10
+ * reader, the `DeliveryState` projection helpers, and the legacy-file
11
+ * migration hint. Behaviour is byte-identical to the pre-split version —
12
+ * Goldens stay green.
13
+ */
14
+
15
+ import * as fs from 'node:fs';
16
+ import * as path from 'node:path';
17
+
18
+ import { DEFAULT_STATE_FILE, LEGACY_STATE_FILE, _FMT_V0, _FMT_V1 } from './cli_args.js';
19
+ import {
20
+ DeliveryState,
21
+ type Any as DeliveryAny,
22
+ } from './delivery_state.js';
23
+ import { _CLIError } from './errors.js';
24
+ import type {
25
+ WorkState} from './state.js';
26
+ import {
27
+ DEFAULT_DIRECTIVE_SET,
28
+ DEFAULT_INTENT,
29
+ SCHEMA_VERSION,
30
+ SchemaError,
31
+ type Dict,
32
+ type JsonValue,
33
+ from_dict as _state_from_dict,
34
+ to_dict as _state_to_dict,
35
+ } from './state.js';
36
+
37
+ /**
38
+ * Surface a migration hint when only the pre-1.15.0 file is present.
39
+ *
40
+ * The dispatcher renamed the default state file from
41
+ * `.implement-ticket-state.json` to `.work-state.json` in 1.15.0
42
+ * (alongside the `implement_ticket → work_engine` package move).
43
+ * Existing checkouts that still carry the legacy file would otherwise
44
+ * fail with a generic "no state file" message. This helper detects
45
+ * the legacy file in the same directory and points the user at the
46
+ * one-shot migration command instead.
47
+ *
48
+ * Only fires when `state_file` has the canonical default name and
49
+ * sits next to a legacy file — explicit `--state-file` overrides
50
+ * bypass the hint so power users can carry their own naming scheme.
51
+ */
52
+ export function _maybe_raise_legacy_hint(state_file: string): void {
53
+ // Python compares `state_file.name` (the basename) against the default's
54
+ // basename. The constants are plain strings (no directory part), so
55
+ // `path.basename(state_file)` vs the constant matches `Path.name`.
56
+ if (path.basename(state_file) !== path.basename(DEFAULT_STATE_FILE)) {
57
+ return;
58
+ }
59
+ // `state_file.with_name(LEGACY_STATE_FILE.name)` — same directory, legacy
60
+ // basename.
61
+ const legacy_candidate = path.join(path.dirname(state_file), path.basename(LEGACY_STATE_FILE));
62
+ if (!_isFile(legacy_candidate)) {
63
+ return;
64
+ }
65
+ throw new _CLIError(
66
+ `Found legacy state file ${legacy_candidate} but no ` +
67
+ `${state_file}. The default state file was renamed in 1.15.0. ` +
68
+ `Run \`node node_modules/.bin/tsx work_engine/migration/v0_to_v1.ts ` +
69
+ `${legacy_candidate}\` to migrate, or pass \`--state-file ` +
70
+ `${legacy_candidate}\` to keep using the old name. See ` +
71
+ 'docs/MIGRATION.md.',
72
+ );
73
+ }
74
+
75
+ /** Load `state_file` and tag it with the wire format detected. */
76
+ export function _load(state_file: string): [WorkState, string] {
77
+ const data = _read_json(state_file);
78
+ if (!_isPlainDict(data)) {
79
+ throw new _CLIError(
80
+ `State file ${state_file} must carry a JSON object; ` +
81
+ `got ${_pyTypeName(data)}.`,
82
+ );
83
+ }
84
+ const d = data as Record<string, JsonValue>;
85
+
86
+ // v1 declares `version`; v0 has none. Anything else is invalid.
87
+ if (_get(d, 'version', undefined) === SCHEMA_VERSION) {
88
+ try {
89
+ return [_state_from_dict(d as JsonValue), _FMT_V1];
90
+ } catch (exc) {
91
+ if (exc instanceof SchemaError) {
92
+ throw new _CLIError(`State file shape is invalid: ${exc.message}`);
93
+ }
94
+ throw exc;
95
+ }
96
+ }
97
+ if ('version' in d) {
98
+ throw new _CLIError(
99
+ `State file shape is invalid: unsupported version ` +
100
+ `${_pyRepr(_get(d, 'version', undefined))}; expected ${SCHEMA_VERSION}`,
101
+ );
102
+ }
103
+ if (!('ticket' in d)) {
104
+ throw new _CLIError(
105
+ "State file shape is invalid: missing 'ticket' (v0) or " +
106
+ "'version' (v1) — file is neither shape.",
107
+ );
108
+ }
109
+ try {
110
+ const migrated = migrate_payload(d);
111
+ return [_state_from_dict(migrated as JsonValue), _FMT_V0];
112
+ } catch (exc) {
113
+ if (exc instanceof SchemaError) {
114
+ throw new _CLIError(`State file shape is invalid: ${exc.message}`);
115
+ }
116
+ throw exc;
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Project `work` into a `DeliveryState` for handler dispatch.
122
+ *
123
+ * R1 P4 S1 (Option A2): handlers continue to consume `DeliveryState`
124
+ * with `state.ticket`; the `WorkState` wrapper exists at the CLI
125
+ * boundary so the dispatcher's directive-set selection has a v1
126
+ * state object to read `directive_set` from. Mutable containers
127
+ * (`memory`, `changes`, `outcomes`, `questions`) are passed
128
+ * by reference — in-place mutations land on both objects without an
129
+ * explicit sync. Reassignments (`state.plan = …`, `state.report = …`)
130
+ * are mirrored back by {@link _sync_back}.
131
+ */
132
+ export function _to_delivery(work: WorkState): DeliveryState {
133
+ return new DeliveryState({
134
+ ticket: work.input.data as Record<string, DeliveryAny>,
135
+ persona: work.persona,
136
+ memory: work.memory as Array<Record<string, DeliveryAny>>,
137
+ plan: work.plan,
138
+ changes: work.changes as Array<Record<string, DeliveryAny>>,
139
+ tests: work.tests,
140
+ verify: work.verify,
141
+ outcomes: work.outcomes,
142
+ questions: work.questions,
143
+ report: work.report,
144
+ ui_audit: work.ui_audit as Record<string, DeliveryAny> | null,
145
+ app_spec: work.app_spec as Record<string, DeliveryAny> | null,
146
+ ui_design: work.ui_design as Record<string, DeliveryAny> | null,
147
+ ui_scaffold: work.ui_scaffold as Record<string, DeliveryAny> | null,
148
+ ui_review: work.ui_review as Record<string, DeliveryAny> | null,
149
+ ui_polish: work.ui_polish as Record<string, DeliveryAny> | null,
150
+ contract: work.contract as Record<string, DeliveryAny> | null,
151
+ stitch: work.stitch as Record<string, DeliveryAny> | null,
152
+ stack: work.stack as Record<string, DeliveryAny> | null,
153
+ });
154
+ }
155
+
156
+ /**
157
+ * Mirror handler mutations from `delivery` back into `work`.
158
+ *
159
+ * Container fields are shared by reference (see {@link _to_delivery})
160
+ * so the assignment is a no-op for those — we still mirror them
161
+ * defensively to cover the case where a handler reassigned the
162
+ * attribute (`state.memory = [new_list]`) instead of mutating in
163
+ * place.
164
+ */
165
+ export function _sync_back(work: WorkState, delivery: DeliveryState): void {
166
+ work.input.data = delivery.ticket as Dict;
167
+ work.persona = delivery.persona;
168
+ work.memory = delivery.memory as Dict[];
169
+ work.plan = delivery.plan as JsonValue;
170
+ work.changes = delivery.changes as Dict[];
171
+ work.tests = delivery.tests as JsonValue;
172
+ work.verify = delivery.verify as JsonValue;
173
+ work.outcomes = delivery.outcomes;
174
+ work.questions = delivery.questions;
175
+ work.report = delivery.report;
176
+ work.ui_audit = delivery.ui_audit as Dict | null;
177
+ work.app_spec = delivery.app_spec as Dict | null;
178
+ work.ui_design = delivery.ui_design as Dict | null;
179
+ work.ui_scaffold = delivery.ui_scaffold as Dict | null;
180
+ work.ui_review = delivery.ui_review as Dict | null;
181
+ work.ui_polish = delivery.ui_polish as Dict | null;
182
+ work.contract = delivery.contract as Dict | null;
183
+ work.stitch = delivery.stitch as Dict | null;
184
+ work.stack = delivery.stack as Dict | null;
185
+ }
186
+
187
+ /**
188
+ * Persist `work` in the wire format it was loaded with.
189
+ *
190
+ * v1 emits the canonical envelope via {@link _state_to_dict}; v0 emits the
191
+ * legacy flat shape that `DeliveryState.asdict` used to produce, byte-identical
192
+ * to the pre-Phase-4 output so the Golden Transcript replay stays green.
193
+ */
194
+ export function _save(state_file: string, work: WorkState, fmt: string): void {
195
+ fs.mkdirSync(path.dirname(state_file), { recursive: true });
196
+ const payload = fmt === _FMT_V1 ? _state_to_dict(work) : _to_v0_dict(work);
197
+ fs.writeFileSync(
198
+ state_file,
199
+ _jsonDumps(payload) + '\n',
200
+ 'utf-8',
201
+ );
202
+ }
203
+
204
+ /**
205
+ * Serialise `work` in the legacy v0 wire format.
206
+ *
207
+ * Field order matches `DeliveryState` declaration order so
208
+ * pre-Phase-4 state files round-trip byte-equal.
209
+ */
210
+ export function _to_v0_dict(work: WorkState): Dict {
211
+ return {
212
+ ticket: work.input.data,
213
+ persona: work.persona,
214
+ memory: work.memory,
215
+ plan: work.plan,
216
+ changes: work.changes,
217
+ tests: work.tests,
218
+ verify: work.verify,
219
+ outcomes: work.outcomes,
220
+ questions: work.questions,
221
+ report: work.report,
222
+ };
223
+ }
224
+
225
+ export function _read_json(p: string): JsonValue {
226
+ let raw: string;
227
+ try {
228
+ raw = fs.readFileSync(p, 'utf-8');
229
+ } catch (exc) {
230
+ // Python `OSError` → "Cannot read {path}: {exc}".
231
+ throw new _CLIError(`Cannot read ${p}: ${_osErrorText(exc, p)}`);
232
+ }
233
+ try {
234
+ return JSON.parse(raw) as JsonValue;
235
+ } catch (exc) {
236
+ // Python `json.JSONDecodeError` → "Invalid JSON in {path}: {exc}".
237
+ throw new _CLIError(`Invalid JSON in ${p}: ${(exc as Error).message}`);
238
+ }
239
+ }
240
+
241
+ // ── Inlined v0→v1 payload migration ──────────────────────────────────────
242
+ //
243
+ // The Python source imports `migrate_payload` from
244
+ // `work_engine.migration.v0_to_v1`. A `.ts` twin may not import a `.py`
245
+ // (ADR-200), and the full `v0_to_v1` module (with its CLI / `migrate_file`
246
+ // surface) lands in a later phase. The `migrate_payload` slice `_load` needs
247
+ // is reproduced here verbatim so the load path stays byte-identical; when the
248
+ // `v0_to_v1.ts` twin lands it can re-export this or replace it 1:1.
249
+
250
+ /**
251
+ * Return the v1 form of `payload`.
252
+ *
253
+ * A payload that already declares `version: 1` is returned unchanged
254
+ * (deep-copied via `JSON.parse(JSON.stringify(...))` so the caller cannot
255
+ * accidentally mutate the input). Anything else is treated as v0 and wrapped:
256
+ * `ticket` becomes `input.data`, `input.kind` is set to `"ticket"`, and the
257
+ * engine defaults are filled in.
258
+ *
259
+ * @throws SchemaError If the payload is not a dict, declares a higher version
260
+ * than this migration knows about, or lacks a `ticket` key.
261
+ */
262
+ export function migrate_payload(payload: JsonValue): Dict {
263
+ if (!_isPlainDict(payload)) {
264
+ throw new SchemaError(
265
+ `v0 state must be a JSON object; got ${_pyTypeName(payload)}`,
266
+ );
267
+ }
268
+ const p = payload as Record<string, JsonValue>;
269
+
270
+ const declared_version = _get(p, 'version', undefined);
271
+ if (declared_version === SCHEMA_VERSION) {
272
+ return JSON.parse(JSON.stringify(p)) as Dict;
273
+ }
274
+ if (declared_version !== undefined && declared_version !== null) {
275
+ throw new SchemaError(
276
+ `cannot migrate from version ${_pyRepr(declared_version)} to ` +
277
+ `${SCHEMA_VERSION}; this script only handles v0 (no version key)`,
278
+ );
279
+ }
280
+
281
+ if (!('ticket' in p)) {
282
+ throw new SchemaError(
283
+ "v0 state must carry a 'ticket' key; got keys: " +
284
+ _pyReprList(_sortedStr(Object.keys(p))),
285
+ );
286
+ }
287
+ const ticket = p['ticket'];
288
+ if (!_isPlainDict(ticket)) {
289
+ throw new SchemaError(
290
+ `v0 state.ticket must be a JSON object; got ${_pyTypeName(ticket)}`,
291
+ );
292
+ }
293
+
294
+ return {
295
+ version: SCHEMA_VERSION,
296
+ input: { kind: 'ticket', data: ticket },
297
+ intent: DEFAULT_INTENT,
298
+ directive_set: DEFAULT_DIRECTIVE_SET,
299
+ persona: _get(p, 'persona', 'senior-engineer') as JsonValue,
300
+ memory: [...((_get(p, 'memory', []) as JsonValue[]) ?? [])],
301
+ plan: _get(p, 'plan', null) as JsonValue,
302
+ changes: [...((_get(p, 'changes', []) as JsonValue[]) ?? [])],
303
+ tests: _get(p, 'tests', null) as JsonValue,
304
+ verify: _get(p, 'verify', null) as JsonValue,
305
+ outcomes: { ...((_get(p, 'outcomes', {}) as Dict) ?? {}) },
306
+ questions: [...((_get(p, 'questions', []) as JsonValue[]) ?? [])],
307
+ report: _get(p, 'report', '') as JsonValue,
308
+ };
309
+ }
310
+
311
+ // ── Python-parity primitives ────────────────────────────────────────────
312
+
313
+ /**
314
+ * Mirror Python `json.dumps(obj, indent=2, ensure_ascii=False)`.
315
+ *
316
+ * For round-tripped JSON, `JSON.stringify` with a 2-space indent matches
317
+ * CPython byte-for-byte: 2-space indent, `": "` key separator, `{}` / `[]`
318
+ * for empties, non-ASCII verbatim. Same rationale as `state.ts::jsonDumps`:
319
+ * no float/int tag survives JSON parsing, so the CPython `N.0` divergence
320
+ * cannot arise here.
321
+ */
322
+ function _jsonDumps(obj: Dict): string {
323
+ return JSON.stringify(obj, null, 2);
324
+ }
325
+
326
+ /** `dict.get(key, default)`. */
327
+ function _get(obj: Record<string, JsonValue>, key: string, dflt: unknown): unknown {
328
+ return key in obj ? obj[key] : dflt;
329
+ }
330
+
331
+ /** Python `isinstance(x, dict)` — plain object only (not array, not null). */
332
+ function _isPlainDict(value: unknown): boolean {
333
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
334
+ }
335
+
336
+ function _isFile(p: string): boolean {
337
+ try {
338
+ return fs.statSync(p).isFile();
339
+ } catch {
340
+ return false;
341
+ }
342
+ }
343
+
344
+ /** Python `type(x).__name__` for the JSON value shapes the messages emit. */
345
+ function _pyTypeName(value: unknown): string {
346
+ if (value === null || value === undefined) {
347
+ return 'NoneType';
348
+ }
349
+ if (Array.isArray(value)) {
350
+ return 'list';
351
+ }
352
+ switch (typeof value) {
353
+ case 'string':
354
+ return 'str';
355
+ case 'boolean':
356
+ return 'bool';
357
+ case 'number':
358
+ return Number.isInteger(value) ? 'int' : 'float';
359
+ case 'object':
360
+ return 'dict';
361
+ default:
362
+ return typeof value;
363
+ }
364
+ }
365
+
366
+ /** Python `repr(x)` for the scalar shapes interpolated via `{value!r}`. */
367
+ function _pyRepr(value: unknown): string {
368
+ if (value === null || value === undefined) {
369
+ return 'None';
370
+ }
371
+ if (value === true) {
372
+ return 'True';
373
+ }
374
+ if (value === false) {
375
+ return 'False';
376
+ }
377
+ if (typeof value === 'string') {
378
+ return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
379
+ }
380
+ return String(value);
381
+ }
382
+
383
+ /** Python `repr(list_of_str)` — `['a', 'b']`. */
384
+ function _pyReprList(values: string[]): string {
385
+ return '[' + values.map((v) => _pyRepr(v)).join(', ') + ']';
386
+ }
387
+
388
+ /** Python `sorted(list_of_str)` — code-point ascending. */
389
+ function _sortedStr(values: string[]): string[] {
390
+ return [...values].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
391
+ }
392
+
393
+ /**
394
+ * Approximate the text of Python's `OSError` for a failed read. Python
395
+ * formats `errno`-based messages like "[Errno 2] No such file or directory:
396
+ * 'path'". Node's error `.message` is shaped differently, so this is one of
397
+ * the few non-byte-identical surfaces — the error *channel* (exit 2 via
398
+ * `_CLIError`) and the *prefix* ("Cannot read <path>: ") stay identical;
399
+ * only the trailing OS detail differs. Tests normalise this trailing detail.
400
+ */
401
+ function _osErrorText(exc: unknown, p: string): string {
402
+ const e = exc as NodeJS.ErrnoException;
403
+ if (e && e.code === 'ENOENT') {
404
+ return `[Errno 2] No such file or directory: '${p}'`;
405
+ }
406
+ if (e && e.code === 'EISDIR') {
407
+ return `[Errno 21] Is a directory: '${p}'`;
408
+ }
409
+ if (e && e.code === 'EACCES') {
410
+ return `[Errno 13] Permission denied: '${p}'`;
411
+ }
412
+ return e && e.message ? e.message : String(exc);
413
+ }
@@ -0,0 +1,213 @@
1
+ # Surface Matrix — machine-checked per-tool canonical-surface declarations.
2
+ #
3
+ # One entry per tool in install.ts::USER_SCOPE_PATHS (set equality enforced by
4
+ # lint_surface_matrix). Consumed by:
5
+ # - `agent-config doctor` (surface-state check — flags duplicate surfaces)
6
+ # - `agent-config converge` (per-tool cleanup actions)
7
+ # - CI (src/scripts/lint_surface_matrix.ts — coverage + drift)
8
+ # Contract: docs/contracts/surface-matrix.md
9
+ #
10
+ # Fields per tool:
11
+ # surface: projection | plugin | bundles | export-only
12
+ # projection = installer writes content files into the user
13
+ # scope path; that projection is canonical
14
+ # plugin = the tool's own plugin/marketplace channel is
15
+ # canonical (no user-scope projection exists)
16
+ # bundles = manual import artifacts (ZIPs); no filesystem
17
+ # discovery convention
18
+ # export-only = a config/marker export; no content tree
19
+ # hooks: managed-settings-block | settings-hooks-opt-in | plugin | none
20
+ # alternates: legitimate secondary surfaces (NOT duplicates), with role
21
+ # duplicate: the duplicate-surface definition — `detect` paths that, when
22
+ # ALL present, mean the tool has a duplicate content surface.
23
+ # Only tools with a defined duplicate class are checked by
24
+ # doctor/converge. `pending-evidence` documents a suspected but
25
+ # unverified class (never acted on).
26
+ # converge: the consented cleanup action for the duplicate class
27
+ # notes: provenance — where the classification comes from
28
+
29
+ schema_version: 1
30
+
31
+ tools:
32
+ claude-code:
33
+ surface: projection
34
+ scope_path: "~/.claude/"
35
+ hooks: managed-settings-block
36
+ alternates:
37
+ - id: marketplace-plugin
38
+ role: bootstrap-shim
39
+ detail: >-
40
+ hooks/hooks.json (byte-identical to the managed settings block, so
41
+ Claude Code dedupes) plus one install-pointer skill; zero content
42
+ skills by construction (lint_marketplace enforces).
43
+ duplicate:
44
+ description: >-
45
+ A pre-shim marketplace plugin snapshot (content skills) installed next
46
+ to the ~/.claude/ file projection — every skill/command lists twice
47
+ and the snapshot rots silently.
48
+ detect:
49
+ all_of:
50
+ - "~/.claude/skills"
51
+ - "~/.claude/plugins/cache/event4u-agent-config"
52
+ converge:
53
+ action: plugin-uninstall
54
+ command: "claude plugin uninstall agent-config@event4u-agent-config"
55
+ reaps:
56
+ - "~/.claude/plugins/cache/event4u-agent-config"
57
+ notes: >-
58
+ Single-surface decision (agents/settings/contexts/
59
+ claude-code-single-surface-decision.md); shim invariant verified via
60
+ scratch marketplace install, 2026-07-08.
61
+
62
+ augment:
63
+ surface: projection
64
+ scope_path: "~/.augment/"
65
+ hooks: settings-hooks-opt-in
66
+ alternates:
67
+ - id: augment-plugin
68
+ role: dormant
69
+ detail: >-
70
+ .augment-plugin/ manifest-only wrapper (source: "."). Marketplace
71
+ feature is account-gated ("Plugin marketplace feature is not
72
+ enabled for your account") — dual install unreproducible today.
73
+ duplicate:
74
+ pending_evidence: >-
75
+ Suspected duplicate class analogous to Claude Code IF Augment enables
76
+ the plugin marketplace broadly. NOT acted on — evidence gate
77
+ agents/settings/contexts/augment-surface-parity.md (2026-07-08) could
78
+ not reproduce; re-run the gate before defining detect paths.
79
+ notes: >-
80
+ Projection-primary on evidence. NO retirement move without a fresh
81
+ parity gate (roadmap Phase 0 rule).
82
+
83
+ copilot:
84
+ surface: plugin
85
+ scope_path: "~/.copilot/"
86
+ hooks: plugin
87
+ notes: >-
88
+ Plugin-primary by design — no user-scope content projection exists for
89
+ Copilot; the project bridge is .github/plugin/marketplace.json. The
90
+ plugin is the correct surface here, not a duplicate (council Q3,
91
+ 2026-07-07). Do not "fix".
92
+
93
+ claude-desktop:
94
+ surface: bundles
95
+ scope_path: "~/Library/Application Support/Claude/"
96
+ hooks: none
97
+ notes: >-
98
+ Skill ZIPs imported manually via Customize → Skills; no filesystem
99
+ discovery, no plugin channel — no dual-surface class (Phase 0 survey).
100
+
101
+ cursor:
102
+ surface: projection
103
+ scope_path: "~/.cursor/"
104
+ hooks: none
105
+ notes: Plain projection (rules/commands/personas); no plugin channel.
106
+
107
+ windsurf:
108
+ surface: projection
109
+ scope_path: "~/.codeium/windsurf/"
110
+ hooks: none
111
+ notes: Plain projection (rules only); no plugin channel.
112
+
113
+ cline:
114
+ surface: projection
115
+ scope_path: "~/Documents/Cline/Rules/"
116
+ hooks: none
117
+ notes: Plain projection (rules only); no plugin channel.
118
+
119
+ gemini-cli:
120
+ surface: projection
121
+ scope_path: "~/.gemini/"
122
+ hooks: none
123
+ notes: Plain projection (skill bundle); no plugin channel (Phase 0 survey).
124
+
125
+ codex:
126
+ surface: projection
127
+ scope_path: "~/.codex/"
128
+ hooks: none
129
+ notes: Plain projection (skill bundle); no plugin channel (Phase 0 survey).
130
+
131
+ continue:
132
+ surface: projection
133
+ scope_path: "~/.continue/"
134
+ hooks: none
135
+ notes: Plain projection (skill bundle); no plugin channel (Phase 0 survey).
136
+
137
+ droid:
138
+ surface: projection
139
+ scope_path: "~/.factory/"
140
+ hooks: none
141
+ notes: Plain projection (skill bundle); no plugin channel (Phase 0 survey).
142
+
143
+ roocode:
144
+ surface: projection
145
+ scope_path: "~/.roo/"
146
+ hooks: none
147
+ notes: Plain projection (skill bundle); no plugin channel.
148
+
149
+ kilocode:
150
+ surface: projection
151
+ scope_path: "~/.kilocode/"
152
+ hooks: none
153
+ notes: Plain projection (skill bundle); no plugin channel.
154
+
155
+ kiro:
156
+ surface: projection
157
+ scope_path: "~/.kiro/"
158
+ hooks: none
159
+ notes: Plain projection (rules/steering/personas); no plugin channel.
160
+
161
+ qoder:
162
+ surface: projection
163
+ scope_path: "~/.qoder/"
164
+ hooks: none
165
+ notes: Plain projection (skill bundle); no plugin channel.
166
+
167
+ opencode:
168
+ surface: projection
169
+ scope_path: "~/.opencode/"
170
+ hooks: none
171
+ notes: Plain projection (skill bundle); no plugin channel.
172
+
173
+ trae:
174
+ surface: projection
175
+ scope_path: "~/.trae/"
176
+ hooks: none
177
+ notes: Plain projection (skill bundle); no plugin channel.
178
+
179
+ antigravity:
180
+ surface: projection
181
+ scope_path: "~/.agents/"
182
+ hooks: none
183
+ notes: Plain projection (skill bundle); no plugin channel.
184
+
185
+ codebuddy:
186
+ surface: projection
187
+ scope_path: "~/.codebuddy/"
188
+ hooks: none
189
+ notes: Plain projection (skill bundle); no plugin channel.
190
+
191
+ warp:
192
+ surface: projection
193
+ scope_path: "~/.warp/"
194
+ hooks: none
195
+ notes: Plain projection (skill bundle); no plugin channel.
196
+
197
+ aider:
198
+ surface: export-only
199
+ scope_path: "~/.aider.conf.yml"
200
+ hooks: none
201
+ notes: Config-file export; no content tree, no plugin channel (Phase 0 survey).
202
+
203
+ zed:
204
+ surface: export-only
205
+ scope_path: "~/.config/zed/"
206
+ hooks: none
207
+ notes: Export/marker only; no plugin channel (Phase 0 survey).
208
+
209
+ jetbrains:
210
+ surface: export-only
211
+ scope_path: "~/.config/JetBrains/"
212
+ hooks: none
213
+ notes: Export/marker only; no plugin channel (Phase 0 survey).