@event4u/agent-config 12.0.0 → 13.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 (148) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +273 -0
  3. package/README.md +1 -1
  4. package/agents/templates/.ai-council.yml.example +46 -3
  5. package/dist/agent-src/commands/cost/report.md +23 -0
  6. package/dist/agent-src/contexts/execution/subagent-routing.md +148 -33
  7. package/dist/agent-src/contexts/model-recommendations.md +6 -4
  8. package/dist/agent-src/contexts/subagent-configuration.md +69 -7
  9. package/dist/agent-src/rules/autonomous-execution.md +1 -1
  10. package/dist/agent-src/rules/design-fidelity.md +1 -1
  11. package/dist/agent-src/rules/design-review-after-ui-write.md +10 -5
  12. package/dist/agent-src/rules/evaluator-independence.md +27 -5
  13. package/dist/agent-src/rules/git-history-discipline.md +5 -1
  14. package/dist/agent-src/rules/low-impact-corpus-privacy-floor.md +1 -0
  15. package/dist/agent-src/rules/missing-skill-recovery.md +89 -0
  16. package/dist/agent-src/rules/ui-audit-gate.md +9 -4
  17. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
  18. package/dist/agent-src/scripts/roadmap_gates.ts +30 -2
  19. package/dist/agent-src/scripts/update_roadmap_progress.ts +47 -1
  20. package/dist/agent-src/skills/authz-review/SKILL.md +4 -0
  21. package/dist/agent-src/skills/design-system-capture/SKILL.md +4 -0
  22. package/dist/agent-src/skills/design-system-capture/references/snapshot-preference-order.md +77 -0
  23. package/dist/agent-src/skills/merge-conflicts/SKILL.md +4 -0
  24. package/dist/agent-src/skills/prompt-optimizer/SKILL.md +17 -1
  25. package/dist/agent-src/skills/roadmap-writing/SKILL.md +11 -11
  26. package/dist/agent-src/skills/systematic-debugging/SKILL.md +4 -0
  27. package/dist/agent-src/skills/threat-modeling/SKILL.md +3 -0
  28. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  29. package/dist/agent-src/templates/roadmaps.md +41 -3
  30. package/dist/cli/registry.js +1 -0
  31. package/dist/cli/registry.js.map +1 -1
  32. package/dist/cli-delegate/{chunk-NN4KW4HZ.js → chunk-3B72LSZL.js} +123 -2338
  33. package/dist/cli-delegate/{chunk-55WZN5VF.js → chunk-EVTY7ISR.js} +2 -2
  34. package/dist/cli-delegate/chunk-N7FEST6D.js +3895 -0
  35. package/dist/cli-delegate/{chunk-L565YFXH.js → chunk-XURJ4AQC.js} +566 -298
  36. package/dist/cli-delegate/cmd_conformance.js +4 -4
  37. package/dist/cli-delegate/cmd_doctor.js +2 -2
  38. package/dist/cli-delegate/cmd_export.js +2 -2
  39. package/dist/cli-delegate/cmd_handoff.js +1 -1
  40. package/dist/cli-delegate/cmd_prune.js +2 -2
  41. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  42. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  43. package/dist/cli-delegate/cmd_sync.js +2 -2
  44. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  45. package/dist/cli-delegate/cmd_validate.js +2 -2
  46. package/dist/discovery/deprecation-report.md +1 -1
  47. package/dist/discovery/discovery-manifest.json +44 -23
  48. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  49. package/dist/discovery/discovery-manifest.summary.md +3 -3
  50. package/dist/discovery/dormancy-report.md +1 -1
  51. package/dist/discovery/orphan-report.md +1 -1
  52. package/dist/discovery/packs.json +6 -5
  53. package/dist/discovery/trust-report.md +2 -2
  54. package/dist/discovery/workspaces.json +5 -4
  55. package/dist/hooks/dispatch.js +10625 -8541
  56. package/dist/install/install.mjs +754 -484
  57. package/dist/mcp/registry-manifest.json +3 -3
  58. package/dist/mcp/server.json +2 -2
  59. package/dist/mcp/server.mjs +1302 -167
  60. package/dist/router.json +1 -1
  61. package/dist/scripts/_lib/model_tier.js +9 -0
  62. package/dist/scripts/_lib/model_tier.js.map +1 -1
  63. package/dist/scripts/condense.js +50 -0
  64. package/dist/scripts/condense.js.map +1 -1
  65. package/dist/server/schemas/settings.js +2 -2
  66. package/dist/server/schemas/settings.js.map +1 -1
  67. package/docs/MIGRATION.md +20 -1
  68. package/docs/guidelines/design-fidelity-mechanics.md +21 -0
  69. package/docs/guidelines/design-handover-extraction.md +87 -0
  70. package/docs/guidelines/prompt-templates.md +24 -1
  71. package/package.json +1 -1
  72. package/src/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
  73. package/src/agent-src/scripts/roadmap_gates.ts +30 -2
  74. package/src/agent-src/scripts/update_roadmap_progress.ts +47 -1
  75. package/src/config/evaluator-budgets.json +9 -8
  76. package/src/config/gate-coverage.yml +63 -11
  77. package/src/config/gate-violation-baselines.json +12 -0
  78. package/src/config/hook-token-budget.json +15 -2
  79. package/src/config/preamble-payload-budget.json +28 -0
  80. package/src/scripts/_dispatch.bash +10 -0
  81. package/src/scripts/_lib/bench_ab_complexity.ts +439 -0
  82. package/src/scripts/_lib/bench_ab_safety_tier.ts +142 -0
  83. package/src/scripts/_lib/bench_ab_scoring_v2.ts +28 -3
  84. package/src/scripts/_lib/bench_ab_search_adherence.ts +423 -0
  85. package/src/scripts/_lib/bench_ab_size_claim.ts +206 -0
  86. package/src/scripts/_lib/bench_ab_workspace.ts +111 -0
  87. package/src/scripts/_lib/git_common_dir.ts +173 -0
  88. package/src/scripts/_lib/model_tier.ts +9 -0
  89. package/src/scripts/_lib/release_highlights.ts +108 -7
  90. package/src/scripts/_lib/skill_catalogue.ts +1115 -0
  91. package/src/scripts/_lib/tier_budget_routing.ts +31 -331
  92. package/src/scripts/ai_council/clients.ts +197 -18
  93. package/src/scripts/ai_council/config.ts +16 -1
  94. package/src/scripts/ai_council/low_impact.ts +14 -1
  95. package/src/scripts/ai_council/orchestrator.ts +14 -1
  96. package/src/scripts/ai_council/transport_resolver.ts +32 -2
  97. package/src/scripts/bench_ab_v2_complexity.ts +252 -0
  98. package/src/scripts/bench_ab_v2_run.ts +3 -42
  99. package/src/scripts/bench_ab_v2_safety.ts +296 -0
  100. package/src/scripts/bench_ab_v2_search.ts +343 -0
  101. package/src/scripts/bench_ab_v2_stats.ts +191 -0
  102. package/src/scripts/build_archive_index.ts +383 -0
  103. package/src/scripts/capture_skill_catalogue.ts +279 -317
  104. package/src/scripts/check_branch_freshness.ts +7 -1
  105. package/src/scripts/check_release_published.ts +6 -2
  106. package/src/scripts/check_release_trunk_sync.ts +18 -2
  107. package/src/scripts/check_source_size_budget.ts +428 -0
  108. package/src/scripts/check_suppression_hygiene.ts +5 -1
  109. package/src/scripts/condense.ts +50 -1
  110. package/src/scripts/cost/backfill_rates.mjs +298 -0
  111. package/src/scripts/cost/budget.mjs +12 -32
  112. package/src/scripts/cost_summary.ts +25 -0
  113. package/src/scripts/dispatch_r2_reviewer.ts +15 -5
  114. package/src/scripts/evidence_report.ts +15 -4
  115. package/src/scripts/external_sources_denylist.json +1 -0
  116. package/src/scripts/hook_manifest.yaml +75 -15
  117. package/src/scripts/hooks/concern_registry.ts +4 -0
  118. package/src/scripts/hooks/orchestration_record_hook.ts +21 -0
  119. package/src/scripts/hooks/skill_route_hook.ts +226 -0
  120. package/src/scripts/hooks/tool_result_bytes_hook.ts +219 -0
  121. package/src/scripts/hooks/ui_route_nudge_hook.ts +6 -3
  122. package/src/scripts/hot_context_hook.ts +13 -1
  123. package/src/scripts/install.ts +129 -0
  124. package/src/scripts/janitor.ts +77 -2
  125. package/src/scripts/lint_design_antipattern_parity.ts +40 -6
  126. package/src/scripts/lint_harvest_provenance.ts +35 -1
  127. package/src/scripts/lint_plan_risk_register.ts +22 -9
  128. package/src/scripts/lint_roadmap_blockers.ts +109 -6
  129. package/src/scripts/lint_rule_skill_pack_reach.ts +43 -0
  130. package/src/scripts/lint_scheduled_deprecations.ts +738 -0
  131. package/src/scripts/lint_skill_router_head.ts +21 -0
  132. package/src/scripts/mcp_server/consumer_tool_catalog.json +26 -3
  133. package/src/scripts/mcp_server/tool_catalog_source.ts +6 -24
  134. package/src/scripts/mcp_server/tools.ts +87 -0
  135. package/src/scripts/migration_status.ts +17 -1
  136. package/src/scripts/probe_review_binding_drift.ts +731 -0
  137. package/src/scripts/release.ts +100 -6
  138. package/src/scripts/routing_doctor.ts +50 -13
  139. package/src/scripts/schemas/command.schema.json +3 -3
  140. package/src/scripts/schemas/skill.schema.json +20 -2
  141. package/src/scripts/schemas/subagent.schema.json +2 -2
  142. package/src/scripts/session_eol_report.ts +81 -0
  143. package/src/scripts/trigger_coverage.ts +224 -3
  144. package/src/scripts/workspace_doctor.ts +464 -0
  145. package/src/scripts/worktree_cleanup_check.ts +2 -1
  146. package/src/server/schemas/settings.ts +2 -2
  147. package/dist/cli-delegate/chunk-AYKQGIBG.js +0 -1594
  148. package/src/config/budget-routing.json +0 -7
