@event4u/agent-config 9.28.0 → 9.29.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 (147) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/AGENTS.md +6 -6
  3. package/CHANGELOG.md +66 -0
  4. package/README.md +1 -1
  5. package/agents/templates/.ai-council.yml.example +55 -24
  6. package/dist/agent-src/commands/cost/report.md +2 -2
  7. package/dist/agent-src/commands/team/adversarial.md +8 -7
  8. package/dist/agent-src/commands/team/delegate.md +13 -12
  9. package/dist/agent-src/commands/team/review.md +9 -8
  10. package/dist/agent-src/commands/team/status.md +9 -8
  11. package/dist/agent-src/commands/team.md +29 -19
  12. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +88 -2
  13. package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +29 -21
  14. package/dist/agent-src/contexts/execution/host-capability-manifest.md +26 -15
  15. package/dist/agent-src/contexts/execution/orchestration-telemetry.md +18 -0
  16. package/dist/agent-src/contexts/execution/subagent-response-contract.md +54 -0
  17. package/dist/agent-src/contexts/execution/subagent-routing.md +13 -11
  18. package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +25 -0
  19. package/dist/agent-src/contexts/execution/subagent-steering.md +6 -3
  20. package/dist/agent-src/rules/delegation-policy.md +11 -7
  21. package/dist/agent-src/rules/settings-ask-protocol.md +8 -7
  22. package/dist/agent-src/rules/token-budget-discipline.md +1 -1
  23. package/dist/agent-src/skills/forensics-report/SKILL.md +114 -0
  24. package/dist/agent-src/skills/forensics-report/evals/triggers.json +17 -0
  25. package/dist/agent-src/skills/premortem/SKILL.md +30 -1
  26. package/dist/agent-src/skills/reasoning-orchestrator/SKILL.md +2 -1
  27. package/dist/agent-src/skills/subagent-orchestration/SKILL.md +7 -5
  28. package/dist/agent-src/skills/subagent-orchestration/schemas/subagent-status.json +61 -3
  29. package/dist/agent-src/skills/test-case-discovery/SKILL.md +1 -1
  30. package/dist/agent-src/templates/AGENTS.md +4 -4
  31. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  32. package/dist/agent-src/templates/roadmaps.md +23 -0
  33. package/dist/cli-delegate/{chunk-22GCIJ2O.js → chunk-66OVWGB5.js} +1 -1
  34. package/dist/cli-delegate/{chunk-GB2FSUEE.js → chunk-LCFJIQ4J.js} +32 -0
  35. package/dist/cli-delegate/{chunk-VSYT7OHO.js → chunk-LVRYBKTI.js} +31 -2
  36. package/dist/cli-delegate/{chunk-DVRS2KVR.js → chunk-MMXA5GY7.js} +10 -17
  37. package/dist/cli-delegate/{chunk-JXARCGVN.js → chunk-MP3JT3IU.js} +1 -1
  38. package/dist/cli-delegate/{chunk-PQSAQKHW.js → chunk-NZ5N2XQL.js} +3 -3
  39. package/dist/cli-delegate/{chunk-7OMQCH4V.js → chunk-OJM3MBJM.js} +1 -1
  40. package/dist/cli-delegate/{chunk-2BE5I2MN.js → chunk-RCIA67XN.js} +111 -90
  41. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  42. package/dist/cli-delegate/cmd_brand_status.js +1 -1
  43. package/dist/cli-delegate/cmd_conformance.js +6 -43
  44. package/dist/cli-delegate/cmd_doctor.js +9 -5
  45. package/dist/cli-delegate/cmd_explain.js +2 -2
  46. package/dist/cli-delegate/cmd_export.js +4 -4
  47. package/dist/cli-delegate/cmd_handoff.js +3 -3
  48. package/dist/cli-delegate/cmd_mcp_available.js +1 -1
  49. package/dist/cli-delegate/cmd_migrate.js +2 -2
  50. package/dist/cli-delegate/cmd_packs_active.js +2 -2
  51. package/dist/cli-delegate/cmd_prune.js +4 -4
  52. package/dist/cli-delegate/cmd_route_audit.js +2 -2
  53. package/dist/cli-delegate/cmd_route_explain.js +1 -1
  54. package/dist/cli-delegate/cmd_settings_get.js +1 -7
  55. package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
  56. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  57. package/dist/cli-delegate/cmd_sync.js +4 -4
  58. package/dist/cli-delegate/cmd_uninstall.js +4 -4
  59. package/dist/cli-delegate/cmd_update.js +1 -1
  60. package/dist/cli-delegate/cmd_upgrade.js +1 -1
  61. package/dist/cli-delegate/cmd_validate.js +4 -4
  62. package/dist/cli-delegate/cmd_versions.js +1 -1
  63. package/dist/discovery/deprecation-report.md +1 -1
  64. package/dist/discovery/discovery-manifest.json +69 -20
  65. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  66. package/dist/discovery/discovery-manifest.summary.md +3 -2
  67. package/dist/discovery/orphan-report.md +1 -1
  68. package/dist/discovery/packs.json +39 -2
  69. package/dist/discovery/trust-report.md +2 -2
  70. package/dist/discovery/workspaces.json +12 -4
  71. package/dist/hooks/dispatch.js +373 -35
  72. package/dist/install/install.mjs +42 -17
  73. package/dist/mcp/registry-manifest.json +2 -2
  74. package/dist/mcp/server.json +2 -2
  75. package/dist/mcp/server.mjs +651 -262
  76. package/dist/router.json +1 -1
  77. package/dist/scripts/_lib/agent_settings.js +54 -0
  78. package/dist/scripts/_lib/agent_settings.js.map +1 -1
  79. package/dist/scripts/condense.js +147 -2
  80. package/dist/scripts/condense.js.map +1 -1
  81. package/dist/server/schemas/settings.js +6 -7
  82. package/dist/server/schemas/settings.js.map +1 -1
  83. package/dist/shared/settingsCarveOut.js +0 -9
  84. package/dist/shared/settingsCarveOut.js.map +1 -1
  85. package/dist/shared/settingsClasses.js +11 -9
  86. package/dist/shared/settingsClasses.js.map +1 -1
  87. package/dist/ui/assets/{index-DmR0mbk4.js → index-JDAF00bC.js} +2 -2
  88. package/dist/ui/assets/{index-DmR0mbk4.js.map → index-JDAF00bC.js.map} +1 -1
  89. package/dist/ui/index.html +1 -1
  90. package/docs/contracts/settings-classes.md +39 -9
  91. package/package.json +1 -1
  92. package/src/agent-src/templates/AGENTS.md +4 -4
  93. package/src/config/agent-settings.template.yml +52 -66
  94. package/src/config/discovery/packs.yml +11 -0
  95. package/src/config/discovery/workspaces.yml +1 -1
  96. package/src/config/gate-violation-baselines.json +3 -3
  97. package/src/scripts/_cli/cmd_conformance.ts +14 -42
  98. package/src/scripts/_cli/cmd_doctor.ts +65 -48
  99. package/src/scripts/_lib/agent_settings.ts +58 -0
  100. package/src/scripts/_lib/auto_dispatch.corpus.test.ts +1 -1
  101. package/src/scripts/_lib/auto_dispatch.ts +23 -18
  102. package/src/scripts/_lib/capsule_trigger.ts +124 -0
  103. package/src/scripts/_lib/host_capability.ts +63 -6
  104. package/src/scripts/_lib/judgment_ladder.ts +382 -0
  105. package/src/scripts/_lib/orchestration_gate.ts +8 -0
  106. package/src/scripts/_lib/orchestration_record.ts +37 -0
  107. package/src/scripts/_lib/orchestration_savings.ts +1 -1
  108. package/src/scripts/_lib/self_repair.ts +160 -13
  109. package/src/scripts/_lib/self_repair_store.ts +28 -0
  110. package/src/scripts/_lib/subagent_capsule.ts +182 -0
  111. package/src/scripts/_lib/subagent_response.ts +19 -0
  112. package/src/scripts/_lib/subagent_spawn.ts +13 -0
  113. package/src/scripts/_lib/subagent_steering.ts +9 -4
  114. package/src/scripts/_lib/tier_budget_routing.ts +6 -1
  115. package/src/scripts/_lib/worker_budget.ts +37 -2
  116. package/src/scripts/ai_council/clients.ts +76 -54
  117. package/src/scripts/ai_council/config.ts +94 -6
  118. package/src/scripts/ai_council/handoff.ts +113 -0
  119. package/src/scripts/ai_council/modes.ts +19 -4
  120. package/src/scripts/ai_council/orchestrator.ts +105 -0
  121. package/src/scripts/ai_council/quorum.ts +84 -0
  122. package/src/scripts/ai_council/session.ts +44 -0
  123. package/src/scripts/ai_council/transport_resolver.ts +127 -1
  124. package/src/scripts/ai_team/availability.ts +110 -0
  125. package/src/scripts/ai_team/config.ts +34 -14
  126. package/src/scripts/ai_team/team_dispatch.ts +71 -29
  127. package/src/scripts/bench_critic_protocol.ts +396 -0
  128. package/src/scripts/check_detector_corpus.ts +202 -0
  129. package/src/scripts/check_hook_bundle_freshness.ts +4 -1
  130. package/src/scripts/condense.ts +162 -2
  131. package/src/scripts/council_cli.ts +401 -25
  132. package/src/scripts/forensics_report.ts +333 -0
  133. package/src/scripts/hooks/delegation_nudge_hook.ts +69 -35
  134. package/src/scripts/lint_discovery_vocabulary.ts +2 -0
  135. package/src/scripts/lint_never_silent.ts +276 -0
  136. package/src/scripts/lint_no_activation_gates.ts +271 -0
  137. package/src/scripts/orchestration_record.ts +8 -0
  138. package/src/scripts/rebuild_hook_bundle.ts +177 -0
  139. package/src/scripts/release.ts +74 -2
  140. package/src/scripts/routing_doctor.ts +21 -36
  141. package/src/scripts/score_skill_selection.ts +19 -5
  142. package/src/scripts/self_repair_cli.ts +287 -57
  143. package/src/scripts/team_events_spike.ts +271 -0
  144. package/src/scripts/team_review_gate_hook.ts +31 -9
  145. package/src/server/schemas/settings.ts +10 -17
  146. package/src/shared/settingsCarveOut.ts +0 -10
  147. package/src/shared/settingsClasses.ts +11 -9
