@hegemonart/get-design-done 1.26.0 → 1.27.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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +50 -0
- package/README.md +10 -8
- package/SKILL.md +3 -0
- package/agents/README.md +29 -0
- package/package.json +1 -1
- package/reference/peer-cli-capabilities.md +151 -0
- package/reference/peer-protocols.md +266 -0
- package/reference/registry.json +14 -0
- package/reference/runtime-models.md +3 -3
- package/scripts/lib/bandit-router.cjs +214 -7
- package/scripts/lib/budget-enforcer.cjs +69 -1
- package/scripts/lib/event-stream/index.ts +14 -1
- package/scripts/lib/event-stream/types.ts +125 -1
- package/scripts/lib/install/runtimes.cjs +58 -0
- package/scripts/lib/peer-cli/acp-client.cjs +375 -0
- package/scripts/lib/peer-cli/adapters/codex.cjs +101 -0
- package/scripts/lib/peer-cli/adapters/copilot.cjs +79 -0
- package/scripts/lib/peer-cli/adapters/cursor.cjs +78 -0
- package/scripts/lib/peer-cli/adapters/gemini.cjs +81 -0
- package/scripts/lib/peer-cli/adapters/qwen.cjs +72 -0
- package/scripts/lib/peer-cli/asp-client.cjs +587 -0
- package/scripts/lib/peer-cli/broker-lifecycle.cjs +406 -0
- package/scripts/lib/peer-cli/registry.cjs +434 -0
- package/scripts/lib/peer-cli/spawn-cmd.cjs +149 -0
- package/scripts/lib/runtime-detect.cjs +1 -1
- package/scripts/lib/session-runner/index.ts +215 -0
- package/scripts/lib/session-runner/types.ts +60 -0
- package/scripts/validate-frontmatter.ts +159 -1
- package/skills/peer-cli-add/SKILL.md +170 -0
- package/skills/peer-cli-customize/SKILL.md +110 -0
- package/skills/peers/SKILL.md +101 -0
package/reference/registry.json
CHANGED
|
@@ -45,6 +45,13 @@
|
|
|
45
45
|
"phase": 19.6,
|
|
46
46
|
"description": "3-invariant framework (body/attention/memory) with grep-able principle→code pairs and reducibility test; wired into design-discussant brief stage"
|
|
47
47
|
},
|
|
48
|
+
{
|
|
49
|
+
"name": "peer-protocols",
|
|
50
|
+
"path": "reference/peer-protocols.md",
|
|
51
|
+
"type": "meta-rules",
|
|
52
|
+
"phase": 27,
|
|
53
|
+
"description": "Phase 27 ACP + ASP protocol cheat sheet for peer-CLI delegation — line-delimited JSON-RPC framing, initialize/prompt/threadStart/turn lifecycle, per-peer ACP entry points, error-path resolution semantics"
|
|
54
|
+
},
|
|
48
55
|
{
|
|
49
56
|
"name": "DEPRECATIONS",
|
|
50
57
|
"path": "reference/DEPRECATIONS.md",
|
|
@@ -638,6 +645,13 @@
|
|
|
638
645
|
"type": "heuristic",
|
|
639
646
|
"description": "Rules for serial/parallel agent dispatch and Touches conflict detection"
|
|
640
647
|
},
|
|
648
|
+
{
|
|
649
|
+
"name": "peer-cli-capabilities",
|
|
650
|
+
"path": "reference/peer-cli-capabilities.md",
|
|
651
|
+
"type": "meta-rules",
|
|
652
|
+
"phase": 27,
|
|
653
|
+
"description": "Phase 27 peer-CLI delegation capability matrix — which peer (codex/copilot/cursor/gemini/qwen) claims which agent role, protocol (ACP/ASP), tie-break order, and opt-in gating semantics"
|
|
654
|
+
},
|
|
641
655
|
{
|
|
642
656
|
"name": "performance",
|
|
643
657
|
"path": "reference/performance.md",
|
|
@@ -8,7 +8,7 @@ This file is parsed by `scripts/lib/install/parse-runtime-models.cjs` and consum
|
|
|
8
8
|
- `scripts/lib/install/installer.cjs` (26-03) — emits `models.json` per runtime config-dir at install time.
|
|
9
9
|
- `hooks/budget-enforcer.ts` + `scripts/lib/budget-enforcer.cjs` (26-05) — concrete model name for cost lookup.
|
|
10
10
|
|
|
11
|
-
**Strict schema** (D-03): each runtime block is a fenced
|
|
11
|
+
**Strict schema** (D-03): each runtime block is a fenced `json` code block validated against `reference/schemas/runtime-models.schema.json`. Schema version is locked at `1` until a breaking change forces a version bump.
|
|
12
12
|
|
|
13
13
|
**Provenance discipline** (D-01): every row carries a `source_url` (runtime-author docs), `retrieved_at` (ISO timestamp), and `last_validated_cycle` (current GDD cycle ID). Placeholder URLs are tagged `<TODO: confirm at <runtime-author-docs-url>>` and validated by Phase 13.2 authority-watcher on later cycles.
|
|
14
14
|
|
|
@@ -118,7 +118,7 @@ Google's Gemini CLI runtime. Public tier docs at https://ai.google.dev/gemini-ap
|
|
|
118
118
|
|
|
119
119
|
## qwen — Qwen Code
|
|
120
120
|
|
|
121
|
-
Alibaba's Qwen Code runtime. Public tier docs at https://
|
|
121
|
+
Alibaba's Qwen Code runtime. Public tier docs at https://github.com/QwenLM/qwen-code. Seed picks per CONTEXT.md D-02.
|
|
122
122
|
|
|
123
123
|
```json
|
|
124
124
|
{
|
|
@@ -135,7 +135,7 @@ Alibaba's Qwen Code runtime. Public tier docs at https://qwenlm.github.io/qwen-c
|
|
|
135
135
|
},
|
|
136
136
|
"provenance": [
|
|
137
137
|
{
|
|
138
|
-
"source_url": "https://
|
|
138
|
+
"source_url": "https://github.com/QwenLM/qwen-code",
|
|
139
139
|
"retrieved_at": "2026-04-29T00:00:00.000Z",
|
|
140
140
|
"last_validated_cycle": "2026-04-29-v1.26",
|
|
141
141
|
"note": "Qwen Code public model catalog."
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* bandit-router.cjs — contextual Thompson-sampling bandit over
|
|
3
|
-
* (agent_type, touches_size_bin) → {haiku, sonnet, opus}
|
|
3
|
+
* (agent_type, touches_size_bin[, delegate]) → {haiku, sonnet, opus}
|
|
4
|
+
* (Plan 23.5-01 + Plan 27-07 delegate dimension).
|
|
4
5
|
*
|
|
5
6
|
* Replaces Phase 10.1's static tier_overrides map when the user opts
|
|
6
7
|
* into adaptive_mode = "full". The static map continues to apply when
|
|
@@ -10,12 +11,25 @@
|
|
|
10
11
|
* .design/telemetry/posterior.json
|
|
11
12
|
* { schema_version: '1.0.0',
|
|
12
13
|
* generated_at: ISO,
|
|
13
|
-
* arms: [{agent, bin, tier, alpha, beta, last_used, count}] }
|
|
14
|
+
* arms: [{agent, bin, tier, delegate?, alpha, beta, last_used, count}] }
|
|
15
|
+
*
|
|
16
|
+
* The `delegate` field on an arm is OPTIONAL (Plan 27-07 / D-08). Existing
|
|
17
|
+
* callers that pass only `(agent, bin)` continue to read/write arms with
|
|
18
|
+
* `delegate === undefined`, which behaves identically to delegate='none'
|
|
19
|
+
* (i.e., the local-call slice). New callers can opt into the delegate
|
|
20
|
+
* dimension via `pullWithDelegate()` / `updateWithDelegate()` which
|
|
21
|
+
* persist `delegate ∈ {none, gemini, codex, cursor, copilot, qwen}`.
|
|
22
|
+
*
|
|
23
|
+
* Bootstrap discipline (D-08):
|
|
24
|
+
* - delegate='none' arms inherit Phase 23.5's TIER_PRIOR (informed).
|
|
25
|
+
* - delegate ∈ {gemini, codex, cursor, copilot, qwen} arms start
|
|
26
|
+
* neutral — the same TIER_PRIOR shape, on the assumption that we
|
|
27
|
+
* have no prior to favour any delegate over local; data drives.
|
|
14
28
|
*
|
|
15
29
|
* Atomic .tmp + rename. Discounted Thompson via per-arm time-decay
|
|
16
30
|
* factor `rho^days_since_last_use` applied at sample time, not stored.
|
|
17
31
|
*
|
|
18
|
-
* Reward computation (D-06): two-stage lexicographic
|
|
32
|
+
* Reward computation (D-06): two-stage lexicographic — UNCHANGED.
|
|
19
33
|
* if !solidify_pass: reward = 0
|
|
20
34
|
* elif user_undo_in_session: reward = 0
|
|
21
35
|
* else: reward = 1 - lambda * normalize(cost + epsilon * wall_time)
|
|
@@ -45,11 +59,25 @@ const TIER_PRIOR = Object.freeze({
|
|
|
45
59
|
const PRIOR_STRENGTH = 10;
|
|
46
60
|
const DEFAULT_TIERS = Object.freeze(['haiku', 'sonnet', 'opus']);
|
|
47
61
|
|
|
62
|
+
// Plan 27-07 / D-08. Delegate context dimension. 'none' = local Anthropic
|
|
63
|
+
// call; the other 5 are peer-CLI delegations via ACP/ASP. Adding this as
|
|
64
|
+
// a third context dimension expands the arm space 6× (78 → ~468 contexts).
|
|
65
|
+
const DELEGATE_NONE = 'none';
|
|
66
|
+
const DEFAULT_DELEGATES = Object.freeze([
|
|
67
|
+
DELEGATE_NONE,
|
|
68
|
+
'gemini',
|
|
69
|
+
'codex',
|
|
70
|
+
'cursor',
|
|
71
|
+
'copilot',
|
|
72
|
+
'qwen',
|
|
73
|
+
]);
|
|
74
|
+
|
|
48
75
|
const DEFAULT_PRIORS = Object.freeze({
|
|
49
76
|
decay: DEFAULT_DECAY,
|
|
50
77
|
strength: PRIOR_STRENGTH,
|
|
51
78
|
tiers: DEFAULT_TIERS,
|
|
52
79
|
perTier: TIER_PRIOR,
|
|
80
|
+
delegates: DEFAULT_DELEGATES,
|
|
53
81
|
});
|
|
54
82
|
|
|
55
83
|
const TOUCHES_BINS = Object.freeze([
|
|
@@ -141,12 +169,47 @@ function priorFor(tier, strength) {
|
|
|
141
169
|
};
|
|
142
170
|
}
|
|
143
171
|
|
|
144
|
-
|
|
145
|
-
|
|
172
|
+
/**
|
|
173
|
+
* @param {object[]} arms
|
|
174
|
+
* @param {string} agent
|
|
175
|
+
* @param {string} bin
|
|
176
|
+
* @param {string} tier
|
|
177
|
+
* @param {string} [delegate] — when provided, match arms with that
|
|
178
|
+
* delegate label. When omitted, match arms with no delegate field
|
|
179
|
+
* (legacy Phase 23.5 slice — equivalent to delegate='none' for
|
|
180
|
+
* bootstrap purposes but persisted distinctly to preserve
|
|
181
|
+
* round-trippability of existing posterior files).
|
|
182
|
+
*/
|
|
183
|
+
function findArm(arms, agent, bin, tier, delegate) {
|
|
184
|
+
if (delegate === undefined) {
|
|
185
|
+
return arms.find(
|
|
186
|
+
(a) =>
|
|
187
|
+
a.agent === agent &&
|
|
188
|
+
a.bin === bin &&
|
|
189
|
+
a.tier === tier &&
|
|
190
|
+
a.delegate === undefined,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
return arms.find(
|
|
194
|
+
(a) =>
|
|
195
|
+
a.agent === agent &&
|
|
196
|
+
a.bin === bin &&
|
|
197
|
+
a.tier === tier &&
|
|
198
|
+
a.delegate === delegate,
|
|
199
|
+
);
|
|
146
200
|
}
|
|
147
201
|
|
|
148
|
-
|
|
149
|
-
|
|
202
|
+
/**
|
|
203
|
+
* Ensure an arm exists, creating it with the informed prior when missing.
|
|
204
|
+
*
|
|
205
|
+
* For Plan 27-07: when `delegate` is provided, the arm is persisted with
|
|
206
|
+
* that label. Bootstrap is identical for delegate='none' (inherits Phase
|
|
207
|
+
* 23.5 prior — no migration needed because the legacy slice and the
|
|
208
|
+
* 'none' slice are independent contexts) and for the 5 peer delegates
|
|
209
|
+
* (each starts neutral with the same TIER_PRIOR shape; data drives).
|
|
210
|
+
*/
|
|
211
|
+
function ensureArm(posterior, agent, bin, tier, strength, delegate) {
|
|
212
|
+
let arm = findArm(posterior.arms, agent, bin, tier, delegate);
|
|
150
213
|
if (arm) return arm;
|
|
151
214
|
const { alpha, beta } = priorFor(tier, strength);
|
|
152
215
|
arm = {
|
|
@@ -158,6 +221,9 @@ function ensureArm(posterior, agent, bin, tier, strength) {
|
|
|
158
221
|
last_used: null,
|
|
159
222
|
count: 0,
|
|
160
223
|
};
|
|
224
|
+
if (delegate !== undefined) {
|
|
225
|
+
arm.delegate = delegate;
|
|
226
|
+
}
|
|
161
227
|
posterior.arms.push(arm);
|
|
162
228
|
return arm;
|
|
163
229
|
}
|
|
@@ -310,6 +376,143 @@ function update(input) {
|
|
|
310
376
|
return { alpha: arm.alpha, beta: arm.beta, posteriorPath: p };
|
|
311
377
|
}
|
|
312
378
|
|
|
379
|
+
/**
|
|
380
|
+
* Pull an arm with the delegate context dimension (Plan 27-07 / D-08).
|
|
381
|
+
*
|
|
382
|
+
* Joint sample over `tiers × delegates` — i.e., 3 × 6 = 18 arms in the
|
|
383
|
+
* default case. Returns the (tier, delegate) pair with the highest
|
|
384
|
+
* sampled posterior. Bumps the chosen arm's last_used + count.
|
|
385
|
+
*
|
|
386
|
+
* Caller-restricted delegate set:
|
|
387
|
+
* - For `delegate_to: none` agents (Plan 27-06 frontmatter), the caller
|
|
388
|
+
* should pass `delegates: ['none']` to constrain sampling to the
|
|
389
|
+
* local-call slice — the bandit will not explore peer delegations.
|
|
390
|
+
* - For agents without `delegate_to` (default), the caller may either
|
|
391
|
+
* omit delegates (legacy `pull()` behaviour) or pass DEFAULT_DELEGATES
|
|
392
|
+
* to enable adaptive routing across the full 18-arm space.
|
|
393
|
+
*
|
|
394
|
+
* @param {{
|
|
395
|
+
* agent: string,
|
|
396
|
+
* bin: string,
|
|
397
|
+
* tiers?: string[],
|
|
398
|
+
* delegates?: string[],
|
|
399
|
+
* baseDir?: string,
|
|
400
|
+
* posteriorPath?: string,
|
|
401
|
+
* decay?: number,
|
|
402
|
+
* strength?: number,
|
|
403
|
+
* now?: Date,
|
|
404
|
+
* }} input
|
|
405
|
+
* @returns {{
|
|
406
|
+
* tier: string,
|
|
407
|
+
* delegate: string,
|
|
408
|
+
* samples: Record<string, Record<string, number>>,
|
|
409
|
+
* posteriorPath: string,
|
|
410
|
+
* }}
|
|
411
|
+
*/
|
|
412
|
+
function pullWithDelegate(input) {
|
|
413
|
+
if (!input || typeof input.agent !== 'string' || input.agent.length === 0) {
|
|
414
|
+
throw new TypeError('bandit-router.pullWithDelegate: agent (string) required');
|
|
415
|
+
}
|
|
416
|
+
if (typeof input.bin !== 'string' || input.bin.length === 0) {
|
|
417
|
+
throw new TypeError('bandit-router.pullWithDelegate: bin (string) required');
|
|
418
|
+
}
|
|
419
|
+
const tiers = input.tiers ?? DEFAULT_TIERS;
|
|
420
|
+
const delegates = input.delegates ?? DEFAULT_DELEGATES;
|
|
421
|
+
if (!Array.isArray(delegates) || delegates.length === 0) {
|
|
422
|
+
throw new TypeError(
|
|
423
|
+
'bandit-router.pullWithDelegate: delegates must be a non-empty array',
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
const strength = input.strength ?? PRIOR_STRENGTH;
|
|
427
|
+
const now = input.now ?? new Date();
|
|
428
|
+
|
|
429
|
+
const posterior = loadPosterior(input);
|
|
430
|
+
/** @type {Record<string, Record<string, number>>} */
|
|
431
|
+
const samples = {};
|
|
432
|
+
let bestTier = tiers[0];
|
|
433
|
+
let bestDelegate = delegates[0];
|
|
434
|
+
let bestSample = -1;
|
|
435
|
+
for (const delegate of delegates) {
|
|
436
|
+
samples[delegate] = {};
|
|
437
|
+
for (const tier of tiers) {
|
|
438
|
+
const arm = ensureArm(posterior, input.agent, input.bin, tier, strength, delegate);
|
|
439
|
+
const decayed = decayArm(arm, { decay: input.decay, now, strength });
|
|
440
|
+
const s = sampleBeta(decayed.alpha, decayed.beta);
|
|
441
|
+
samples[delegate][tier] = s;
|
|
442
|
+
if (s > bestSample) {
|
|
443
|
+
bestSample = s;
|
|
444
|
+
bestTier = tier;
|
|
445
|
+
bestDelegate = delegate;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
const chosen = ensureArm(
|
|
450
|
+
posterior,
|
|
451
|
+
input.agent,
|
|
452
|
+
input.bin,
|
|
453
|
+
bestTier,
|
|
454
|
+
strength,
|
|
455
|
+
bestDelegate,
|
|
456
|
+
);
|
|
457
|
+
chosen.last_used = now.toISOString();
|
|
458
|
+
chosen.count += 1;
|
|
459
|
+
const written = savePosterior(posterior, input);
|
|
460
|
+
return {
|
|
461
|
+
tier: bestTier,
|
|
462
|
+
delegate: bestDelegate,
|
|
463
|
+
samples,
|
|
464
|
+
posteriorPath: written,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Update the posterior with a reward signal — delegate-aware variant.
|
|
470
|
+
*
|
|
471
|
+
* Reward signal is UNCHANGED from Phase 23.5 (D-08): two-stage
|
|
472
|
+
* lexicographic via `computeReward()` — correctness first, cost as
|
|
473
|
+
* tiebreaker. The delegate dimension is applied at the arm-locator
|
|
474
|
+
* level, not the reward computation.
|
|
475
|
+
*
|
|
476
|
+
* @param {{
|
|
477
|
+
* agent: string,
|
|
478
|
+
* bin: string,
|
|
479
|
+
* tier: string,
|
|
480
|
+
* delegate: string,
|
|
481
|
+
* reward: number,
|
|
482
|
+
* baseDir?: string,
|
|
483
|
+
* posteriorPath?: string,
|
|
484
|
+
* strength?: number,
|
|
485
|
+
* }} input
|
|
486
|
+
* @returns {{alpha: number, beta: number, posteriorPath: string}}
|
|
487
|
+
*/
|
|
488
|
+
function updateWithDelegate(input) {
|
|
489
|
+
if (!input) throw new TypeError('bandit-router.updateWithDelegate: input required');
|
|
490
|
+
for (const k of ['agent', 'bin', 'tier', 'delegate']) {
|
|
491
|
+
if (typeof input[k] !== 'string' || input[k].length === 0) {
|
|
492
|
+
throw new TypeError(
|
|
493
|
+
`bandit-router.updateWithDelegate: ${k} (string) required`,
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (typeof input.reward !== 'number' || Number.isNaN(input.reward)) {
|
|
498
|
+
throw new TypeError('bandit-router.updateWithDelegate: reward (number) required');
|
|
499
|
+
}
|
|
500
|
+
const r = Math.min(1, Math.max(0, input.reward));
|
|
501
|
+
const posterior = loadPosterior(input);
|
|
502
|
+
const arm = ensureArm(
|
|
503
|
+
posterior,
|
|
504
|
+
input.agent,
|
|
505
|
+
input.bin,
|
|
506
|
+
input.tier,
|
|
507
|
+
input.strength ?? PRIOR_STRENGTH,
|
|
508
|
+
input.delegate,
|
|
509
|
+
);
|
|
510
|
+
arm.alpha += r;
|
|
511
|
+
arm.beta += 1 - r;
|
|
512
|
+
const p = savePosterior(posterior, input);
|
|
513
|
+
return { alpha: arm.alpha, beta: arm.beta, posteriorPath: p };
|
|
514
|
+
}
|
|
515
|
+
|
|
313
516
|
/**
|
|
314
517
|
* Two-stage lexicographic reward (D-06).
|
|
315
518
|
*
|
|
@@ -350,6 +553,8 @@ function computeReward(input) {
|
|
|
350
553
|
module.exports = {
|
|
351
554
|
pull,
|
|
352
555
|
update,
|
|
556
|
+
pullWithDelegate,
|
|
557
|
+
updateWithDelegate,
|
|
353
558
|
reset,
|
|
354
559
|
loadPosterior,
|
|
355
560
|
savePosterior,
|
|
@@ -360,6 +565,8 @@ module.exports = {
|
|
|
360
565
|
priorFor,
|
|
361
566
|
DEFAULT_PRIORS,
|
|
362
567
|
DEFAULT_TIERS,
|
|
568
|
+
DEFAULT_DELEGATES,
|
|
569
|
+
DELEGATE_NONE,
|
|
363
570
|
TIER_PRIOR,
|
|
364
571
|
PRIOR_STRENGTH,
|
|
365
572
|
TOUCHES_BINS,
|
|
@@ -384,6 +384,15 @@ function computeCost(args, opts) {
|
|
|
384
384
|
* event-stream import); .cjs callers (non-CC mirrors) compose this with
|
|
385
385
|
* the JSONL line shape used in tier-resolver.cjs's `emitEvent()`.
|
|
386
386
|
*
|
|
387
|
+
* Phase 27 / Plan 27-08 (D-09) extension — additive: cost rows now
|
|
388
|
+
* optionally carry `runtime_role` ("host" | "peer", default "host" when
|
|
389
|
+
* absent for back-compat) and `peer_id` (set only when
|
|
390
|
+
* `runtime_role === "peer"`). Pre-Phase-27 callers that don't pass these
|
|
391
|
+
* fields get the legacy shape with `runtime_role: "host"` stamped — so
|
|
392
|
+
* the cost-aggregator + reflector cross-runtime arbitrage
|
|
393
|
+
* (`scripts/lib/cost-arbitrage.cjs`) never sees an absent role tag and
|
|
394
|
+
* mixed-role cycle history rolls up correctly without crashing.
|
|
395
|
+
*
|
|
387
396
|
* @param {object} args
|
|
388
397
|
* @param {string} args.runtime
|
|
389
398
|
* @param {string} args.agent
|
|
@@ -392,10 +401,18 @@ function computeCost(args, opts) {
|
|
|
392
401
|
* @param {number} args.tokens_in
|
|
393
402
|
* @param {number} args.tokens_out
|
|
394
403
|
* @param {number|null} args.cost_usd
|
|
404
|
+
* @param {('host'|'peer')} [args.runtime_role]
|
|
405
|
+
* Phase 27. Defaults to `"host"` when absent.
|
|
406
|
+
* @param {string|null} [args.peer_id]
|
|
407
|
+
* Phase 27. The peer-CLI ID when `runtime_role === "peer"` (e.g.
|
|
408
|
+
* `"gemini"`, `"codex"`). Omitted from output when absent or when
|
|
409
|
+
* `runtime_role === "host"`.
|
|
395
410
|
* @returns {object}
|
|
396
411
|
*/
|
|
397
412
|
function buildCostEventPayload(args) {
|
|
398
|
-
|
|
413
|
+
const role = args && args.runtime_role === 'peer' ? 'peer' : 'host';
|
|
414
|
+
/** @type {Record<string, unknown>} */
|
|
415
|
+
const out = {
|
|
399
416
|
runtime: args.runtime,
|
|
400
417
|
agent: args.agent,
|
|
401
418
|
model_id: args.model_id,
|
|
@@ -405,7 +422,54 @@ function buildCostEventPayload(args) {
|
|
|
405
422
|
cost_usd: typeof args.cost_usd === 'number' && Number.isFinite(args.cost_usd)
|
|
406
423
|
? args.cost_usd
|
|
407
424
|
: null,
|
|
425
|
+
runtime_role: role,
|
|
408
426
|
};
|
|
427
|
+
// peer_id is only meaningful when role === "peer"; we omit it for
|
|
428
|
+
// host rows to keep the legacy on-disk shape stable for cost-aggregator
|
|
429
|
+
// tests that snapshot the line content.
|
|
430
|
+
if (role === 'peer') {
|
|
431
|
+
const pid = args && typeof args.peer_id === 'string' && args.peer_id.length > 0
|
|
432
|
+
? args.peer_id
|
|
433
|
+
: null;
|
|
434
|
+
out.peer_id = pid;
|
|
435
|
+
}
|
|
436
|
+
return out;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Phase 27 / Plan 27-08 helper — derive `(runtime_role, peer_id)` from a
|
|
441
|
+
* router decision shape, mirroring `modelFromResolved`'s contract:
|
|
442
|
+
*
|
|
443
|
+
* - When the router decision carries `runtime_role: "peer"` AND
|
|
444
|
+
* `peer_id: "<non-empty-string>"`, return `{ role: 'peer', peer_id }`.
|
|
445
|
+
* - When the decision is absent, malformed, or not flagged as peer-mode,
|
|
446
|
+
* return `{ role: 'host', peer_id: null }`.
|
|
447
|
+
*
|
|
448
|
+
* Plan 27-06's session-runner sets `runtime_role` + `peer_id` on its
|
|
449
|
+
* router-decision payload before invoking the budget-enforcer hook; this
|
|
450
|
+
* helper is the pure backend lookup that the hook (and any non-CC
|
|
451
|
+
* cost-recorder mirror) calls to thread those values into the cost row.
|
|
452
|
+
*
|
|
453
|
+
* Defensive on every shape — never throws on null / wrong type. Returning
|
|
454
|
+
* `host` on every error path keeps the legacy back-compat default
|
|
455
|
+
* everywhere.
|
|
456
|
+
*
|
|
457
|
+
* @param {unknown} routerDecision
|
|
458
|
+
* @returns {{ role: 'host' | 'peer', peer_id: string | null }}
|
|
459
|
+
*/
|
|
460
|
+
function roleFromRouterDecision(routerDecision) {
|
|
461
|
+
if (!routerDecision || typeof routerDecision !== 'object') {
|
|
462
|
+
return { role: 'host', peer_id: null };
|
|
463
|
+
}
|
|
464
|
+
const role = routerDecision.runtime_role;
|
|
465
|
+
if (role !== 'peer') return { role: 'host', peer_id: null };
|
|
466
|
+
const pid = routerDecision.peer_id;
|
|
467
|
+
if (typeof pid !== 'string' || pid.length === 0) {
|
|
468
|
+
// peer-flagged but no peer_id is malformed — degrade to host to
|
|
469
|
+
// avoid emitting a half-tagged cost row.
|
|
470
|
+
return { role: 'host', peer_id: null };
|
|
471
|
+
}
|
|
472
|
+
return { role: 'peer', peer_id: pid };
|
|
409
473
|
}
|
|
410
474
|
|
|
411
475
|
/**
|
|
@@ -435,6 +499,10 @@ module.exports = {
|
|
|
435
499
|
computeCost,
|
|
436
500
|
buildCostEventPayload,
|
|
437
501
|
modelFromResolved,
|
|
502
|
+
// Plan 27-08 (D-09): runtime-role + peer-id derivation from router
|
|
503
|
+
// decision. Used by the .ts hook and any non-CC cost-recorder mirror to
|
|
504
|
+
// thread peer-delegation tags into cost.jsonl rows.
|
|
505
|
+
roleFromRouterDecision,
|
|
438
506
|
parsePriceTable,
|
|
439
507
|
loadPriceTable,
|
|
440
508
|
priceTablePath,
|
|
@@ -50,8 +50,21 @@ export type {
|
|
|
50
50
|
ToolCallCompletedEvent,
|
|
51
51
|
AgentSpawnEvent,
|
|
52
52
|
AgentOutcomeEvent,
|
|
53
|
+
// Phase 27 / Plan 27-08 — peer-CLI delegation events (D-09).
|
|
54
|
+
RuntimeRole,
|
|
55
|
+
PeerCallStartedEvent,
|
|
56
|
+
PeerCallCompleteEvent,
|
|
57
|
+
PeerCallFailedEvent,
|
|
58
|
+
} from './types.ts';
|
|
59
|
+
export {
|
|
60
|
+
KNOWN_EVENT_TYPES,
|
|
61
|
+
// Phase 27 / Plan 27-08 — symbolic constants for peer-CLI event names.
|
|
62
|
+
PEER_CALL_STARTED,
|
|
63
|
+
PEER_CALL_COMPLETE,
|
|
64
|
+
PEER_CALL_FAILED,
|
|
65
|
+
PEER_CALL_EVENT_TYPES,
|
|
66
|
+
DEFAULT_RUNTIME_ROLE,
|
|
53
67
|
} from './types.ts';
|
|
54
|
-
export { KNOWN_EVENT_TYPES } from './types.ts';
|
|
55
68
|
export { EventBus } from './emitter.ts';
|
|
56
69
|
export type { EventHandler, Unsubscribe } from './emitter.ts';
|
|
57
70
|
export { EventWriter, DEFAULT_EVENTS_PATH, DEFAULT_MAX_LINE_BYTES } from './writer.ts';
|
|
@@ -218,6 +218,87 @@ export type AgentOutcomeEvent = BaseEvent & {
|
|
|
218
218
|
};
|
|
219
219
|
};
|
|
220
220
|
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
// Phase 27 — peer-CLI delegation lifecycle (Plan 27-08, D-09)
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
//
|
|
225
|
+
// Additive extension. Every event keeps existing fields. Peer-call events
|
|
226
|
+
// gain two payload tags:
|
|
227
|
+
//
|
|
228
|
+
// * `runtime_role: "host" | "peer"` — defaults to `"host"` if absent at
|
|
229
|
+
// read time (so all pre-Phase-27 events continue to read as host-mode).
|
|
230
|
+
// Only the three `peer_call_*` events below MUST carry it as `"peer"`.
|
|
231
|
+
// * `peer_id` — the peer-CLI ID (`"gemini"`, `"codex"`, `"cursor"`,
|
|
232
|
+
// `"copilot"`, `"qwen"`, …) — set when `runtime_role === "peer"`.
|
|
233
|
+
//
|
|
234
|
+
// `costs.jsonl` cost rows (`cost_recorded` / `cost.update`) gain the same
|
|
235
|
+
// two tags so Phase 26's reflector cross-runtime arbitrage continues to
|
|
236
|
+
// roll up correctly with mixed-role data. See
|
|
237
|
+
// `scripts/lib/budget-enforcer.cjs#buildCostEventPayload` for the cost-row
|
|
238
|
+
// extension.
|
|
239
|
+
//
|
|
240
|
+
// Plan 27-06 owns the actual emission call sites in session-runner; this
|
|
241
|
+
// file provides the shape + symbolic constants so 27-06 can import the
|
|
242
|
+
// type names without redefining them.
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Narrow union for the runtime-role tag. Pre-Phase-27 events do not carry
|
|
246
|
+
* this field; readers MUST default to `"host"` when absent.
|
|
247
|
+
*/
|
|
248
|
+
export type RuntimeRole = 'host' | 'peer';
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Emitted by session-runner (Plan 27-06) when a peer-CLI delegation is
|
|
252
|
+
* about to start. `latency_ms` is captured on the corresponding
|
|
253
|
+
* `peer_call_complete` / `peer_call_failed` event; this event marks the
|
|
254
|
+
* boundary so chain-walkers can pair started/complete via shared
|
|
255
|
+
* sessionId + peer_id + role.
|
|
256
|
+
*/
|
|
257
|
+
export type PeerCallStartedEvent = BaseEvent & {
|
|
258
|
+
type: 'peer_call_started';
|
|
259
|
+
payload: {
|
|
260
|
+
runtime_role: 'peer';
|
|
261
|
+
peer_id: string;
|
|
262
|
+
role: string;
|
|
263
|
+
};
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Emitted by session-runner on successful peer-CLI delegation.
|
|
268
|
+
* `cost_usd` is computed via the shared cost backend (Plan 26-05)
|
|
269
|
+
* extended with `runtime_role` + `peer_id` tags so the cost-aggregator
|
|
270
|
+
* rolls up peer spend correctly.
|
|
271
|
+
*/
|
|
272
|
+
export type PeerCallCompleteEvent = BaseEvent & {
|
|
273
|
+
type: 'peer_call_complete';
|
|
274
|
+
payload: {
|
|
275
|
+
runtime_role: 'peer';
|
|
276
|
+
peer_id: string;
|
|
277
|
+
role: string;
|
|
278
|
+
latency_ms: number;
|
|
279
|
+
tokens_in: number;
|
|
280
|
+
tokens_out: number;
|
|
281
|
+
cost_usd: number | null;
|
|
282
|
+
};
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Emitted by session-runner when peer-CLI delegation fails (peer-absent,
|
|
287
|
+
* peer-error, timeout). D-07: failure is transparent — session-runner
|
|
288
|
+
* falls back to the local Anthropic call — so this event is purely a
|
|
289
|
+
* measurement signal for the reflector. `error_class` mirrors
|
|
290
|
+
* Plan 20-04's `classify(err).kind`.
|
|
291
|
+
*/
|
|
292
|
+
export type PeerCallFailedEvent = BaseEvent & {
|
|
293
|
+
type: 'peer_call_failed';
|
|
294
|
+
payload: {
|
|
295
|
+
runtime_role: 'peer';
|
|
296
|
+
peer_id: string;
|
|
297
|
+
role: string;
|
|
298
|
+
error_class: string;
|
|
299
|
+
};
|
|
300
|
+
};
|
|
301
|
+
|
|
221
302
|
/**
|
|
222
303
|
* Union of all pre-registered event types. Not a closed enum at the
|
|
223
304
|
* envelope level — callers can emit unknown types — but downstream
|
|
@@ -247,7 +328,10 @@ export type KnownEvent =
|
|
|
247
328
|
| ToolCallStartedEvent
|
|
248
329
|
| ToolCallCompletedEvent
|
|
249
330
|
| AgentSpawnEvent
|
|
250
|
-
| AgentOutcomeEvent
|
|
331
|
+
| AgentOutcomeEvent
|
|
332
|
+
| PeerCallStartedEvent
|
|
333
|
+
| PeerCallCompleteEvent
|
|
334
|
+
| PeerCallFailedEvent;
|
|
251
335
|
|
|
252
336
|
/**
|
|
253
337
|
* Runtime list of all pre-registered event `type` strings. Used by the
|
|
@@ -278,4 +362,44 @@ export const KNOWN_EVENT_TYPES: readonly string[] = [
|
|
|
278
362
|
'tool_call.completed',
|
|
279
363
|
'agent.spawn',
|
|
280
364
|
'agent.outcome',
|
|
365
|
+
// Phase 27 / Plan 27-08 — peer-CLI delegation lifecycle (D-09).
|
|
366
|
+
'peer_call_started',
|
|
367
|
+
'peer_call_complete',
|
|
368
|
+
'peer_call_failed',
|
|
369
|
+
] as const;
|
|
370
|
+
|
|
371
|
+
// ---------------------------------------------------------------------------
|
|
372
|
+
// Phase 27 / Plan 27-08 — symbolic constants for peer-CLI event names.
|
|
373
|
+
// ---------------------------------------------------------------------------
|
|
374
|
+
//
|
|
375
|
+
// Plan 27-06 (session-runner) imports these by name rather than copying
|
|
376
|
+
// string literals so a downstream rename is a single-source-of-truth edit.
|
|
377
|
+
// All three are also present in `KNOWN_EVENT_TYPES` above for the registry
|
|
378
|
+
// test in `tests/event-types-registry.test.ts`.
|
|
379
|
+
|
|
380
|
+
/** Event type emitted when a peer-CLI delegation starts. See `PeerCallStartedEvent`. */
|
|
381
|
+
export const PEER_CALL_STARTED = 'peer_call_started' as const;
|
|
382
|
+
/** Event type emitted when a peer-CLI delegation succeeds. See `PeerCallCompleteEvent`. */
|
|
383
|
+
export const PEER_CALL_COMPLETE = 'peer_call_complete' as const;
|
|
384
|
+
/** Event type emitted when a peer-CLI delegation fails (D-07: transparent fallback). See `PeerCallFailedEvent`. */
|
|
385
|
+
export const PEER_CALL_FAILED = 'peer_call_failed' as const;
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Frozen set of all three peer-call event names. Convenient for
|
|
389
|
+
* downstream code that wants to gate "is this a peer-call event?" checks
|
|
390
|
+
* (e.g. the cost-aggregator's mixed-role roll-up).
|
|
391
|
+
*/
|
|
392
|
+
export const PEER_CALL_EVENT_TYPES: readonly string[] = [
|
|
393
|
+
PEER_CALL_STARTED,
|
|
394
|
+
PEER_CALL_COMPLETE,
|
|
395
|
+
PEER_CALL_FAILED,
|
|
281
396
|
] as const;
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Default runtime-role tag for events that pre-date Phase 27. Readers
|
|
400
|
+
* SHOULD substitute this when `payload.runtime_role` is absent so legacy
|
|
401
|
+
* event-stream consumers continue to read uniformly. Stamping at write
|
|
402
|
+
* time on every emission would be a much larger surface change — see
|
|
403
|
+
* Plan 27-08 deviation notes for rationale.
|
|
404
|
+
*/
|
|
405
|
+
export const DEFAULT_RUNTIME_ROLE: RuntimeRole = 'host';
|