@event4u/agent-config 4.9.0 → 5.0.0

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 (65) hide show
  1. package/.agent-src/commands/implement-ticket.md +5 -4
  2. package/.agent-src/rules/language-and-tone.md +4 -10
  3. package/.agent-src/skills/command-routing/SKILL.md +5 -4
  4. package/.claude-plugin/marketplace.json +1 -1
  5. package/CHANGELOG.md +73 -0
  6. package/CONTRIBUTING.md +19 -0
  7. package/README.md +11 -0
  8. package/dist/cli/registry.js +0 -2
  9. package/dist/cli/registry.js.map +1 -1
  10. package/dist/discovery/deprecation-report.md +1 -1
  11. package/dist/discovery/discovery-manifest.json +5 -5
  12. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  13. package/dist/discovery/discovery-manifest.summary.md +1 -1
  14. package/dist/discovery/orphan-report.md +1 -1
  15. package/dist/discovery/packs.json +2 -2
  16. package/dist/discovery/trust-report.md +1 -1
  17. package/dist/discovery/workspaces.json +2 -2
  18. package/dist/mcp/registry-manifest.json +2 -2
  19. package/dist/router.json +1 -1671
  20. package/docs/benchmark.md +20 -8
  21. package/docs/benchmarks.md +11 -0
  22. package/docs/contracts/benchmark-corpus-spec.md +31 -3
  23. package/docs/contracts/command-surface-tiers.md +1 -1
  24. package/docs/contracts/hook-architecture-v1.md +33 -0
  25. package/docs/contracts/migrate-command.md +197 -0
  26. package/docs/contracts/settings-api.md +2 -1
  27. package/docs/contracts/value-dashboard-spec.md +374 -0
  28. package/docs/contracts/value-report-schema.md +150 -0
  29. package/docs/decisions/ADR-031-validation-severity-tiers-and-projection-roundtrip.md +97 -0
  30. package/docs/decisions/INDEX.md +1 -0
  31. package/docs/guidelines/agent-infra/installed-tools-manifest.md +6 -3
  32. package/docs/guidelines/agent-infra/language-and-tone-examples.md +35 -0
  33. package/docs/migration/v1-to-v2.md +40 -27
  34. package/docs/value.md +84 -0
  35. package/package.json +8 -8
  36. package/scripts/__pycache__/validate_frontmatter.cpython-312.pyc +0 -0
  37. package/scripts/_cli/cmd_migrate.py +264 -102
  38. package/scripts/_cli/cmd_settings_migrate.py +2 -1
  39. package/scripts/_dispatch.bash +147 -49
  40. package/scripts/_lib/__pycache__/__init__.cpython-312.pyc +0 -0
  41. package/scripts/_lib/__pycache__/agent_src.cpython-312.pyc +0 -0
  42. package/scripts/_lib/install_regenerator.py +129 -0
  43. package/scripts/_lib/value_ladder.py +599 -0
  44. package/scripts/_lib/value_report.py +441 -0
  45. package/scripts/bench_rtk_savings.py +320 -0
  46. package/scripts/compile_router.py +19 -5
  47. package/scripts/expected_perms.json +1 -1
  48. package/scripts/first_run_gate_hook.py +178 -0
  49. package/scripts/hook_manifest.yaml +16 -7
  50. package/scripts/hooks/dispatch_hook.py +27 -0
  51. package/scripts/hooks/dispatch_issues.py +136 -0
  52. package/scripts/hooks_doctor.py +40 -1
  53. package/scripts/install.py +25 -21
  54. package/scripts/lint_agents_layout.py +5 -4
  55. package/scripts/lint_bench_corpus.py +86 -4
  56. package/scripts/lint_global_paths.py +4 -3
  57. package/scripts/lint_marketplace_install_completeness.py +188 -0
  58. package/scripts/lint_value_dashboard.py +218 -0
  59. package/scripts/render_benchmark_md.py +6 -2
  60. package/scripts/render_value_md.py +355 -0
  61. package/scripts/repro/repro_marketplace_install_gap.sh +161 -0
  62. package/scripts/roadmap_progress_hook.py +23 -0
  63. package/scripts/router_telemetry.py +470 -0
  64. package/scripts/validate_frontmatter.py +23 -9
  65. package/scripts/_cli/cmd_migrate_to_global.py +0 -415