@@ -12,9 +12,14 @@
12
12
  * instruction lifts that floor. Running this command IS the user's word.
13
13
  *
14
14
  * Route selection follows the spec exactly: a pull request when the fix can be
15
- * authored and pushed, an issue when it cannot, and a local record when nothing
16
- * can leave the machine. `--dry-run` prints the resolved route and the rendered
17
- * body and touches no network — that is the path the tests drive.
15
+ * authored and pushed — directly when the pre-flight probe shows upstream push
16
+ * rights, via fork → cross-repo PR when it does not — an issue when it cannot,
17
+ * and a local record when nothing can leave the machine. Once publishing was
18
+ * authorized, failures degrade down the ladder WITHIN the same call (push/PR
19
+ * failure or 30 s timeout → issue; issue failure → the record stays open with
20
+ * the errors attached) — degrading never widens what leaves the machine, only
21
+ * the vehicle. `--dry-run` prints the resolved route and the rendered body and
22
+ * touches no network — that is the path the tests drive.
18
23
  *
19
24
  * Fail-closed on privacy: the record's own text goes through the audited
20
25
  * privacy floor before anything leaves the machine, and a refusal downgrades
@@ -26,17 +31,31 @@ import * as path from 'node:path';
26
31
  import { fileURLToPath, pathToFileURL } from 'node:url';