@@ -0,0 +1,464 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * workspace:doctor — read-only answer to "where am I", with provenance.
4
+ *
5
+ * Phase 3 of `road-to-inbox-harvest-2026-08-c-workspace-identity`. Two reads,
6
+ * one command, and **no action**: the roadmap's Non-goals put worktree deletion
7
+ * out of scope, and its Risk 3 names "the pressure read becomes a disposal path
8
+ * by accident" as the failure to design against. There is therefore no `--fix`,
9
+ * no `--prune`, and no removal plan here — `worktree_cleanup_check.ts` owns
10
+ * that question behind its own approval.
11
+ *
12
+ * 1. **Identity** — the five fields of `workspaceIdentity()` plus the session
13
+ * facts a location question actually needs: the roadmap claim, foreign live
14
+ * records, expired-but-unpruned records, and whether this checkout is
15
+ * contained in the main worktree. Every field prints its provenance, in the
16
+ * shape `routing:doctor` uses, because a value whose source is invisible is
17
+ * indistinguishable from a guess.
18
+ *
19
+ * 2. **Worktree pressure** — how large the estate is and how much of it is
20
+ * merged. The buckets **partition** the registered set, so they sum to the
21
+ * `git worktree list` total exactly. The live-session count is deliberately
22
+ * NOT one of them: it overlaps every bucket, and printing it inside the
23
+ * partition would invite a reader to add it in.
24
+ *
25
+ * Exit code is 0 whenever the command could read the repository. It is a
26
+ * report, not a gate: nothing here has a pass/fail opinion, so a non-zero exit
27
+ * would have to invent one.
28
+ *
29
+ * `--strict` exits 1 when **any** identity field is unresolved. Note what that
30
+ * means in a repo with no `refs/remotes/origin/HEAD` on disk: `prBase` is
31
+ * unresolved *correctly*, so `--strict` is red there by design. It is a probe
32
+ * for "is every field answerable here", not a health check — and this file
33
+ * previously claimed a per-field selection that does not exist.
34
+ */
35
+
36
+ import { execFileSync } from 'node:child_process';
37
+ import * as fs from 'node:fs';
38
+ import * as path from 'node:path';
39
+ import { fileURLToPath, pathToFileURL } from 'node:url';
40
+
41
+ import {
42
+ type IdentityField,
43
+ type WorkspaceIdentity,
44
+ workspaceIdentity,
45
+ } from './_lib/git_common_dir.js';
46
+ import { gitEnv } from './_lib/git_env.js';
47
+ import {
48
+ read_live_records,
49
+ register_dir,
50
+ type SessionRecord,
51
+ } from './_lib/session_register.js';
52
+ import { listWorktrees, resolveTrunk } from './worktree_cleanup_check.js';
53
+
54
+ const IDENTITY_FIELDS = ['repoRoot', 'mainWorktree', 'currentWorktree', 'branch', 'prBase'] as const;
55
+ type IdentityFieldName = (typeof IDENTITY_FIELDS)[number];
56
+
57
+ interface SessionView {
58
+ /** Roadmap slug this session claims, or `null`; `source` says where from. */
59
+ readonly claim: string | null;
60
+ readonly claim_source: string;
61
+ /** Live foreign records, i.e. every live record that is not this session's. */
62
+ readonly live_records: number;
63
+ /** Live records whose `worktree` is this checkout — a real collision. */
64
+ readonly conflicting: number;
65
+ /** Record files on disk that `read_live_records` judged expired. */
66
+ readonly stale: number;
67
+ /** Where the register lives, or why it could not be located. */
68
+ readonly register: string;
69
+ }
70
+
71
+ interface ContainmentView {
72
+ /** Is the current checkout inside the main worktree's directory tree? */
73
+ readonly contained: boolean | null;
74
+ readonly reason: string;
75
+ }
76
+
77
+ interface PressureView {
78
+ readonly trunk: string | null;
79
+ /** Where `trunk` came from — a candidate search, not a recorded default. */
80
+ readonly trunk_provenance: string;
81
+ readonly registered: number;
82
+ /** Same count from a DIFFERENT command; a mismatch is the real signal. */
83
+ readonly independent_registered: number | null;
84
+ /** Branch appears in `for-each-ref --merged <trunk>`. */
85
+ readonly merged: number;
86
+ /** Branch exists and is NOT merged into the trunk. */
87
+ readonly unmerged: number;
88
+ /** No branch to ask about (detached), or the merged probe was unavailable. */
89
+ readonly unclassifiable: number;
90
+ /** Overlaps the three buckets above on purpose; never summed with them. */
91
+ readonly with_live_session: number;
92
+ /** `merged + unmerged + unclassifiable`, printed so the sum is auditable. */
93
+ readonly partition_total: number;
94
+ readonly note: string | null;
95
+ }
96
+
97
+ export interface WorkspaceDoctorReport {
98
+ readonly identity: Record<IdentityFieldName, IdentityField>;
99
+ readonly session: SessionView;
100
+ readonly containment: ContainmentView;
101
+ readonly pressure: PressureView;
102
+ }
103
+
104
+ function _git(cwd: string, args: readonly string[]): string | null {
105
+ try {
106
+ return execFileSync('git', [...args], {
107
+ cwd,
108
+ encoding: 'utf-8',
109
+ stdio: ['ignore', 'pipe', 'ignore'],
110
+ timeout: 15_000,
111
+ // cwd decides, never an inherited GIT_DIR (hook environments).
112
+ env: gitEnv(),
113
+ }).trim();
114
+ } catch {
115
+ return null;
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Independent count of registered worktrees, parsed from the **plain**
121
+ * `git worktree list` rather than the porcelain form `listWorktrees` reads.
122
+ *
123
+ * This exists so the partition assertion below is falsifiable. Comparing
124
+ * `merged + unmerged + unclassifiable` against `rows.length` — where the
125
+ * buckets are incremented once per row — is a tautology that holds by
126
+ * construction and cannot detect a dropped entry. Two different commands with
127
+ * two different output formats can disagree; that disagreement is the signal.
128
+ */
129
+ export function independentRegisteredCount(repoPath: string): number | null {
130
+ const out = _git(repoPath, ['worktree', 'list']);
131
+ if (out === null) return null;
132
+ return out.split('\n').filter((l) => l.trim() !== '').length;
133
+ }
134
+
135
+ /**
136
+ * Short branch names merged into `trunk`, in **one** subprocess.
137
+ *
138
+ * Replaces one `merge-base --is-ancestor` spawn per registered worktree —
139
+ * measured at 307 spawns and 15.2 s wall for a read-only report. It also
140
+ * removes a silent misclassification: `_git` returns `null` on any failure
141
+ * (bad ref, timeout, git absent), and mapping that onto "not an ancestor"
142
+ * reported a failed probe as `unmerged`. Here a failed call is `null` and the
143
+ * caller sends every row to `unclassifiable` instead of guessing a side.
144
+ */
145
+ function mergedBranches(repoPath: string, trunk: string): Set<string> | null {
146
+ const out = _git(repoPath, ['for-each-ref', '--merged', trunk, '--format=%(refname:short)', 'refs/heads/']);
147
+ if (out === null) return null;
148
+ return new Set(out.split('\n').map((l) => l.trim()).filter((l) => l !== ''));
149
+ }
150
+
151
+ function realpathOrSelf(p: string): string {
152
+ try {
153
+ return fs.realpathSync(p);
154
+ } catch {
155
+ return p;
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Is `child` inside `parent`?
161
+ *
162
+ * Both sides are realpath-normalised first, and the comparison is
163
+ * separator-anchored: a plain `startsWith` would report `/a/repo-backup` as
164
+ * contained in `/a/repo`.
165
+ */
166
+ export function isContained(parent: string, child: string): boolean {
167
+ const p = realpathOrSelf(parent);
168
+ const c = realpathOrSelf(child);
169
+ if (p === c) return false; // the main worktree is not "inside" itself
170
+ return c.startsWith(p.endsWith(path.sep) ? p : p + path.sep);
171
+ }
172
+
173
+ function collectSession(identity: WorkspaceIdentity, sessionId: string | null): SessionView {
174
+ const main = identity.mainWorktree;
175
+ const here = identity.currentWorktree;
176
+ if (!main.resolved) {
177
+ return {
178
+ claim: null,
179
+ claim_source: 'unavailable — no main worktree to anchor the register on',
180
+ live_records: 0,
181
+ conflicting: 0,
182
+ stale: 0,
183
+ register: `unresolvable: ${main.reason}`,
184
+ };
185
+ }
186
+ const dir = register_dir(main.value);
187
+ if (dir === null) {
188
+ return {
189
+ claim: null,
190
+ claim_source: 'unavailable — register_dir declined to resolve',
191
+ live_records: 0,
192
+ conflicting: 0,
193
+ stale: 0,
194
+ register: 'unresolvable',
195
+ };
196
+ }
197
+
198
+ // `read_live_records` filters expired ones out; the difference against the
199
+ // files on disk is the stale count. Never prune here — this is a report.
200
+ const live = read_live_records(dir);
201
+ let onDisk = 0;
202
+ try {
203
+ onDisk = fs.readdirSync(dir).filter((n) => n.endsWith('.json')).length;
204
+ } catch {
205
+ onDisk = 0;
206
+ }
207
+
208
+ const mine = sessionId === null ? null : live.find((r) => r.session_id === sessionId) ?? null;
209
+ const foreign: SessionRecord[] = live.filter((r) => r.session_id !== sessionId);
210
+ const hereValue = here.resolved ? realpathOrSelf(here.value) : null;
211
+ const conflicting =
212
+ hereValue === null ? 0 : foreign.filter((r) => realpathOrSelf(r.worktree) === hereValue).length;
213
+
214
+ let claim: string | null = null;
215
+ let claimSource: string;
216
+ if (mine?.roadmap_slug != null && mine.roadmap_slug !== '') {
217
+ claim = mine.roadmap_slug;
218
+ claimSource = 'this session\'s register record';
219
+ } else if (sessionId === null) {
220
+ claimSource = 'no session id in the environment — the host exports none, or this is not an agent session';
221
+ } else {
222
+ claimSource = 'no claim in this session\'s record (none picked, or the heartbeat has not lifted it yet)';
223
+ }
224
+
225
+ return {
226
+ claim,
227
+ claim_source: claimSource,
228
+ live_records: foreign.length,
229
+ conflicting,
230
+ stale: Math.max(0, onDisk - live.length),
231
+ register: dir,
232
+ };
233
+ }
234
+
235
+ function collectContainment(identity: WorkspaceIdentity): ContainmentView {
236
+ const main = identity.mainWorktree;
237
+ const here = identity.currentWorktree;
238
+ if (!main.resolved) return { contained: null, reason: `main worktree unresolved: ${main.reason}` };
239
+ if (!here.resolved) return { contained: null, reason: `current worktree unresolved: ${here.reason}` };
240
+ if (realpathOrSelf(main.value) === realpathOrSelf(here.value)) {
241
+ // `null`, not `false`: the question does not apply, and rendering it as
242
+ // the word "outside" beside the reason "this IS the main worktree" was
243
+ // a self-contradicting line.
244
+ return { contained: null, reason: 'this IS the main worktree — containment does not apply' };
245
+ }
246
+ const contained = isContained(main.value, here.value);
247
+ return {
248
+ contained,
249
+ reason: contained
250
+ ? 'this checkout lives inside the main worktree'
251
+ : 'this checkout lives outside the main worktree — a sibling or unrelated directory',
252
+ };
253
+ }
254
+
255
+ function collectPressure(identity: WorkspaceIdentity, live: SessionRecord[]): PressureView {
256
+ const main = identity.mainWorktree;
257
+ if (!main.resolved) {
258
+ return {
259
+ trunk: null,
260
+ trunk_provenance: 'not attempted — no main worktree to resolve from',
261
+ registered: 0,
262
+ independent_registered: null,
263
+ merged: 0,
264
+ unmerged: 0,
265
+ unclassifiable: 0,
266
+ with_live_session: 0,
267
+ partition_total: 0,
268
+ note: `no main worktree to enumerate from: ${main.reason}`,
269
+ };
270
+ }
271
+ // Both helpers are the EXPORTED ones from worktree_cleanup_check. Re-deriving
272
+ // them here would have been a fourth `worktree list --porcelain` parser and a
273
+ // second `resolveTrunk` beside the two this roadmap's own census counts — the
274
+ // duplication the whole change exists to remove.
275
+ const rows = listWorktrees(main.value);
276
+ // `resolveTrunk` returns the literal 'HEAD' when no trunk candidate resolves.
277
+ const trunkRef = resolveTrunk(main.value);
278
+ const trunk = trunkRef === 'HEAD' ? null : trunkRef;
279
+ const merged_set = trunk === null ? null : mergedBranches(main.value, trunk);
280
+ const livePaths = new Set(live.map((r) => realpathOrSelf(r.worktree)));
281
+
282
+ let merged = 0;
283
+ let unmerged = 0;
284
+ let unclassifiable = 0;
285
+ let withLive = 0;
286
+ for (const row of rows) {
287
+ if (livePaths.has(realpathOrSelf(row.path))) withLive += 1;
288
+ if (row.branch === null || merged_set === null) {
289
+ unclassifiable += 1;
290
+ } else if (merged_set.has(row.branch)) {
291
+ merged += 1;
292
+ } else {
293
+ unmerged += 1;
294
+ }
295
+ }
296
+
297
+ const independent = independentRegisteredCount(main.value);
298
+ const notes: string[] = [];
299
+ if (trunk === null) {
300
+ notes.push(
301
+ 'no trunk ref resolved — every entry is unclassifiable rather than assumed merged',
302
+ );
303
+ } else if (merged_set === null) {
304
+ notes.push(
305
+ `the merged-branch probe against ${trunk} failed — every entry is unclassifiable rather than assumed unmerged`,
306
+ );
307
+ }
308
+ if (independent !== null && independent !== rows.length) {
309
+ notes.push(
310
+ `COUNT DISAGREEMENT: the porcelain parse sees ${rows.length}, the plain listing ${independent}`,
311
+ );
312
+ }
313
+
314
+ return {
315
+ trunk,
316
+ trunk_provenance:
317
+ trunk === null
318
+ ? 'unresolved — no candidate ref verified'
319
+ : 'candidate-ref search (first of origin/main, origin/master, main, master that resolves) — NOT a recorded remote default, which is what identity.prBase reports',
320
+ registered: rows.length,
321
+ independent_registered: independent,
322
+ merged,
323
+ unmerged,
324
+ unclassifiable,
325
+ with_live_session: withLive,
326
+ partition_total: merged + unmerged + unclassifiable,
327
+ note: notes.length === 0 ? null : notes.join(' · '),
328
+ };
329
+ }
330
+
331
+ export function collectReport(start: string, sessionId: string | null): WorkspaceDoctorReport {
332
+ const identity = workspaceIdentity(start);
333
+ const session = collectSession(identity, sessionId);
334
+ const main = identity.mainWorktree;
335
+ const dir = main.resolved ? register_dir(main.value) : null;
336
+ const live = dir === null ? [] : read_live_records(dir);
337
+ return {
338
+ identity: {
339
+ repoRoot: identity.repoRoot,
340
+ mainWorktree: identity.mainWorktree,
341
+ currentWorktree: identity.currentWorktree,
342
+ branch: identity.branch,
343
+ prBase: identity.prBase,
344
+ },
345
+ session,
346
+ containment: collectContainment(identity),
347
+ pressure: collectPressure(identity, live),
348
+ };
349
+ }
350
+
351
+ function renderField(name: string, f: IdentityField): string {
352
+ const marker = f.resolved ? '✅' : '⚠️';
353
+ const body = f.resolved ? `${f.value} [${f.provenance}]` : `UNRESOLVED — ${f.reason}`;
354
+ return ` ${marker} ${name.padEnd(16)} ${body}`;
355
+ }
356
+
357
+ export function render(report: WorkspaceDoctorReport): string {
358
+ const lines: string[] = [];
359
+ lines.push('workspace:doctor · read-only · no disposal path by design');
360
+ lines.push('');
361
+ lines.push('Identity (every field with its provenance):');
362
+ for (const name of IDENTITY_FIELDS) {
363
+ lines.push(renderField(name, report.identity[name]));
364
+ }
365
+ lines.push('');
366
+
367
+ const s = report.session;
368
+ lines.push('Session:');
369
+ lines.push(` claim: ${s.claim ?? 'none'} [${s.claim_source}]`);
370
+ lines.push(` register: ${s.register}`);
371
+ lines.push(
372
+ ` foreign live records: ${s.live_records} · conflicting on THIS checkout: ${s.conflicting} · expired-not-pruned: ${s.stale}`,
373
+ );
374
+ lines.push(
375
+ ' advisory, never a lock — two sessions can claim in the same millisecond, and an idle session leaves the register while its user returns',
376
+ );
377
+ lines.push('');
378
+
379
+ const c = report.containment;
380
+ lines.push(`Path containment: ${c.contained === null ? 'n/a' : c.contained ? 'inside' : 'outside'} — ${c.reason}`);
381
+ lines.push('');
382
+
383
+ const p = report.pressure;
384
+ lines.push(`Worktree pressure (trunk = ${p.trunk ?? 'unresolved'}):`);
385
+ lines.push(` trunk provenance: ${p.trunk_provenance}`);
386
+ lines.push(` registered: ${p.registered}`);
387
+ lines.push(` merged into trunk: ${p.merged}`);
388
+ lines.push(` carrying unmerged commits:${String(p.unmerged).padStart(4)}`);
389
+ lines.push(` unclassifiable: ${String(p.unclassifiable).padStart(4)} (detached, or no merged probe)`);
390
+ const independentOk = p.independent_registered === null || p.independent_registered === p.registered;
391
+ lines.push(
392
+ ` ── partition sums to ${p.partition_total} of ${p.registered}` +
393
+ `, cross-checked against an independent count of ${p.independent_registered ?? 'n/a'}` +
394
+ `${p.partition_total === p.registered && independentOk ? ' ✅' : ' ❌ MISMATCH'}`,
395
+ );
396
+ lines.push(
397
+ ` with a live session record: ${p.with_live_session} (overlaps the buckets above — not part of the partition)`,
398
+ );
399
+ if (p.note !== null) lines.push(` note: ${p.note}`);
400
+ lines.push('');
401
+ lines.push('Disposal is out of scope here (roadmap Non-goals; Risk 3). `worktree:cleanup` owns it, behind its own approval.');
402
+ return lines.join('\n');
403
+ }
404
+
405
+ export function main(argv: readonly string[] = process.argv.slice(2)): number {
406
+ let start = process.cwd();
407
+ let json = false;
408
+ let strict = false;
409
+ for (let i = 0; i < argv.length; i += 1) {
410
+ const a = argv[i];
411
+ if (a === '--from' && argv[i + 1] !== undefined) {
412
+ start = path.resolve(String(argv[(i += 1)]));
413
+ } else if (a === '--json') {
414
+ json = true;
415
+ } else if (a === '--strict') {
416
+ strict = true;
417
+ } else if (a === '--help' || a === '-h') {
418
+ process.stdout.write(
419
+ 'workspace:doctor [--from <dir>] [--json] [--strict]\n' +
420
+ ' Read-only workspace identity + worktree pressure report.\n' +
421
+ ' --strict exits 1 when any identity field is unresolved.\n',
422
+ );
423
+ return 0;
424
+ }
425
+ }
426
+
427
+ const sessionId = process.env['CLAUDE_CODE_SESSION_ID'] ?? process.env['AGENT_SESSION_ID'] ?? null;
428
+ const report = collectReport(start, sessionId);
429
+ process.stdout.write(json ? `${JSON.stringify(report, null, 2)}\n` : `${render(report)}\n`);
430
+
431
+ if (strict) {
432
+ const unresolved = IDENTITY_FIELDS.filter((n) => !report.identity[n].resolved);
433
+ if (unresolved.length > 0) {
434
+ process.stderr.write(`unresolved identity field(s): ${unresolved.join(', ')}\n`);
435
+ return 1;
436
+ }
437
+ }
438
+ return 0;
439
+ }
440
+
441
+ declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
442
+ function _isCliEntry(): boolean {
443
+ if (typeof __AGENT_CONFIG_BUNDLE__ !== 'undefined' && __AGENT_CONFIG_BUNDLE__) {
444
+ return false;
445
+ }
446
+ if (process.argv[1] === undefined) {
447
+ return false;
448
+ }
449
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
450
+ if (import.meta.url === argvUrl) {
451
+ return true;
452
+ }
453
+ try {
454
+ const here = fs.realpathSync(fileURLToPath(import.meta.url));
455
+ const argv1 = fs.realpathSync(path.resolve(process.argv[1]));
456
+ return here === argv1;
457
+ } catch {
458
+ return false;
459
+ }
460
+ }
461
+
462
+ if (_isCliEntry()) {
463
+ process.exit(main());
464
+ }
@@ -122,8 +122,9 @@ export function checkWorktree(worktreePath: string): CheckResult {
122
122
  return { allowed: reasons.length === 0, reasons };
123
123
  }
124
124
 
125
- interface WorktreeEntry {
125
+ export interface WorktreeEntry {
126
126
  path: string;
127
+ /** Short branch name (`refs/heads/` stripped), or `null` when detached. */
127
128
  branch: string | null;
128
129
  }
129
130
 
@@ -80,13 +80,13 @@ export const settingsSchema = z.object({
80
80
  ),
81
81
  per_tier: z.object({
82
82
  cheap: z.number().min(0).nullable().default(null).describe(
83
- 'USD ceiling for the cheap model tier under budget-aware delegation (docs/contracts/budget-routing.md). null = no separate tier cap; global ceilings still apply.',
83
+ 'USD ceiling for the cheap model tier. Budget-aware delegation was ARCHIVED 2026-08-16 (docs/contracts/budget-routing.md), so no code routes on this today; the cap is still summed and reported by `budget.mjs tier`. null = no separate tier cap; global ceilings still apply.',
84
84
  ),
85
85
  medium: z.number().min(0).nullable().default(null).describe(
86
86
  'USD ceiling for the medium model tier. null = no separate tier cap.',
87
87
  ),
88
88
  strong: z.number().min(0).nullable().default(null).describe(
89
- 'USD ceiling for the strong model tier. null = no separate tier cap — with cheap exhausted and strong funded, budget routing uses the strong tier rather than blocking work.',
89
+ 'USD ceiling for the strong model tier. null = no separate tier cap. The never-block-to-save-money relation this used to describe went with the archived budget-routing layer (docs/contracts/budget-routing.md); nothing routes between tiers today.',
90
90
  ),
91
91
  }).default({ cheap: null, medium: null, strong: null }),
92
92
  }),