package/docs/benchmark.md CHANGED
@@ -4,12 +4,14 @@
4
4
 
5
5
  ## Headline
6
6
 
7
+ > **Track A confirms surface availability** — a precondition, not an impact metric. For the impact view (cost-ladder + behaviour with vs. without), see [`docs/value.md`](value.md).
8
+
7
9
  | Metric | with | without | delta |
8
10
  |---|---|---|---|
9
- | Track A trigger-accuracy | 100.0% | 0.0% | 100.0% |
10
- | Track B completion-rate | | | 0.0% |
11
- | Track B mean wall-time | —s | —s | 0.00s |
12
- | Track B ask-vs-act ratio | | | — |
11
+ | Track A surface-availability | 100.0% | 0.0% | 100.0% _(structural — files present)_ |
12
+ | Track B completion-rate | 0.0% | 0.0% | 0.0% |
13
+ | Track B mean wall-time | 0.00s | 0.00s | 0.00s |
14
+ | Track B ask-vs-act ratio | 0.000 | 0.000 | — |
13
15
 
14
16
  ## Track A — Behavioural eval
15
17
 
@@ -33,9 +35,19 @@ Per-target presence (sample):
33
35
 
34
36
  ## Track B — Task completion
35
37
 
36
- - Mode: `—`
38
+ - Mode: `dry-run`
39
+ - with → **0.0%** (0/13)
40
+ - without → **0.0%** (0/13)
41
+
42
+ Per-category:
37
43
 
38
- _No Track B reports yet. Run `task bench:ab:track-b`._
44
+ | Category | with | without | delta |
45
+ |---|---|---|---|
46
+ | bugfix | 0.0% | 0.0% | 0.0% |
47
+ | feature | 0.0% | 0.0% | 0.0% |
48
+ | refactor | 0.0% | 0.0% | 0.0% |
49
+ | testadd | 0.0% | 0.0% | 0.0% |
50
+ | uiaudit | 0.0% | 0.0% | 0.0% |
39
51
 
40
52
  ## Methodology
41
53
 
@@ -50,7 +62,7 @@ Cache key for the latest run:
50
62
  - `claude_cli_version`: `2.1.150 (Claude Code)`
51
63
  - `target_shape_hash`: `3f2f67cebfbb5fff`
52
64
 
53
- - **Last rendered:** `2026-05-26T15:33:46+00:00`
65
+ - **Last rendered:** `2026-05-28T13:55:30+00:00`
54
66
 
55
67
  ## History
56
68
 
@@ -62,4 +74,4 @@ Last 5 runs (per corpus):
62
74
 
63
75
  ### `ab-trackb`
64
76
 
65
- _no runs yet_
77
+ - `2026-05-28T13-47-41Z` → 0.0%
@@ -19,6 +19,8 @@ discipline (upstream `5b71c7a`).
19
19
  |---|---|---|
20
20
  | `dev` | `tests/eval/corpus-dev.yaml` | router / engine selection |
21
21
  | `telegraph` | `internal/bench/corpora/telegraph/prompts.yaml` | condensation dialect (`vs_raw` + `vs_terse`) |
22
+ | `rtk` | `internal/bench/corpora/rtk/commands.yaml` | rtk CLI-output filtering savings (Phase 2 of `road-to-readable-value-dashboard.md`) |
23
+ | `value` | _derived_ | aggregated dashboard — no own corpus, reads from the others |
22
24
 
23
25
  ## Reports — naming and trail
24
26
 
@@ -39,11 +41,20 @@ one without the other.
39
41
  | Edit to `scripts/bench_run.py` `--telegraph` arm | `telegraph` | report refreshed in same PR |
40
42
  | Edit to `internal/bench/corpora/telegraph/prompts.yaml` | `telegraph` | report refreshed, version bumped (`telegraph-vN+1`) |
41
43
  | Edit to `scripts/_lib/bench_telegraph*.py` | `telegraph` | report refreshed in same PR |