27
32
 
28
33
  import {
29
- chooseEgressRoute,
34
+ chooseEgress,
30
35
  type DefectRecord,
31
36
  type EgressCapability,
32
37
  type EgressRoute,
33
38
  egressBlockedReason,
39
+ type PushVia,
34
40
  renderReport,
41
+ sanitizeEvidence,
35
42
  } from './_lib/self_repair.js';
36
- import { listRecords, markReleased, openRecords, readRecord } from './_lib/self_repair_store.js';
43
+ import {
44
+ attachReleaseErrors,
45
+ listRecords,
46
+ markReleased,
47
+ openRecords,
48
+ readRecord,
49
+ } from './_lib/self_repair_store.js';
37
50
 
38
51
  export const UPSTREAM_REPO = 'event4u-app/agent-config';
39
52
 
53
+ /**
54
+ * Per-step ceiling for every network-touching egress step. A hung `git push`
55
+ * or `gh` call degrades down the ladder instead of hanging the release.
56
+ */
57
+ export const EGRESS_STEP_TIMEOUT_MS = 30_000;
58
+
40
59
  const EXIT_OK = 0;
41
60
  const EXIT_FAIL = 1;
42
61
 
@@ -44,12 +63,30 @@ const EXIT_FAIL = 1;
44
63
  export interface Probe {
45
64
  agentConfigCheckout: string | null;
46
65
  ghAuthenticated: boolean;
47
- canPush: boolean;
66
+ canPushUpstream: boolean;
67
+ canFork: boolean;
68
+ }
69
+
70
+ export interface RunResult {
71
+ ok: boolean;
72
+ out: string;
73
+ timedOut: boolean;
48
74
  }
49
75
 
