@hegemonart/get-design-done 1.27.0 → 1.27.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,14 +5,14 @@
5
5
  },
6
6
  "metadata": {
7
7
  "description": "Get Design Done — 5-stage agent-orchestrated design pipeline with 9 connections, handoff-first workflow, bidirectional Figma write-back, 22+ specialized agents, queryable knowledge layer (intel store, dependency analysis, learnings extraction), and a self-improvement loop (reflector, frontmatter + budget feedback, global-skills layer). v1.20.0 ships the SDK foundation: gdd-state MCP server (11 typed tools), lockfile-safe STATE.md mutations, event stream, and resilience primitives (jittered-backoff, rate-guard, error-classifier, iteration-budget) for rate-limit + 429 + context-overflow recovery. Full CI/CD pipeline (Node 22/24 × Linux/macOS/Windows) and release automation (auto-tag + GitHub Release + release-time smoke test).",
8
- "version": "1.27.0"
8
+ "version": "1.27.1"
9
9
  },
10
10
  "plugins": [
11
11
  {
12
12
  "name": "get-design-done",
13
13
  "source": "./",
14
14
  "description": "Agent-orchestrated 5-stage design pipeline: Brief → Explore → Plan → Design → Verify. 22+ specialized agents, 9 connections (Figma, Refero, Preview, Storybook, Chromatic, Figma Writer, Graphify, Pinterest, Claude Design), Claude Design handoff, bidirectional Figma write-back, and a queryable intel store (.design/intel/) for dependency and learnings queries. Standalone commands: style, darkmode, compare, figma-write, graphify, handoff, analyze-dependencies, skill-manifest, extract-learnings. Embeds NNG heuristics, WCAG thresholds, typographic systems, motion framework, and anti-pattern catalog. Ships with a full CI/CD pipeline (Node 22/24 × Linux/macOS/Windows) and release automation. Optimization layer (v1.0.4.1, retroactive): gdd-router + gdd-cache-manager skills, PreToolUse budget-enforcer hook, tier-aware agent frontmatter, lazy checker gates, streaming synthesizer, /gdd:warm-cache + /gdd:optimize commands, and cost telemetry at .design/telemetry/costs.jsonl — targeting 50-70% per-task token-cost reduction with no quality-floor regression. v1.20.0 SDK foundation: gdd-state MCP server (11 typed tools), lockfile-safe STATE.md mutations, event stream at .design/telemetry/events.jsonl, resilience primitives (jittered-backoff, rate-guard, error-classifier, iteration-budget) with rate-limit + 429 + context-overflow recovery, and TypeScript toolchain.",
15
- "version": "1.27.0",
15
+ "version": "1.27.1",
16
16
  "author": {
17
17
  "name": "hegemonart"
18
18
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "get-design-done",
3
3
  "short_name": "gdd",
4
- "version": "1.27.0",
4
+ "version": "1.27.1",
5
5
  "description": "Agent-orchestrated 5-stage design pipeline: Brief → Explore → Plan → Design → Verify. 22+ specialized agents, 9 connections (Figma, Refero, Preview, Storybook, Chromatic, Figma Writer, Graphify, Pinterest, Claude Design), handoff-first workflow via Claude Design bundles, bidirectional Figma write-back (annotations, Code Connect), queryable intel store (`.design/intel/`) for O(1) design surface lookups, and self-improvement loop (reflector agent, frontmatter + budget feedback, global-skills layer at `~/.claude/gdd/global-skills/`). Standalone commands: style, darkmode, compare, figma-write, graphify, handoff, analyze-dependencies, skill-manifest, extract-learnings, reflect, apply-reflections. Embeds NNG heuristics, WCAG thresholds, typographic systems, motion framework, and anti-pattern catalog. Ships with a full CI/CD pipeline (Node 22/24 × Linux/macOS/Windows, lint + schema + frontmatter + stale-ref + shellcheck + gitleaks + injection-scan + blocking size-budget) and release automation (auto-tag + GitHub Release + release-time smoke test). Optimization layer (v1.0.4.1, retroactive): gdd-router + gdd-cache-manager skills, PreToolUse budget-enforcer hook, tier-aware agent frontmatter, lazy checker gates, streaming synthesizer, /gdd:warm-cache + /gdd:optimize commands, and cost telemetry at .design/telemetry/costs.jsonl — targeting 50-70% per-task token-cost reduction with no quality-floor regression. v1.20.0 SDK foundation: gdd-state MCP server (11 typed tools), lockfile-safe STATE.md mutations, event stream at .design/telemetry/events.jsonl, resilience primitives (jittered-backoff, rate-guard, error-classifier, iteration-budget) with rate-limit + 429 + context-overflow recovery, and TypeScript toolchain.",
6
6
  "author": {
7
7
  "name": "hegemonart",
package/CHANGELOG.md CHANGED
@@ -4,6 +4,30 @@ All notable changes to get-design-done are documented here. Versions follow [sem
4
4
 
5
5
  ---
6
6
 
7
+ ## [1.27.1] — 2026-04-30
8
+
9
+ Phase 27 wiring patch — closes the production-integration gaps left by v1.27.0's "structural ship". v1.27.0 landed all peer-CLI library code + tests + docs but the helpers were exported without callers, so `delegate_to:` on agent frontmatter was validated and then ignored at runtime. v1.27.1 wires the four integration points so delegation actually fires for users who set `delegate_to:` AND allowlist the peer.
10
+
11
+ ### Fixed
12
+
13
+ - **`session-runner.run()` now invokes `tryDelegate` (Plan 27-06 wiring)** — when `opts.delegateTo` is set to a `<peer>-<role>` value AND the registry can route AND the peer is in `.design/config.json#peer_cli.enabled_peers`, the prompt runs on the peer-CLI and `run()` returns the peer result. On peer-absent / peer-error / null result, falls through transparently to the local Anthropic SDK loop (D-07). Previously the `tryDelegate` helper existed in the file but `run()` never called it.
14
+
15
+ - **Real `appendEvent('peer_call_started|complete|failed', ...)` emission (Plan 27-08 wiring)** — replaced the stderr-only placeholder in session-runner with three real event-emission calls. Events flow through Phase 22's `appendEvent()` API using the constants registered in v1.27.0, tagged with `runtime_role: 'peer'` and `peer_id`. Reflector cross-runtime cost-arbitrage (Plan 26-06) now sees peer telemetry. `GDD_PEER_DEBUG=1` continues to mirror the failed events to stderr for live tailing.
16
+
17
+ - **`install.cjs` interactive peer-detection nudge (Plan 27-11 wiring)** — after a successful (non-uninstall, non-dry-run) install in a TTY, scans `peerBinary` paths via `detectInstalledPeers()`. If 1+ peer detected, prompts via `@clack/prompts` with `confirm()` (default: NO). On yes, writes `.design/config.json#peer_cli.enabled_peers`. New `--no-peer-prompt` flag suppresses the prompt entirely (CI-friendly). Silent skip when zero peers detected. Default-NO preserves the opt-in trust contract (D-11).
18
+
19
+ ### Out of scope (known, deferred)
20
+
21
+ - **Bandit `pullWithDelegate` caller (Plan 27-07 wiring)** — `pullWithDelegate` and `updateWithDelegate` ship in the bandit module surface (v1.27.0) but no production caller invokes them yet. Wiring requires `gdd-router` SKILL.md change (procedural, not code) which is out of scope for a wiring patch. Phase 28+ territory once the integration shape is decided. The `delegate?` dimension stays exported as a library extension for ad-hoc use.
22
+
23
+ ### Tests
24
+
25
+ - Existing 23 peer-CLI session-runner / events / end-to-end tests pass after wiring.
26
+ - Existing 33 install.cjs + peer-detect tests pass after the nudge addition.
27
+ - Full Phase 27 surface tests stay green; no new test files (this is a wiring patch, not a new surface).
28
+
29
+ ---
30
+
7
31
  ## [1.27.0] — 2026-04-30
8
32
 
9
33
  Phase 27 Peer-CLI Delegation Layer milestone — closes the **outbound** half of multi-runtime. Phase 24 made gdd installable on 14 runtimes; Phase 21 made the same pipeline run on each; Phase 26 made tier→model resolve correctly per runtime. v1.27.0 adds the missing piece: gdd agents OPTIONALLY delegate to local peer CLIs (Codex via App Server Protocol; Gemini/Cursor/Copilot/Qwen via Agent Client Protocol) when measurably cheaper or higher-quality for the role. Falls back to in-process Anthropic SDK when peer is unavailable. Honors Phase 26 tier maps + Phase 22 event chain + Phase 23.5 bandit posterior — `delegate?` becomes another arm in `(agent_type × tier × delegate)` Thompson sampling, no new ML.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hegemonart/get-design-done",
3
- "version": "1.27.0",
3
+ "version": "1.27.1",
4
4
  "description": "A design-quality pipeline for AI coding agents: brief, plan, implement, and verify UI work against your design system.",
5
5
  "author": "Hegemon",
6
6
  "homepage": "https://github.com/hegemonart/get-design-done",
@@ -37,7 +37,7 @@
37
37
  "provenance": true
38
38
  },
39
39
  "scripts": {
40
- "test": "node --test --experimental-strip-types \"tests/**/*.cjs\" \"tests/**/*.ts\"",
40
+ "test": "node --test --experimental-strip-types \"tests/**/*.test.cjs\" \"tests/**/*.test.ts\"",
41
41
  "typecheck": "tsc --noEmit",
42
42
  "codegen:schemas": "node --experimental-strip-types scripts/codegen-schema-types.ts",
43
43
  "lint:md": "npx --yes markdownlint-cli2 \"**/*.md\" \"#node_modules\" \"#.planning\" \"#.claude\" \"#test-fixture/baselines\"",
@@ -4,7 +4,7 @@
4
4
 
5
5
  For ops-level guidance (when delegation fires, how to enable/disable, fallback diagnostics), see `docs/PEER-DELEGATION.md`.
6
6
 
7
- Protocol shapes are adapted from [`greenpolo/cc-multi-cli`](https://github.com/greenpolo/cc-multi-cli) under Apache 2.0 — see `NOTICE` for full attribution.
7
+ Protocol shapes are adapted from [`greenpolo/cc-multi-cli-plugin`](https://github.com/greenpolo/cc-multi-cli-plugin) under Apache 2.0 — see `NOTICE` for full attribution.
8
8
 
9
9
  ---
10
10
 
@@ -18,8 +18,9 @@
18
18
 
19
19
  const path = require('node:path');
20
20
 
21
- const { listRuntimes, listRuntimeIds } = require('./lib/install/runtimes.cjs');
21
+ const { listRuntimes, listRuntimeIds, detectInstalledPeers, listPeerCapableRuntimes } = require('./lib/install/runtimes.cjs');
22
22
  const { installRuntime, uninstallRuntime } = require('./lib/install/installer.cjs');
23
+ const fs = require('node:fs');
23
24
 
24
25
  function parseArgs(argv) {
25
26
  const args = argv.slice(2);
@@ -60,6 +61,7 @@ function helpText() {
60
61
  ' --uninstall Remove the plugin from selected runtimes',
61
62
  ' --dry-run Print the diff without writing',
62
63
  ' --config-dir D Override the config directory',
64
+ ' --no-peer-prompt Suppress the post-install peer-CLI detection nudge',
63
65
  ' --help, -h Show this message',
64
66
  '',
65
67
  'Environment overrides (per-runtime):',
@@ -196,6 +198,103 @@ async function main() {
196
198
  '',
197
199
  ].join('\n'),
198
200
  );
201
+
202
+ // v1.27.1 — Plan 27-11 wiring: post-install peer-CLI detection nudge.
203
+ // Fires only on real install (not uninstall, not dry-run) when not
204
+ // suppressed by --no-peer-prompt. Silently skips when no peers detected.
205
+ // Always opt-in: writes .design/config.json#peer_cli.enabled_peers
206
+ // ONLY on explicit y/Y; default is no.
207
+ if (!uninstall && !dryRun && !flags.has('--no-peer-prompt')) {
208
+ try {
209
+ await maybeNudgePeerCli({ flags });
210
+ } catch (e) {
211
+ // Nudge is non-critical. Surface a one-line warning but don't fail
212
+ // the install — the plugin is fully functional without peer-CLI.
213
+ process.stderr.write(
214
+ `\n[peer-cli] post-install nudge skipped: ${e && e.message ? e.message : e}\n`,
215
+ );
216
+ }
217
+ }
218
+ }
219
+
220
+ // v1.27.1 — Plan 27-11: post-install nudge. Detects installed peer CLIs,
221
+ // asks the user (interactive y/N) whether to wire them as peers, writes
222
+ // .design/config.json#peer_cli.enabled_peers on yes. Default = NO (opt-in).
223
+ async function maybeNudgePeerCli({ flags }) {
224
+ const detected = detectInstalledPeers();
225
+ if (!detected || detected.length === 0) {
226
+ // Nothing detected — silent skip. (No bad UX of "we found 0 peers".)
227
+ return;
228
+ }
229
+
230
+ // Build the human-readable peer line for the prompt.
231
+ const allPeerCapable = listPeerCapableRuntimes();
232
+ const detectedDisplay = detected
233
+ .map((id) => {
234
+ const r = allPeerCapable.find((x) => x.id === id);
235
+ return r && r.displayName ? r.displayName : id;
236
+ })
237
+ .join(', ');
238
+
239
+ process.stdout.write(
240
+ [
241
+ '',
242
+ '✓ Detected peer CLIs: ' + detectedDisplay,
243
+ '',
244
+ 'gdd v1.27.0 introduced optional peer-CLI delegation. With your',
245
+ 'agents\\u2019 frontmatter `delegate_to:` set, gdd can route specific',
246
+ 'roles through these peer CLIs (cost or quality wins per Phase 23.5',
247
+ 'bandit). You can change this anytime via .design/config.json.',
248
+ '',
249
+ ].join('\n'),
250
+ );
251
+
252
+ // Decide interactive vs scripted. shouldUseInteractive lives in this
253
+ // file; reuse it. If non-TTY, default to no (silent opt-out) so CI
254
+ // installers don't hang waiting for input.
255
+ let confirmed = false;
256
+ if (shouldUseInteractive(flags)) {
257
+ try {
258
+ const clack = require('@clack/prompts');
259
+ const ans = await clack.confirm({
260
+ message: 'Enable peer-CLI delegation for these peers?',
261
+ initialValue: false,
262
+ });
263
+ confirmed = (ans === true);
264
+ } catch {
265
+ // @clack/prompts unavailable — silently default to no.
266
+ confirmed = false;
267
+ }
268
+ }
269
+
270
+ if (!confirmed) {
271
+ process.stdout.write(
272
+ 'Skipped — peer-CLI delegation remains disabled.\n' +
273
+ 'Enable later by adding to .design/config.json:\n' +
274
+ ' { "peer_cli": { "enabled_peers": ' + JSON.stringify(detected) + ' } }\n\n',
275
+ );
276
+ return;
277
+ }
278
+
279
+ // Write the allowlist. Merge with any existing .design/config.json.
280
+ const cfgPath = path.join(process.cwd(), '.design', 'config.json');
281
+ let cfg = {};
282
+ try {
283
+ if (fs.existsSync(cfgPath)) {
284
+ cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
285
+ }
286
+ } catch {
287
+ cfg = {};
288
+ }
289
+ if (!cfg.peer_cli || typeof cfg.peer_cli !== 'object') cfg.peer_cli = {};
290
+ cfg.peer_cli.enabled_peers = detected;
291
+ fs.mkdirSync(path.dirname(cfgPath), { recursive: true });
292
+ fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n');
293
+ process.stdout.write(
294
+ `✓ Wrote .design/config.json — peer-CLI enabled for: ${detected.join(', ')}\n` +
295
+ ' Set delegate_to: <peer>-<role> on agent frontmatter to opt agents in.\n' +
296
+ ' See docs/PEER-DELEGATION.md for the full ops guide.\n\n',
297
+ );
199
298
  }
200
299
 
201
300
  main().catch((err) => {
@@ -21,8 +21,8 @@
21
21
  // string with `shell: true`. We forward-slash the path so Windows shell
22
22
  // resolves it correctly even when the path contains backslashes:
23
23
  //
24
- // // BROKEN on Windows for .cmd shims:
25
- // spawn('C:\\Users\\me\\AppData\\Local\\codex.cmd', ['app-server'])
24
+ // // BROKEN on Windows for .cmd shims (absPath ends in .cmd):
25
+ // spawn(absPath, ['app-server'])
26
26
  //
27
27
  // // WORKS everywhere (.cmd via cmd.exe; non-.cmd via direct exec):
28
28
  // const fwd = absPath.replace(/\\/g, '/');
@@ -200,15 +200,22 @@ async function tryDelegate(args: {
200
200
  return reg !== null ? reg.dispatch : null;
201
201
  })();
202
202
  if (dispatcher === null) {
203
- // No registry available at all — fall through to local. Phase 22
204
- // event emission (Plan 27-08) hooks here as `peer_call_failed`
205
- // with reason="registry_missing". For now, a placeholder stderr
206
- // breadcrumb so operators can grep for delegation drops without
207
- // CI-failing on stdout pollution.
208
- _logPeerCallFailed({ peer: parsed.peer, role, errorClass: 'registry_missing' });
203
+ // No registry available at all — fall through to local.
204
+ _logPeerCallFailed({
205
+ peer: parsed.peer, role, errorClass: 'registry_missing',
206
+ sessionId: args.sessionId, stage: opts.stage,
207
+ });
209
208
  return null;
210
209
  }
211
210
 
211
+ // v1.27.1 — emit peer_call_started before dispatcher invocation so the
212
+ // events.jsonl trail captures the attempt even if the dispatcher hangs.
213
+ _logPeerCallStarted({
214
+ peer: parsed.peer, role,
215
+ sessionId: args.sessionId, stage: opts.stage,
216
+ });
217
+ const dispatchStartedAt = Date.now();
218
+
212
219
  let dispatchResult: { result: unknown; peer: string; protocol: 'acp' | 'asp' } | null = null;
213
220
  try {
214
221
  dispatchResult = await dispatcher(role, tier, sanitizedPrompt, { cwd: process.cwd() });
@@ -218,6 +225,8 @@ async function tryDelegate(args: {
218
225
  role,
219
226
  errorClass: 'dispatch_threw',
220
227
  message: err instanceof Error ? err.message : String(err),
228
+ sessionId: args.sessionId,
229
+ stage: opts.stage,
221
230
  });
222
231
  return null; // transparent fallback
223
232
  }
@@ -225,10 +234,28 @@ async function tryDelegate(args: {
225
234
  if (dispatchResult === null) {
226
235
  // Registry returned null — peer absent, capability mismatch, or
227
236
  // adapter-side error. Per D-07 we fall back silently.
228
- _logPeerCallFailed({ peer: parsed.peer, role, errorClass: 'registry_returned_null' });
237
+ _logPeerCallFailed({
238
+ peer: parsed.peer, role, errorClass: 'registry_returned_null',
239
+ sessionId: args.sessionId, stage: opts.stage,
240
+ });
229
241
  return null;
230
242
  }
231
243
 
244
+ // v1.27.1 — peer round-trip succeeded. Emit peer_call_complete with the
245
+ // measured latency. Token counts + cost are 0 / null because adapters
246
+ // don't surface usage in v1.27 (Plan 27-04 spec deferred it); reflector
247
+ // tolerates null cost (Plan 26-06 cost-arbitrage analysis).
248
+ _logPeerCallComplete({
249
+ peer: dispatchResult.peer,
250
+ role,
251
+ latencyMs: Date.now() - dispatchStartedAt,
252
+ tokensIn: 0,
253
+ tokensOut: 0,
254
+ costUsd: null,
255
+ sessionId: args.sessionId,
256
+ stage: opts.stage,
257
+ });
258
+
232
259
  // Peer succeeded. Build a SessionResult that mirrors the local path's
233
260
  // shape so downstream consumers (stage-handlers, transcript readers,
234
261
  // tests) treat both paths uniformly. We do NOT write a transcript file
@@ -271,34 +298,122 @@ function _coerceFinalText(result: unknown): string | undefined {
271
298
  }
272
299
 
273
300
  /**
274
- * Placeholder for Plan 27-08's `peer_call_failed` event. Until 27-08
275
- * wires real `appendEvent('peer_call_failed', ...)`, we write a single
276
- * stderr line so operators can grep for silent delegation drops. We
277
- * deliberately don't go through `appendEvent` here because the Phase 22
278
- * event-stream hasn't gained a `peer_call_failed` type yet (that's
279
- * 27-08's job) and pushing an unknown event type today would create a
280
- * migration mess for the reflector.
301
+ * v1.27.1 wires Plan 27-08's `peer_call_failed` event for real.
302
+ * Phase 22 `appendEvent` accepts the new event type (registered in
303
+ * KNOWN_EVENT_TYPES via Plan 27-08), so the reflector and downstream
304
+ * telemetry consumers see delegation drops as a measurement signal.
305
+ *
306
+ * Errors from `appendEvent` (e.g., events.jsonl unwritable) are
307
+ * swallowed peer-call telemetry is observability, not critical
308
+ * path. STATE.md remains the durable record of session outcomes.
309
+ *
310
+ * Operators can additionally set `GDD_PEER_DEBUG=1` to emit a
311
+ * one-line stderr breadcrumb mirroring the event for live tailing.
281
312
  */
282
313
  function _logPeerCallFailed(args: {
283
314
  peer: string;
284
315
  role: string;
285
316
  errorClass: string;
286
317
  message?: string;
318
+ sessionId?: string;
319
+ stage?: SessionRunnerOptions['stage'];
287
320
  }): void {
288
- // One-line, machine-greppable. Quiet by default in test runs (NODE_ENV)
289
- // so the test output stays clean. Operators set GDD_PEER_DEBUG=1 to see
290
- // the breadcrumb in production logs.
291
- if (process.env['GDD_PEER_DEBUG'] !== '1') return;
292
- const payload = JSON.stringify({
293
- type: 'peer_call_failed',
294
- peer_id: args.peer,
295
- role: args.role,
296
- error_class: args.errorClass,
297
- ...(args.message !== undefined ? { message: args.message } : {}),
298
- ts: new Date().toISOString(),
299
- });
300
- // eslint-disable-next-line no-console
301
- console.error(`[peer-cli] ${payload}`);
321
+ try {
322
+ appendEvent({
323
+ type: 'peer_call_failed',
324
+ timestamp: new Date().toISOString(),
325
+ sessionId: args.sessionId ?? 'unknown',
326
+ ...(args.stage !== undefined && args.stage !== 'init' && args.stage !== 'custom' ? { stage: args.stage } : {}),
327
+ payload: {
328
+ runtime_role: 'peer',
329
+ peer_id: args.peer,
330
+ role: args.role,
331
+ error_class: args.errorClass,
332
+ ...(args.message !== undefined ? { message: args.message } : {}),
333
+ },
334
+ });
335
+ } catch {
336
+ // Telemetry is best-effort — never let an event-stream failure
337
+ // break the actual session flow.
338
+ }
339
+ if (process.env['GDD_PEER_DEBUG'] === '1') {
340
+ const payload = JSON.stringify({
341
+ type: 'peer_call_failed',
342
+ peer_id: args.peer,
343
+ role: args.role,
344
+ error_class: args.errorClass,
345
+ ...(args.message !== undefined ? { message: args.message } : {}),
346
+ ts: new Date().toISOString(),
347
+ });
348
+ // eslint-disable-next-line no-console
349
+ console.error(`[peer-cli] ${payload}`);
350
+ }
351
+ }
352
+
353
+ /**
354
+ * v1.27.1 — emit `peer_call_started` event. Fired once at the beginning
355
+ * of a delegation attempt, before the dispatcher is invoked. Pairs with
356
+ * `peer_call_complete` (success path) or `peer_call_failed` (any failure
357
+ * path, transparent to caller per D-07).
358
+ */
359
+ function _logPeerCallStarted(args: {
360
+ peer: string;
361
+ role: string;
362
+ sessionId?: string;
363
+ stage?: SessionRunnerOptions['stage'];
364
+ }): void {
365
+ try {
366
+ appendEvent({
367
+ type: 'peer_call_started',
368
+ timestamp: new Date().toISOString(),
369
+ sessionId: args.sessionId ?? 'unknown',
370
+ ...(args.stage !== undefined && args.stage !== 'init' && args.stage !== 'custom' ? { stage: args.stage } : {}),
371
+ payload: {
372
+ runtime_role: 'peer',
373
+ peer_id: args.peer,
374
+ role: args.role,
375
+ },
376
+ });
377
+ } catch {
378
+ // best-effort
379
+ }
380
+ }
381
+
382
+ /**
383
+ * v1.27.1 — emit `peer_call_complete` event. Fired after a successful
384
+ * dispatcher round-trip. Cost is null when the adapter doesn't return
385
+ * usage data (some peers don't surface token counts); the reflector
386
+ * tolerates null cost for arbitrage analysis (Plan 26-06).
387
+ */
388
+ function _logPeerCallComplete(args: {
389
+ peer: string;
390
+ role: string;
391
+ latencyMs: number;
392
+ tokensIn: number;
393
+ tokensOut: number;
394
+ costUsd: number | null;
395
+ sessionId?: string;
396
+ stage?: SessionRunnerOptions['stage'];
397
+ }): void {
398
+ try {
399
+ appendEvent({
400
+ type: 'peer_call_complete',
401
+ timestamp: new Date().toISOString(),
402
+ sessionId: args.sessionId ?? 'unknown',
403
+ ...(args.stage !== undefined && args.stage !== 'init' && args.stage !== 'custom' ? { stage: args.stage } : {}),
404
+ payload: {
405
+ runtime_role: 'peer',
406
+ peer_id: args.peer,
407
+ role: args.role,
408
+ latency_ms: args.latencyMs,
409
+ tokens_in: args.tokensIn,
410
+ tokens_out: args.tokensOut,
411
+ cost_usd: args.costUsd,
412
+ },
413
+ });
414
+ } catch {
415
+ // best-effort
416
+ }
302
417
  }
303
418
 
304
419
  /** Baseline retry backoff parameters (matches jittered-backoff defaults for
@@ -623,6 +738,38 @@ export async function run(opts: SessionRunnerOptions): Promise<SessionResult> {
623
738
  }
624
739
  }
625
740
 
741
+ // -- 6.5. Peer-CLI delegation try (Plan 27-06 wiring, v1.27.1). ---------
742
+ // If the agent's frontmatter declares `delegate_to: <peer>-<role>` AND the
743
+ // peer is allowlisted AND the registry can route, run the prompt on the
744
+ // peer-CLI and return early. On peer-absent / peer-error / null result,
745
+ // fall through transparently to the local SDK loop (D-07).
746
+ //
747
+ // tryDelegate is a no-op when opts.delegateTo is undefined / 'none', when
748
+ // the registry can't load, when the peer isn't allowlisted, when the
749
+ // dispatcher returns null, or when the dispatcher throws. In all those
750
+ // cases tryDelegate returns null and we proceed to the local SDK path.
751
+ const peerResult = await tryDelegate({
752
+ opts,
753
+ sanitizedPrompt,
754
+ transcriptPath,
755
+ sessionId,
756
+ sanitizer: sanResult,
757
+ });
758
+ if (peerResult !== null) {
759
+ emit('session.completed', opts.stage, sessionId, {
760
+ stage: opts.stage,
761
+ sessionId,
762
+ status: peerResult.status,
763
+ turns: peerResult.turns,
764
+ usage: peerResult.usage,
765
+ transcript_path: transcriptPath,
766
+ sanitizer: { applied: [...peerResult.sanitizer.applied], removedSections: [...peerResult.sanitizer.removedSections] },
767
+ });
768
+ transcript.close();
769
+ if (opts.signal !== undefined) opts.signal.removeEventListener('abort', onExternalAbort);
770
+ return peerResult;
771
+ }
772
+
626
773
  // -- 7. Retry-once loop. ------------------------------------------------
627
774
  const maxAttempts = opts.maxRetries !== undefined && opts.maxRetries > 0
628
775
  ? opts.maxRetries