44
+ | Edit to any rung source (frugality / telegraph / rtk / A/B) | `value` | `task value` re-renders `docs/value.md` in same PR |
45
+ | Edit to `internal/bench/corpora/rtk/commands.yaml` | `rtk` | `scripts/bench_rtk_savings.py` refreshed in same PR |
46
+ | Edit to `dist/router.json` or any rule frontmatter `triggers:` | `router-telemetry` | `task value:telemetry:replay` refreshes attribution map; required before any Phase 4 / Phase 5 cut |
42
47
 
43
48
  A PR that touches any of the cadence triggers without refreshing the
44
49
  corresponding report is rejected by reviewer convention (no CI gate yet
45
50
  — the trigger surface is too small to warrant one).
46
51
 
52
+ ## Cost envelope (`rtk` corpus)
53
+
54
+ 8 commands × 2 arms (raw vs. rtk-filtered) = 16 local shell invocations
55
+ per run. Zero API spend — pure local measurement. Wall-time ≈ 5–10 s on
56
+ the maintainer's repo (`scripts/bench_rtk_savings.py --quiet`).
57
+
47
58
  ## Cost envelope (`telegraph` corpus)
48
59
 
49
60
  10 prompts × 3 arms (`condensed` · `terse-control` · `uncondensed`) = 30
@@ -45,12 +45,35 @@ corpus_id: <id> # short kebab-case identifier
45
45
  selection_accuracy_target: 0.60 # 0.0–1.0; runner exits non-zero below
46
46
  prompts:
47
47
  - id: <bucket>-<NN> # e.g. canonical-01, ambiguous-03
48
- category: <bucket> # canonical | ambiguous | destructive | long-context
48
+ category: <bucket> # canonical | ambiguous | destructive | long-context | router-coverage
49
49
  user_type_candidates: [<slug>, ...] # optional; informational
50
50
  language: en # en | de — per language-and-tone
51
51
  prompt: "<text>" # the agent-facing prompt
52
52
  expected_skills: [<slug>, ...] # ≥ 1 entry; non-empty
53
53
  expected_carve_outs: [<slug>, ...] # required when category == destructive
54
+ intended_triggers: [<rule_id>, ...] # router-telemetry attribution — rule ids the
55
+ # corpus author expects to activate AND that the
56
+ # deterministic replay can verify (keyword / phrase
57
+ # / command / path with supplied open_files or
58
+ # command context). Replay checks intended vs
59
+ # observed; drift surfaces as a finding, not
60
+ # silently. OPTIONAL on non-router-coverage corpora.
61
+ # (Council R3 honesty floor — pass-3 onwards.)
62
+ replay_opaque_triggers: [<rule_id>, ...] # rule ids the author expects to fire at
63
+ # RUNTIME but only via an `intent` trigger (or a
64
+ # router coverage gap) the static replay cannot
65
+ # see. Reported separately by the telemetry — NOT
66
+ # counted as missed_intended (would be false drift)
67
+ # nor as unintended_activations. A rule may sit in
68
+ # at most one bucket. router-coverage tasks need at
69
+ # least one of {intended_triggers,
70
+ # replay_opaque_triggers} non-empty.
71
+ open_files: [<path>, ...] # optional — paths the agent has "open" when
72
+ # the prompt fires. Used to drive `path_prefix`
73
+ # and `file_pattern` triggers in router replay.
74
+ command: "<slash-command>" # optional — exact command invoked (e.g.
75
+ # `/roadmap:process-step`). Drives `command:`
76
+ # triggers in router replay.
54
77
  rubric: # optional structural assertion
55
78
  must_include: ["<phrase>", ...] # all phrases must appear in output
56
79
  must_not_include: ["<phrase>", ...]
@@ -67,12 +90,17 @@ prompts:
67
90
  | `selection_accuracy_target` outside `[0.0, 1.0]` | `target_out_of_range` | `1.5` |
68
91
  | Duplicate `id` across prompts | `duplicate_id` | two `canonical-01` |
69
92
  | `id` does not match `^[a-z][a-z0-9-]*-\d{2}$` | `bad_id_format` | `Canonical_1` |