50
- function run(cmd: string, args: string[], cwd?: string): { ok: boolean; out: string } {
51
- const r = spawnSync(cmd, args, { cwd, encoding: 'utf-8' });
52
- return { ok: r.status === 0, out: `${r.stdout ?? ''}${r.stderr ?? ''}` };
76
+ /** Injectable command runner — tests script it, production shells out. */
77
+ export type Runner = (cmd: string, args: string[], cwd?: string) => RunResult;
78
+
79
+ function run(cmd: string, args: string[], cwd?: string): RunResult {
80
+ const r = spawnSync(cmd, args, { cwd, encoding: 'utf-8', timeout: EGRESS_STEP_TIMEOUT_MS });
81
+ // Only spawnSync's own ETIMEDOUT counts: a child killed externally by
82
+ // SIGTERM is a failure, not a timeout, and recording it as "timed out"
83
+ // points the next debugging session at the wrong cause (R2 finding #3).
84
+ const timedOut = (r.error as NodeJS.ErrnoException | undefined)?.code === 'ETIMEDOUT';
85
+ return {
86
+ ok: r.status === 0 && !timedOut,
87
+ out: `${r.stdout ?? ''}${r.stderr ?? ''}`,
88
+ timedOut,
89
+ };
53
90
  }
54
91
 
55
92
  /**
@@ -71,18 +108,63 @@ export function findAgentConfigCheckout(env: NodeJS.ProcessEnv, cwd: string): st
71
108
  return null;
72
109
  }
73
110
 
74
- export function probeMachine(env: NodeJS.ProcessEnv, cwd: string): Probe {
111
+ /**
112
+ * Pre-flight probe of ACTUAL push rights, not remote existence. `git remote`
113
+ * returning a name proves only that the repo was cloned — every consumer of
114
+ * the public repo has that and no write access. `gh api repos/<repo>` returns
115
+ * the authenticated viewer's `permissions.push` and the repo's `allow_forking`,
116
+ * which are the two facts the route actually depends on. An unreachable or
117
+ * unauthenticated API answers `false` for both — the ladder then degrades to
118
+ * an issue rather than failing at push time.
119
+ */
120
+ export function probeUpstreamRights(
121
+ runner: Runner,
122
+ repo: string,
123
+ ): { canPushUpstream: boolean; canFork: boolean } {
124
+ const r = runner('gh', ['api', `repos/${repo}`]);
125
+ if (!r.ok) {
126
+ return { canPushUpstream: false, canFork: false };
127
+ }
128
+ try {
129
+ const parsed: unknown = JSON.parse(r.out);
130
+ const meta = parsed as {
131
+ permissions?: { push?: boolean };
132
+ allow_forking?: boolean;
133
+ };
134
+ return {
135
+ canPushUpstream: meta.permissions?.push === true,
136
+ canFork: meta.allow_forking !== false,
137
+ };
138
+ } catch {
139
+ return { canPushUpstream: false, canFork: false };
140
+ }
141
+ }
142
+
143
+ export function probeMachine(
144
+ env: NodeJS.ProcessEnv,
145
+ cwd: string,
146
+ runner: Runner = run,
147
+ repo: string = UPSTREAM_REPO,
148
+ ): Probe {
75
149
  const checkout = findAgentConfigCheckout(env, cwd);
76
- const gh = run('gh', ['auth', 'status']);
77
- const canPush = checkout !== null && run('git', ['remote'], checkout).out.trim().length > 0;
78
- return { agentConfigCheckout: checkout, ghAuthenticated: gh.ok, canPush };
150
+ const gh = runner('gh', ['auth', 'status']);
151
+ const rights = gh.ok
152
+ ? probeUpstreamRights(runner, repo)
153
+ : { canPushUpstream: false, canFork: false };
154
+ return {
155
+ agentConfigCheckout: checkout,
156
+ ghAuthenticated: gh.ok,
157
+ canPushUpstream: rights.canPushUpstream,
158
+ canFork: gh.ok && rights.canFork,
159
+ };
79
160
  }
80
161
 
81
162
  export function capabilityOf(probe: Probe): EgressCapability {
82
163
  return {
83
164
  hasAgentConfigCheckout: probe.agentConfigCheckout !== null,
84
165
  ghAuthenticated: probe.ghAuthenticated,
85
- canPush: probe.canPush,
166
+ canPushUpstream: probe.canPushUpstream,
167
+ canFork: probe.canFork,
86
168
  };
87
169
  }
88
170
 
@@ -93,6 +175,7 @@ export function titleFor(record: DefectRecord): string {
93
175
 
94
176
  export interface ReleasePlan {
95
177
  route: EgressRoute;
178
+ pushVia: PushVia | null;
96
179
  blocked: string | null;
97
180
  title: string;
98
181
  body: string;
@@ -101,9 +184,177 @@ export interface ReleasePlan {
101
184
  /** Pure: everything `release` decides before it touches the network. */
102
185
  export function planRelease(record: DefectRecord, probe: Probe, repoRoot: string): ReleasePlan {
103
186
  const blocked = egressBlockedReason(record, repoRoot);
104
- const route: EgressRoute =
105
- blocked !== null ? 'local-only' : chooseEgressRoute(capabilityOf(probe));
106
- return { route, blocked, title: titleFor(record), body: renderReport(record, route) };
187
+ const choice =
188
+ blocked !== null
189
+ ? { route: 'local-only' as const, pushVia: null }
190
+ : chooseEgress(capabilityOf(probe));
191
+ return {
192
+ route: choice.route,
193
+ pushVia: choice.pushVia,
194
+ blocked,
195
+ title: titleFor(record),
196
+ body: renderReport(record, choice.route),
197
+ };
198
+ }
199
+
200
+ // ── egress ladder execution ────────────────────────────────────────
201
+
202
+ /** One egress step's outcome — kept for the record when the ladder exhausts. */
203
+ export interface EgressAttempt {
204
+ step: string;
205
+ ok: boolean;
206
+ detail: string;
207
+ }
208
+
209
+ export interface ReleaseOutcome {
210
+ /** What actually got published, or null when the whole ladder failed. */
211
+ published: 'pull-request' | 'issue' | null;
212
+ attempts: EgressAttempt[];
213
+ }
214
+
215
+ export interface ReleaseDeps {
216
+ runner: Runner;
217
+ now: () => string;
218
+ }
219
+
220
+ const FORK_REMOTE = 'self-repair-fork';
221
+
222
+ function failDetail(step: string, r: RunResult): string {
223
+ return r.timedOut
224
+ ? `${step}: timed out after ${EGRESS_STEP_TIMEOUT_MS / 1000}s`
225
+ : `${step}: ${r.out.trim() || 'failed'}`;
226
+ }
227
+
228
+ /**
229
+ * Push the fix branch — directly, or via fork → cross-repo PR — then open the
230
+ * PR. Returns null on success; on any step failure or timeout returns the
231
+ * failed attempt so the caller degrades down the ladder.
232
+ */
233
+ function tryPullRequest(
234
+ plan: ReleasePlan,
235
+ probe: Probe,
236
+ repo: string,
237
+ branch: string,
238
+ via: PushVia,
239
+ deps: ReleaseDeps,
240
+ ): EgressAttempt | null {
241
+ const checkout = probe.agentConfigCheckout!;
242
+ let prHead: string | null = null;
243
+
244
+ if (via === 'upstream') {
245
+ const pushed = deps.runner('git', ['push', '-u', 'origin', branch], checkout);
246
+ if (!pushed.ok) {
247
+ return { step: 'push-upstream', ok: false, detail: failDetail('push-upstream', pushed) };
248
+ }
249
+ } else {
250
+ const forked = deps.runner('gh', ['repo', 'fork', repo, '--clone=false']);
251
+ if (!forked.ok) {
252
+ return { step: 'fork-ensure', ok: false, detail: failDetail('fork-ensure', forked) };
253
+ }
254
+ const who = deps.runner('gh', ['api', 'user', '--jq', '.login']);
255
+ if (!who.ok || who.out.trim().length === 0) {
256
+ return { step: 'whoami', ok: false, detail: failDetail('whoami', who) };
257
+ }
258
+ const login = who.out.trim();
259
+ const repoName = repo.split('/')[1] ?? repo;
260
+ const hasRemote = deps.runner('git', ['remote', 'get-url', FORK_REMOTE], checkout);
261
+ if (!hasRemote.ok) {
262
+ const added = deps.runner(
263
+ 'git',
264
+ ['remote', 'add', FORK_REMOTE, `https://github.com/${login}/${repoName}.git`],
265
+ checkout,
266
+ );
267
+ if (!added.ok) {
268
+ return { step: 'fork-remote', ok: false, detail: failDetail('fork-remote', added) };
269
+ }
270
+ }
271
+ const pushed = deps.runner('git', ['push', '-u', FORK_REMOTE, branch], checkout);
272
+ if (!pushed.ok) {
273
+ return { step: 'push-fork', ok: false, detail: failDetail('push-fork', pushed) };
274
+ }
275
+ prHead = `${login}:${branch}`;
276
+ }
277
+
278
+ const prArgs = ['pr', 'create', '--repo', repo, '--title', plan.title, '--body', plan.body];
279
+ if (prHead !== null) {
280
+ prArgs.push('--head', prHead);
281
+ }
282
+ const pr = deps.runner('gh', prArgs, checkout);
283
+ if (!pr.ok) {
284
+ return { step: 'pr-create', ok: false, detail: failDetail('pr-create', pr) };
285
+ }
286
+ process.stdout.write(`${pr.out}`);
287
+ return null;
288
+ }
289
+
290
+ /**
291
+ * The egress ladder, executed within ONE `release` call: direct push → fork
292
+ * push → issue → open-with-errors. Every rung is separately timed out; a rung
293
+ * that fails degrades to the next instead of aborting the release. When the
294
+ * whole ladder fails, the record stays `open` with every failed attempt
295
+ * attached (sanitized — command output can carry local paths).
296
+ */
297
+ export function executeRelease(
298
+ record: DefectRecord,
299
+ plan: ReleasePlan,
300
+ probe: Probe,
301
+ repo: string,
302
+ deps: ReleaseDeps,
303
+ ): ReleaseOutcome {
304
+ const attempts: EgressAttempt[] = [];
305
+
306
+ if (plan.route === 'pull-request') {
307
+ const checkout = probe.agentConfigCheckout!;
308
+ const branch = deps.runner('git', ['branch', '--show-current'], checkout).out.trim();
309
+ if (!branch || branch === 'main') {
310
+ // Precondition, not an egress failure: the authored fix exists and
311
+ // must ride its own branch — an issue instead would drop the fix.
312
+ attempts.push({
313
+ step: 'branch-check',
314
+ ok: false,
315
+ detail:
316
+ 'the fix must sit on its own branch in the agent-config checkout ' +
317
+ `(currently: ${branch || 'detached'})`,
318
+ });
319
+ return { published: null, attempts };
320
+ }
321
+ const ladder: PushVia[] =
322
+ plan.pushVia === 'upstream' && probe.canFork
323
+ ? ['upstream', 'fork']
324
+ : [plan.pushVia ?? 'upstream'];
325
+ for (const via of ladder) {
326
+ const failed = tryPullRequest(plan, probe, repo, branch, via, deps);
327
+ if (failed === null) {
328
+ return { published: 'pull-request', attempts };
329
+ }
330
+ attempts.push(failed);
331
+ }
332
+ }
333
+
334
+ if (plan.route === 'pull-request' || plan.route === 'issue') {
335
+ // Re-render for the rung actually taken: a plan that degraded from
336
+ // pull-request must not file an issue whose body claims
337
+ // `Route: pull-request` (R2 finding #4).
338
+ const issueBody =
339
+ plan.route === 'issue' ? plan.body : renderReport(record, 'issue');
340
+ const issue = deps.runner('gh', [
341
+ 'issue',
342
+ 'create',
343
+ '--repo',
344
+ repo,
345
+ '--title',
346
+ plan.title,
347
+ '--body',
348
+ issueBody,
349
+ ]);
350
+ if (issue.ok) {
351
+ process.stdout.write(`${issue.out}`);
352
+ return { published: 'issue', attempts };
353
+ }
354
+ attempts.push({ step: 'issue-create', ok: false, detail: failDetail('issue-create', issue) });
355
+ }
356
+
357
+ return { published: null, attempts };
107
358
  }
108
359
 
109
360
  function statusCmd(root: string): number {
@@ -142,7 +393,8 @@ function releaseCmd(root: string, argv: string[]): number {
142
393
  const probe = probeMachine(process.env, process.cwd());
143
394
  const plan = planRelease(record, probe, root);
144
395
 
145
- process.stdout.write(`self-repair:release ${fp} -> route=${plan.route}\n`);
396
+ const via = plan.pushVia !== null ? ` via=${plan.pushVia}` : '';
397
+ process.stdout.write(`self-repair:release ${fp} -> route=${plan.route}${via}\n`);
146
398
  if (plan.blocked !== null) {
147
399
  process.stdout.write(` WARN ${plan.blocked}\n`);
148
400
  }
@@ -156,47 +408,25 @@ function releaseCmd(root: string, argv: string[]): number {
156
408
  );
157
409
  return EXIT_OK;
158
410
  }
159
- if (plan.route === 'issue') {
160
- const r = run('gh', [
161
- 'issue',
162
- 'create',
163
- '--repo',
164
- repo,
165
- '--title',
166
- plan.title,
167
- '--body',
168
- plan.body,
169
- ]);
170
- process.stdout.write(`${r.out}\n`);
171
- if (!r.ok) {
172
- return EXIT_FAIL;
173
- }
174
- } else {
175
- const checkout = probe.agentConfigCheckout!;
176
- const branch = run('git', ['branch', '--show-current'], checkout).out.trim();
177
- if (!branch || branch === 'main') {
178
- process.stderr.write(
179
- ' The fix must sit on its own branch in the agent-config checkout ' +
180
- `(currently: ${branch || 'detached'}).\n`,
181
- );
182
- return EXIT_FAIL;
183
- }
184
- const pushed = run('git', ['push', '-u', 'origin', branch], checkout);
185
- if (!pushed.ok) {
186
- process.stderr.write(`${pushed.out}\n`);
187
- return EXIT_FAIL;
188
- }
189
- const pr = run(
190
- 'gh',
191
- ['pr', 'create', '--repo', repo, '--title', plan.title, '--body', plan.body],
192
- checkout,
411
+
412
+ const deps: ReleaseDeps = { runner: run, now: () => new Date().toISOString() };
413
+ const outcome = executeRelease(record, plan, probe, repo, deps);
414
+
415
+ for (const a of outcome.attempts) {
416
+ process.stderr.write(` FAIL ${a.detail}\n`);
417
+ }
418
+ if (outcome.published === null) {
419
+ const errors = outcome.attempts.map((a) => sanitizeEvidence(a.detail));
420
+ attachReleaseErrors(root, fp, errors, deps.now());
421
+ process.stderr.write(
422
+ ' The egress ladder is exhausted; the record stays open with the errors attached.\n',
193
423
  );
194
- process.stdout.write(`${pr.out}\n`);
195
- if (!pr.ok) {
196
- return EXIT_FAIL;
197
- }
424
+ return EXIT_FAIL;
425
+ }
426
+ if (outcome.published !== plan.route) {
427
+ process.stdout.write(` Degraded to ${outcome.published} after failed attempt(s).\n`);
198
428
  }
199
- markReleased(root, fp, new Date().toISOString());
429
+ markReleased(root, fp, deps.now());
200
430
  return EXIT_OK;
201
431
  }
202
432
 
@@ -0,0 +1,271 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Team-event payload shape spike (Phase 5.1, road-to-always-on-orchestration).
4
+ *
5
+ * `TaskCreated` / `TaskCompleted` / `TeammateIdle` are documented Claude Code
6
+ * hook events (agent teams, behind `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS`),
7
+ * but this repo has never observed a real payload for any of them — the
8
+ * shapes are documented, not verified. This script is manually startable
9
+ * ONLY: it is never wired into `hook_manifest.yaml` / `hooks.json`. A
10
+ * maintainer points a hook command at it (or pipes a captured payload) to
11
+ * record a SHAPE — field names + JS/JSON types — and never a value.
12
+ * Privacy by construction: the recursive walk below inspects `typeof` /
13
+ * `Array.isArray`, nothing else, so there is no code path that could copy a
14
+ * value into the written report.
15
+ *
16
+ * SCOPE OF THE PRIVACY CLAIM (stated honestly, per independent review): the
17
+ * claim above holds for a payload whose OWN keys are static field names —
18
+ * true of the three documented envelopes this script targets
19
+ * (`TaskCreated` / `TaskCompleted` / `TeammateIdle`), where a key is always
20
+ * a schema name like `task_id` or `teammate_name`. It does NOT hold for a
21
+ * payload with dynamically-named keys — a map keyed by a file path, a
22
+ * person's name, or any other runtime value — because `extractShape` walks
23
+ * `Object.keys()` and records each key verbatim as a "field name". A
24
+ * dynamic key is not a field name; it is a value wearing a field name's
25
+ * clothes, and this script would write it into the tracked report exactly
26
+ * as it would refuse to write a value. Point this script only at the three
27
+ * documented shapes (or another envelope you have separately confirmed has
28
+ * static keys) — never at an undocumented payload that might carry a
29
+ * dynamically-keyed map.
30
+ *
31
+ * Usage (only meaningful with the experimental flag set):
32
+ *
33
+ * echo '<payload-json>' | \
34
+ * CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 npx tsx src/scripts/team_events_spike.ts [EventName]
35
+ *
36
+ * `EventName` is optional; if omitted, the payload's own `hook_event_name` /
37
+ * `event` / `eventName` / `event_name` / `type` field is read and matched
38
+ * against the three known `TEAM_EVENT_NAMES` — an unrecognised value falls
39
+ * back to `unlabeled-event` rather than copying the raw (payload-supplied)
40
+ * string into the report, which would itself be writing a value under the
41
+ * cover of a "name".
42
+ *
43
+ * Without the flag: clean one-line skip, exit 0, stdin is never touched.
44
+ *
45
+ * Output: appends one dated section per capture to
46
+ * `agents/evidence/analysis/team-event-shapes.md`.
47
+ */
48
+ import * as fs from 'node:fs';
49
+ import * as path from 'node:path';
50
+ import { fileURLToPath, pathToFileURL } from 'node:url';
51
+
52
+ const _HERE = fileURLToPath(import.meta.url);
53
+ const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
54
+ const OUT_FILE = path.join(ROOT, 'agents', 'evidence', 'analysis', 'team-event-shapes.md');
55
+
56
+ export const FLAG = 'CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS';
57
+ export const TEAM_EVENT_NAMES = ['TaskCreated', 'TaskCompleted', 'TeammateIdle'] as const;
58
+
59
+ export interface ShapeEntry {
60
+ path: string;
61
+ type: string;
62
+ }
63
+
64
+ /** JS/JSON type of a value. Reads only `typeof` / `Array.isArray` — never the value. */
65
+ function _typeOf(value: unknown): string {
66
+ if (value === null) return 'null';
67
+ if (Array.isArray(value)) return 'array';
68
+ return typeof value; // 'string' | 'number' | 'boolean' | 'object' | 'undefined'
69
+ }
70
+
71
+ /**
72
+ * Walk a parsed JSON payload and record field-name + type only.
73
+ *
74
+ * This never returns, prints, or embeds a value — recursion is driven
75
+ * entirely by structural type and object keys.
76
+ */
77
+ export function extractShape(value: unknown, prefix = ''): ShapeEntry[] {
78
+ const entries: ShapeEntry[] = [];
79
+ const t = _typeOf(value);
80
+ if (prefix !== '') {
81
+ entries.push({ path: prefix, type: t });
82
+ }
83
+ if (t === 'array') {
84
+ // An event payload's arrays are homogeneous lists, not fixed tuples —
85
+ // merge every element's shape under one `[]` path (deduped below).
86
+ for (const item of value as unknown[]) {
87
+ entries.push(...extractShape(item, `${prefix}[]`));
88
+ }
89
+ return entries;
90
+ }
91
+ if (t === 'object') {
92
+ const obj = value as Record<string, unknown>;
93
+ for (const key of Object.keys(obj).sort()) {
94
+ entries.push(...extractShape(obj[key], prefix === '' ? key : `${prefix}.${key}`));
95
+ }
96
+ }
97
+ return entries;
98
+ }
99
+
100
+ /** De-duplicate by path+type — an array's elements repeat the same shape. */
101
+ function _dedupe(entries: ShapeEntry[]): ShapeEntry[] {
102
+ const seen = new Set<string>();
103
+ const out: ShapeEntry[] = [];
104
+ for (const e of entries) {
105
+ const key = `${e.path}\u0000${e.type}`;
106
+ if (seen.has(key)) continue;
107
+ seen.add(key);
108
+ out.push(e);
109
+ }
110
+ return out;
111
+ }
112
+
113
+ /**
114
+ * Best-effort event-name inference from the payload's own envelope.
115
+ *
116
+ * m4 fix (independent-review finding): the envelope field this reads
117
+ * (`hook_event_name` / `event` / `type` / …) carries a payload-SUPPLIED
118
+ * string — copying it verbatim into the recorded report is exactly the
119
+ * "copy a value into the report" this script otherwise refuses to do
120
+ * (see the module header's privacy claim). Matched against the known
121
+ * `TEAM_EVENT_NAMES` allowlist instead: a recognised name is safe to
122
+ * record because it is drawn from three publicly documented, stable
123
+ * literals, never from the untrusted payload string itself; anything
124
+ * else returns `null` and the caller falls back to the fixed literal
125
+ * `'unlabeled-event'`.
126
+ */
127
+ export function inferEventName(payload: unknown): string | null {
128
+ if (payload === null || typeof payload !== 'object' || Array.isArray(payload)) return null;
129
+ const obj = payload as Record<string, unknown>;
130
+ const known = new Set<string>(TEAM_EVENT_NAMES);
131
+ for (const key of ['hook_event_name', 'event', 'eventName', 'event_name', 'type']) {
132
+ const v = obj[key];
133
+ if (typeof v === 'string' && known.has(v)) return v;
134
+ }
135
+ return null;
136
+ }
137
+
138
+ /** Render one capture as a Markdown section — field names + types only. */
139
+ export function renderShapeReport(eventName: string, entries: ShapeEntry[], observedAt: string): string {
140
+ const deduped = _dedupe(entries);
141
+ const rows = deduped.map((e) => `| \`${e.path}\` | ${e.type} |`).join('\n');
142
+ const table = rows === '' ? '_(empty payload — no fields observed)_' : `| Field | Type |\n|---|---|\n${rows}`;
143
+ return [`## ${eventName} — observed ${observedAt}`, '', table, ''].join('\n');
144
+ }
145
+
146
+ function _printGuide(): void {
147
+ process.stdout.write(
148
+ [
149
+ 'team-event shape spike (manual, not hook-bound)',
150
+ '',
151
+ `${FLAG} is set — recording.`,
152
+ '',
153
+ `Observed events: ${TEAM_EVENT_NAMES.join(', ')}`,
154
+ '',
155
+ 'Pipe one captured JSON payload on stdin — values are never written,',
156
+ 'only field names + types:',
157
+ '',
158
+ ` echo '<payload-json>' | ${FLAG}=1 npx tsx src/scripts/team_events_spike.ts [EventName]`,
159
+ '',
160
+ `Shapes append to ${path.relative(ROOT, OUT_FILE)}.`,
161
+ '',
162
+ ].join('\n'),
163
+ );
164
+ }
165
+
166
+ async function _readStdin(): Promise<string> {
167
+ const chunks: Buffer[] = [];
168
+ // `for await` streams stdin — a synchronous `readFileSync(0)` EAGAINs
169
+ // past the pipe buffer on a large payload; this has no such ceiling.
170
+ for await (const chunk of process.stdin) {
171
+ chunks.push(chunk as Buffer);
172
+ }
173
+ return Buffer.concat(chunks).toString('utf-8');
174
+ }
175
+
176
+ export interface RunResult {
177
+ skipped: boolean;
178
+ reason?: string;
179
+ eventName?: string;
180
+ entryCount?: number;
181
+ outFile?: string;
182
+ }
183
+
184
+ export interface RunOptions {
185
+ argv?: string[];
186
+ env?: NodeJS.ProcessEnv;
187
+ readStdin?: () => Promise<string>;
188
+ writeReport?: (section: string) => void;
189
+ now?: () => string;
190
+ }
191
+
192
+ /**
193
+ * Orchestrates the spike. Every side effect is injectable so tests never
194
+ * touch the real environment, stdin, or the tracked report file.
195
+ */
196
+ export async function run(opts?: RunOptions): Promise<RunResult> {
197
+ const env = opts?.env ?? process.env;
198
+ const argv = opts?.argv ?? process.argv.slice(2);
199
+ const readStdin = opts?.readStdin ?? _readStdin;
200
+ const now = opts?.now ?? (() => new Date().toISOString());
201
+ const writeReport =
202
+ opts?.writeReport ??
203
+ ((section: string) => {
204
+ fs.mkdirSync(path.dirname(OUT_FILE), { recursive: true });
205
+ fs.appendFileSync(OUT_FILE, section, 'utf-8');
206
+ });
207
+
208
+ if (!env[FLAG]) {
209
+ const reason = `${FLAG} is not set — teams are experimental and unavailable; skipping.`;
210
+ process.stdout.write(`${reason}\n`);
211
+ return { skipped: true, reason };
212
+ }
213
+
214
+ _printGuide();
215
+ const raw = (await readStdin()).trim();
216
+ if (raw === '') {
217
+ const reason = 'no payload received on stdin — nothing recorded.';
218
+ process.stdout.write(`${reason}\n`);
219
+ return { skipped: true, reason };
220
+ }
221
+
222
+ let payload: unknown;
223
+ try {
224
+ payload = JSON.parse(raw);
225
+ } catch (e) {
226
+ const reason = `invalid JSON on stdin: ${(e as Error).message}`;
227
+ process.stderr.write(`team_events_spike: ${reason}\n`);
228
+ return { skipped: true, reason };
229
+ }
230
+
231
+ const eventName = argv[0] ?? inferEventName(payload) ?? 'unlabeled-event';
232
+ const entries = extractShape(payload);
233
+ const section = renderShapeReport(eventName, entries, now());
234
+ writeReport(`\n${section}`);
235
+ const count = _dedupe(entries).length;
236
+ process.stdout.write(`recorded ${count} field shape(s) for ${eventName} -> ${path.relative(ROOT, OUT_FILE)}\n`);
237
+ return { skipped: false, eventName, entryCount: count, outFile: OUT_FILE };
238
+ }
239
+
240
+ function _isCliEntry(): boolean {
241
+ if (process.argv[1] === undefined) {
242
+ return false;
243
+ }
244
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
245
+ if (import.meta.url === argvUrl) {
246
+ return true;
247
+ }
248
+ // A symlinked invocation (e.g. via an installed `.augment/` projection)
249
+ // makes the raw URLs differ; compare realpaths so the entry guard still
250
+ // fires when run through a symlink.
251
+ try {
252
+ const here = fs.realpathSync(fileURLToPath(import.meta.url));
253
+ const argv = fs.realpathSync(path.resolve(process.argv[1]));
254
+ return here === argv;
255
+ } catch {
256
+ return false;
257
+ }
258
+ }
259
+
260
+ if (_isCliEntry() || process.argv[1] === _HERE) {
261
+ run()
262
+ .then(() => {
263
+ // Observational spike, never a gate — always exit 0 on a
264
+ // completed run (skip or record alike).
265
+ process.exit(0);
266
+ })
267
+ .catch((e: unknown) => {
268
+ process.stderr.write(`team_events_spike: ${(e as Error).message}\n`);
269
+ process.exit(1);
270
+ });
271
+ }