70
- | `category` not in `{canonical, ambiguous, destructive, long-context}` | `bad_category` | `category: misc` |
93
+ | `category` not in `{canonical, ambiguous, destructive, long-context, router-coverage}` | `bad_category` | `category: misc` |
71
94
  | `language` not in `{en, de}` | `bad_language` | `language: fr` |
72
- | `expected_skills` empty / missing | `empty_expected` | `expected_skills: []` |
95
+ | `expected_skills` empty / missing (**except `category == router-coverage`**, which tests trigger activation, not skill selection) | `empty_expected` | `expected_skills: []` |
73
96
  | `expected_skills` references an unknown skill slug | `unknown_skill` | `expected_skills: [imaginary]` |
74
97
  | `category == destructive` without `expected_carve_outs` | `missing_carve_out` | — |
75
98
  | Prompt text empty / whitespace-only | `empty_prompt` | — |
99
+ | `intended_triggers` / `replay_opaque_triggers` references a rule that doesn't exist in `dist/router.json` | `unknown_intended_trigger` | `intended_triggers: [no-such-rule]` |
100
+ | `intended_triggers` present but not a list | `bad_intended_triggers_shape` | `intended_triggers: foo` |
101
+ | `replay_opaque_triggers` present but not a list | `bad_replay_opaque_triggers_shape` | `replay_opaque_triggers: foo` |
102
+ | Same rule id in both `intended_triggers` and `replay_opaque_triggers` | `trigger_in_both_buckets` | `intended:[x]` + `opaque:[x]` |
103
+ | `category == router-coverage` with **both** `intended_triggers` and `replay_opaque_triggers` empty/absent | `missing_intended_triggers` | — |
76
104
 
77
105
  The linter MUST run with `--quiet` honour per the script-output
78
106
  convention and emit one violation per line in non-quiet mode.
@@ -105,7 +105,7 @@ unless `--tier=all`. Reachable by full name; not advertised.
105
105
  `context-hygiene:hook`, `hooks:install`, `hooks:status`).
106
106
  2. **Internal / programmatic.** Called by other scripts or by the
107
107
  work-engine, never typed by a human (`memory:*`,
108
- `proposal:check`, `refine-ticket:detect`, `migrate-state`,
108
+ `proposal:check`, `refine-ticket:detect`,
109
109
  `telemetry:*`, `mcp:render`, `mcp:check`, `mcp:setup`,
110
110
  `mcp:run`, `roadmap:progress-check`).
111
111
  3. **Sub-command of a slash orchestrator** — the orchestrator is
@@ -250,6 +250,39 @@ the flag are listed by `./agent-config hooks:doctor` as not
250
250
  replay-safe; replay tests assert no `agents/runtime/state/` mutation
251
251
  post-invocation.
252
252
 
253
+ ## Regenerator location — canonical path (Phase 3 of `road-to-hooks-actually-fire-in-consumers`)
254
+
255
+ The `roadmap-progress` concern's resolver searches three locations
256
+ for `update_roadmap_progress.py`. The **canonical consumer-side
257
+ location is**:
258
+
259
+ ```
260
+ <consumer_root>/.augment/scripts/update_roadmap_progress.py
261
+ ```
262
+
263
+ Rationale:
264
+
265
+ - The auto-generated `agents/roadmaps-progress.md` already cites
266
+ `.augment/scripts/update_roadmap_progress.py` in its header.
267
+ - `install.py`'s existing tool projection lays down `.augment/`
268
+ unconditionally; piggy-backing on that directory means consumers
269
+ do not need a separate "scripts" install step.
270
+ - The other two paths (`.agent-src/scripts/`,
271
+ `.agent-src.uncondensed/scripts/`) only populate in
272
+ source-checkouts of the package itself.
273
+
274
+ Source-of-truth in the package: `packages/core/.agent-src.uncondensed/scripts/update_roadmap_progress.py`.
275
+ Helper that copies source → consumer canonical:
276
+ `scripts/_lib/install_regenerator.py`. Consumed by `install.py` and
277
+ `hooks:install --regen`.
278
+
279
+ The resolver in `scripts/roadmap_progress_hook.py::_resolve_regenerator`
280
+ visits the canonical path FIRST; the other two are fallback for
281
+ maintainer / dev workflows. On `return None` the resolver writes a
282
+ `dispatch-issues.jsonl` entry (Phase 1 contract) with
283
+ `prerequisite_missing` so the user can discover the gap via
284
+ `./agent-config hooks:doctor`.
285
+
253
286
  ## Stability
254
287
 
255
288
  Beta. Breaking changes between v1 and v2 are allowed in a minor
@@ -0,0 +1,197 @@
1
+ # `agent-config migrate` — Behavior Contract
2
+
3
+ > **Status:** active · **Owner:** maintainer (`scripts/_cli/cmd_migrate.py`) · **Opened:** 2026-05-29
4
+ >
5
+ > Source: `road-to-one-migrate-command.md` Phase 1. Locks the union of
6
+ > cleanup actions performed by the unified `./agent-config migrate`
7
+ > command and codifies the **deletion-over-migration** policy: the
8
+ > wizard recreates fresh project config, so legacy project-local state
9
+ > is hard-deleted rather than preserved or relocated.
10
+
11
+ ## Design intent
12
+
13
+ One opinionated command runs every cleanup step end-to-end. No flag
14
+ matrix to pick between behaviors, no surprises. The single flag is
15
+ `--dry-run` (preview vs. apply). The legacy three-command surface —
16
+ `migrate`, `migrate-state`, `migrate-to-global` — is collapsed into
17
+ one entry point.
18
+
19
+ Rationale:
20
+
21
+ - **One mental model.** The legacy split forced the user to remember
22
+ which slice each command performed. A single opinionated command
23
+ removes that cognitive load.
24
+ - **Deletion over preservation.** The new wizard
25
+ (`agent-config setup`) recreates fresh project / global config on
26
+ next run. Preserving stale `.agent-settings.yml` only carries
27
+ forward old values the user already chose to leave behind.
28
+ - **No setup-migration.** Project-local config is deleted; global
29
+ config is recreated fresh by the wizard. Different paths into the
30
+ same end state, but the deletion path requires no decisions from
31
+ the agent at migrate time.
32
+
33
+ ## Input signals — what counts as "needs migration"
34
+
35
+ The command considers a consumer repo migratable when **any** of
36
+ these are detected. The full set is the disjunction below; any one
37
+ hit triggers the apply path.
38
+
39
+ | # | Signal | Source |
40
+ |---|---|---|
41
+ | 1 | `@event4u/agent-config` entry in `package.json` `dependencies` or `devDependencies` | npm install era |
42
+ | 2 | `event4u/agent-config` entry in `composer.json` `require` or `require-dev` | Composer install era |
43
+ | 3 | Managed symlink (`.augment`, `.claude`, `.cursor`, `.clinerules`, `.windsurfrules`) pointing into `vendor/` or `node_modules/` | composer / npm install layout |
44
+ | 4 | `.implement-ticket-state.json` file present at project root (v0 work-engine state) | pre-v1 engine schema |
45
+ | 5 | `.agent-settings.yml` at project root (legacy project-local config) | pre-global-only consumer surface |
46
+ | 6 | `.agent-user.yml` at project root (legacy project-local user prefs) | pre-global-only consumer surface |
47
+ | 7 | `settings/.agent-settings.yml` or `settings/.agent-user.yml` (typed-subdir variant) | pre-global-only typed-subdir layout |
48
+ | 8 | Empty `agent-config/` shell directory at project root | leftover from removed `composer/npm` install |
49
+
50
+ The detector returns "already migrated" (exit 0, no writes) when
51
+ none of the signals fire.
52
+
53
+ ## Output state — what the consumer looks like post-migration
54
+
55
+ After `./agent-config migrate` (real apply) returns 0, the consumer
56
+ repo carries **none** of these:
57
+
58
+ - ❌ `@event4u/agent-config` in `package.json` (`dependencies` or
59
+ `devDependencies`); the section is removed if it becomes empty.
60
+ - ❌ `event4u/agent-config` in `composer.json` (`require` or
61
+ `require-dev`); the section is removed if it becomes empty.
62
+ - ❌ Managed symlinks pointing into `vendor/` or `node_modules/`.
63
+ - ❌ `.implement-ticket-state.json` at the project root; if v0
64
+ payload was present, it is migrated to `.work-state.json` and the
65
+ v0 source is renamed `.implement-ticket-state.json.bak`. If no v0
66
+ payload existed (file absent), nothing is written.
67
+ - ❌ Project-root `.agent-settings.yml` (hard-deleted).
68
+ - ❌ Project-root `.agent-user.yml` (hard-deleted).
69
+ - ❌ `settings/.agent-settings.yml` and `settings/.agent-user.yml`
70
+ (hard-deleted; the `settings/` directory is removed if it becomes
71
+ empty).
72
+ - ❌ Empty `agent-config/` shell at the project root.
73
+
74
+ The `.gitignore` block is refreshed to the canonical shape
75
+ documented in `scripts/_cli/cmd_migrate.py::GITIGNORE_NEW_BODY`.
76
+
77
+ ## Action order — opinionated, fixed
78
+
79
+ Apply path is deterministic — re-running the same input yields the
80
+ same diff. Order is foundation-first so that earlier steps cannot
81
+ break detection for later ones:
82
+
83
+ 1. **Detect** legacy signals from the matrix above; collect every
84
+ action that would fire.
85
+ 2. **Strip** `composer.json` / `package.json` package entries
86
+ in-place (preserves sibling keys + 2-space indent + trailing
87
+ newline).
88
+ 3. **Purge** managed symlinks whose target points into `vendor/` or
89
+ `node_modules/`. User-managed symlinks pointing elsewhere are
90
+ preserved with a warning.
91
+ 4. **Migrate state** — if `.implement-ticket-state.json` carries a
92
+ v0 payload, rewrite to `.work-state.json` and rename the v0
93
+ source to `.implement-ticket-state.json.bak`. If the file is
94
+ absent or already v1-shaped, skip.
95
+ 5. **Hard-delete** legacy project-local config files:
96
+ - `.agent-settings.yml` (project root)
97
+ - `.agent-user.yml` (project root)
98
+ - `settings/.agent-settings.yml` (typed-subdir variant)
99
+ - `settings/.agent-user.yml` (typed-subdir variant)
100
+ - `settings/` directory itself if empty after the YAML removals.
101
+ 6. **Remove** the empty `agent-config/` shell directory at the
102
+ project root, if present and empty.
103
+ 7. **Refresh** the `.gitignore` agent-config managed block to the
104
+ canonical shape.
105
+ 8. **Summarize** — print every action taken, one per line, with a
106
+ leading bullet.
107
+
108
+ ## `--dry-run` semantics
109
+
110
+ - Same detection + summary as the apply path.
111
+ - **Zero filesystem mutations** — no file created, modified, or
112
+ deleted; no symlink removed; no directory removed.
113
+ - Exit codes match the apply path: `0` for "nothing to migrate"
114
+ and `0` for "would migrate these N actions".
115
+ - Non-zero only on detection errors (unreadable file, invalid JSON
116
+ in `composer.json` / `package.json`, etc.).
117
+ - The summary is prefixed with `would` instead of past-tense verbs
118
+ so log scraping can distinguish dry-run from real runs.
119
+
120
+ ## Idempotency contract
121
+
122
+ Re-running on a fully-migrated consumer:
123
+
124
+ 1. Detector fires zero hits (every signal in the matrix is absent).
125
+ 2. Command prints `✅ already migrated — nothing to do.`
126
+ 3. Exits `0`.
127
+ 4. **No filesystem mutation occurs** — including on `--dry-run`.
128
+
129
+ A partial migration (e.g., a previous run crashed between steps 3
130
+ and 4) re-runs each remaining step on the next invocation. The
131
+ apply order is chosen so partial state never poisons detection of
132
+ the next pending action.
133
+
134
+ ## Excluded — what `migrate` does NOT do
135
+
136
+ These cleanup actions exist elsewhere in the package today but are
137
+ intentionally **outside** the unified `migrate` command:
138
+
139
+ | Action | Where it lives instead | Why excluded |
140
+ |---|---|---|
141
+ | Lift project-local YAML into `~/.event4u/agent-config/` | wizard (`agent-config setup`) | New global config is created fresh by the wizard; preserving stale values defeats the deletion-over-migration policy. |
142
+ | Write a fresh `.agent-settings.yml` | wizard (`agent-config setup`) | Same as above — the wizard is the source of truth for new project config. |
143
+ | Run the perms gate (`lint_global_paths.py`) | `agent-config doctor` | Migration deletes project-local state; the perms audit is a separate diagnostic on the global tree. |
144
+ | `.legacy-pre-global-only/<stamp>/` snapshot | (removed) | Snapshot-and-rollback was a `migrate-to-global` semantic. The deletion path needs no snapshot — git history is the rollback surface. |
145
+ | `agents/.event4u-bridge.yml` bridge marker write | `install.py` | Bridge marker is an install-time artefact, not a migration concern. |
146
+ | `settings:migrate` (read-only copy of project YAML into global) | (removed; superseded by wizard) | The read-only copy was a stepping stone for the destructive move. With the deletion policy, neither step survives. |
147
+
148
+ ## Exit codes
149
+
150
+ | Code | Meaning |
151
+ |---|---|
152
+ | `0` | Migration complete, or nothing to migrate (already migrated), or `--dry-run` plan computed. |
153
+ | `1` | Detection error — unreadable file, invalid JSON in `composer.json` / `package.json`, work-engine v0 → v1 conversion error. |
154
+ | `2` | Unused (reserved). |
155
+
156
+ The apply path never exits non-zero on a partial-migration recovery
157
+ — a step that fires its predicate is allowed to complete cleanly
158
+ even if a sibling step's predicate is already satisfied.
159
+
160
+ ## Test surface
161
+
162
+ `tests/migrate/test_unified_migrate.py` covers, against a fixture
163
+ consumer dir under `tests/fixtures/migrate/`:
164
+
165
+ - **Full apply** — fixture carries every input signal; assert each
166
+ output-state predicate holds post-run; summary lists each action.
167
+ - **`--dry-run`** — same fixture, assert zero filesystem mutations
168
+ (snapshot dir tree before and after; bit-identical).
169
+ - **Idempotency** — run twice; assert second run is the
170
+ "already migrated" no-op.
171
+
172
+ ## Rollback
173
+
174
+ Restoring the previous command surface:
175
+
176
+ 1. Restore the previous `scripts/_cli/cmd_migrate.py` from git
177
+ history.
178
+ 2. Restore `scripts/_cli/cmd_migrate_to_global.py`,
179
+ `cmd_migrate_state()` / `cmd_migrate_to_global()` in
180
+ `scripts/_dispatch.bash`, and the corresponding registry entries
181
+ in `src/cli/registry.ts`.
182
+ 3. Delete this contract doc.
183
+ 4. Restore `scripts/_cli/cmd_settings_migrate.py` if also removed
184
+ (note: this command was originally cited only as a discussion
185
+ item in Phase 1 cross-check; see "Excluded" table above).
186
+
187
+ ## See also
188
+
189
+ - `road-to-one-migrate-command.md` — the roadmap this contract
190
+ realizes.
191
+ - `road-to-global-only-install.md` — the predecessor roadmap that
192
+ shipped `migrate-to-global`; superseded by this contract.
193
+ - `road-to-portable-runtime-and-update-check.md` § P3.5–P3.6 — the
194
+ original `migrate` command (composer / npm cleanup).
195
+ - `scripts/_cli/cmd_migrate.py` — implementation.
196
+ - `.agent-src/templates/scripts/work_engine/migration/v0_to_v1.py`
197
+ — state-file migration helper invoked from step 4.
@@ -175,7 +175,8 @@ project-local `.agent-user.yml` / `.agent-settings.yml` into
175
175
  `~/.event4u/agent-config/`. Idempotent — refuses to overwrite a
176
176
  non-empty global file without `--force`. Order matches Phase 5
177
177
  amendment A2 (`copy → verify`; the destructive `move` step is owned by
178
- `migrate-to-global`, not this subcommand).
178
+ the unified `agent-config migrate` command, not this subcommand — see
179
+ `docs/contracts/migrate-command.md`).
179
180
 
180
181
  Flags:
181
182