@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
|
@@ -86,6 +86,221 @@ const RATE_GUARD_PROVIDER = 'anthropic';
|
|
|
86
86
|
/** Default retries (first attempt + 1 retry). */
|
|
87
87
|
const DEFAULT_MAX_RETRIES = 2;
|
|
88
88
|
|
|
89
|
+
// ── Plan 27-06 — Peer-CLI delegation primitives ─────────────────────────────
|
|
90
|
+
//
|
|
91
|
+
// Lazy registry loader: the registry is a .cjs module under scripts/lib/peer-cli
|
|
92
|
+
// landed by Plan 27-05. Tests inject a stub via SessionRunnerOptions.registryOverride;
|
|
93
|
+
// real callers fall through to the live module. Resolution is anchored to the
|
|
94
|
+
// repo root via the same `_nodeRequire` we use for jittered-backoff/rate-guard
|
|
95
|
+
// so the runner survives test sandboxes that chdir.
|
|
96
|
+
//
|
|
97
|
+
// We swallow load errors and return null → caller treats as "no peer available"
|
|
98
|
+
// → falls back to local SDK. This keeps the session-runner functional even on
|
|
99
|
+
// fresh checkouts where Plan 27-05 hasn't landed yet.
|
|
100
|
+
|
|
101
|
+
interface PeerRegistry {
|
|
102
|
+
dispatch: (
|
|
103
|
+
role: string,
|
|
104
|
+
tier: string | null,
|
|
105
|
+
text: string,
|
|
106
|
+
opts: { cwd?: string; [k: string]: unknown },
|
|
107
|
+
) => Promise<{ result: unknown; peer: string; protocol: 'acp' | 'asp' } | null>;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let _peerRegistryCache: PeerRegistry | null | undefined;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Resolve the peer-CLI registry. Memoized; returns null if the module
|
|
114
|
+
* isn't installable (missing file, require throws, shape mismatch).
|
|
115
|
+
* Tests bypass this entirely by passing `registryOverride` on the
|
|
116
|
+
* SessionRunnerOptions.
|
|
117
|
+
*/
|
|
118
|
+
function loadPeerRegistry(): PeerRegistry | null {
|
|
119
|
+
if (_peerRegistryCache !== undefined) return _peerRegistryCache;
|
|
120
|
+
try {
|
|
121
|
+
const mod = _nodeRequire(
|
|
122
|
+
_resolve(_REPO_ROOT, 'scripts/lib/peer-cli/registry.cjs'),
|
|
123
|
+
);
|
|
124
|
+
if (mod && typeof mod === 'object' && typeof (mod as { dispatch?: unknown }).dispatch === 'function') {
|
|
125
|
+
_peerRegistryCache = mod as PeerRegistry;
|
|
126
|
+
return _peerRegistryCache;
|
|
127
|
+
}
|
|
128
|
+
} catch {
|
|
129
|
+
// registry.cjs missing or threw on require — treat as "no peers available"
|
|
130
|
+
}
|
|
131
|
+
_peerRegistryCache = null;
|
|
132
|
+
return _peerRegistryCache;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Visible-for-testing reset of the peer-registry cache. The session-runner
|
|
137
|
+
* caches the registry module after first resolve so production runs don't
|
|
138
|
+
* re-require it per call; tests that swap process state (chdir into a
|
|
139
|
+
* sandbox, write a different registry.cjs, etc.) can call this between
|
|
140
|
+
* tests to force reload. Production code never calls this.
|
|
141
|
+
*/
|
|
142
|
+
export function _resetPeerRegistryCache(): void {
|
|
143
|
+
_peerRegistryCache = undefined;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Parse a `delegate_to` value into (peer, role). Returns null when the
|
|
148
|
+
* value is missing, the literal "none" opt-out, or doesn't match the
|
|
149
|
+
* `<peer>-<role>` shape. The session-runner uses this to figure out
|
|
150
|
+
* which role to ask the registry for.
|
|
151
|
+
*
|
|
152
|
+
* Note: validate-frontmatter.ts already enforces the value shape at lint
|
|
153
|
+
* time — by the time a `delegate_to` reaches session-runner it's been
|
|
154
|
+
* validated against the capability matrix. We re-parse here defensively
|
|
155
|
+
* because the runner is consumed by tests that may pass arbitrary
|
|
156
|
+
* strings, and the cost of an extra split is trivial.
|
|
157
|
+
*/
|
|
158
|
+
function parseDelegateTo(value: string | undefined): { peer: string; role: string } | null {
|
|
159
|
+
if (typeof value !== 'string' || value.length === 0) return null;
|
|
160
|
+
if (value === 'none') return null;
|
|
161
|
+
const dashIdx = value.indexOf('-');
|
|
162
|
+
if (dashIdx <= 0 || dashIdx >= value.length - 1) return null;
|
|
163
|
+
return {
|
|
164
|
+
peer: value.slice(0, dashIdx),
|
|
165
|
+
role: value.slice(dashIdx + 1),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Try to dispatch a session via peer-CLI before falling back to the
|
|
171
|
+
* Anthropic SDK. Returns either a fully-built SessionResult on peer
|
|
172
|
+
* success, or null when the caller should continue to the local path.
|
|
173
|
+
*
|
|
174
|
+
* Per CONTEXT D-07 (transparent fallback): every failure path inside
|
|
175
|
+
* this helper returns null — peer-absent, registry-load-failure,
|
|
176
|
+
* adapter-error, dispatch-throw, anything. The local SDK path then
|
|
177
|
+
* runs as if the delegation never happened. Failure is observable
|
|
178
|
+
* only as a placeholder log call (and, once Plan 27-08 wires real
|
|
179
|
+
* events, as a `peer_call_failed` chain entry).
|
|
180
|
+
*/
|
|
181
|
+
async function tryDelegate(args: {
|
|
182
|
+
opts: SessionRunnerOptions;
|
|
183
|
+
sanitizedPrompt: string;
|
|
184
|
+
transcriptPath: string;
|
|
185
|
+
sessionId: string;
|
|
186
|
+
sanitizer: { sanitized: string; applied: readonly string[]; removedSections: readonly string[] };
|
|
187
|
+
}): Promise<SessionResult | null> {
|
|
188
|
+
const { opts, sanitizedPrompt, transcriptPath, sanitizer } = args;
|
|
189
|
+
const parsed = parseDelegateTo(opts.delegateTo);
|
|
190
|
+
if (parsed === null) return null; // not configured / explicit opt-out
|
|
191
|
+
|
|
192
|
+
const role = typeof opts.delegateRole === 'string' && opts.delegateRole.length > 0
|
|
193
|
+
? opts.delegateRole
|
|
194
|
+
: parsed.role;
|
|
195
|
+
const tier = opts.delegateTier === undefined ? null : opts.delegateTier;
|
|
196
|
+
|
|
197
|
+
const dispatcher: PeerRegistry['dispatch'] | null = (() => {
|
|
198
|
+
if (typeof opts.registryOverride === 'function') return opts.registryOverride;
|
|
199
|
+
const reg = loadPeerRegistry();
|
|
200
|
+
return reg !== null ? reg.dispatch : null;
|
|
201
|
+
})();
|
|
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' });
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
let dispatchResult: { result: unknown; peer: string; protocol: 'acp' | 'asp' } | null = null;
|
|
213
|
+
try {
|
|
214
|
+
dispatchResult = await dispatcher(role, tier, sanitizedPrompt, { cwd: process.cwd() });
|
|
215
|
+
} catch (err) {
|
|
216
|
+
_logPeerCallFailed({
|
|
217
|
+
peer: parsed.peer,
|
|
218
|
+
role,
|
|
219
|
+
errorClass: 'dispatch_threw',
|
|
220
|
+
message: err instanceof Error ? err.message : String(err),
|
|
221
|
+
});
|
|
222
|
+
return null; // transparent fallback
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (dispatchResult === null) {
|
|
226
|
+
// Registry returned null — peer absent, capability mismatch, or
|
|
227
|
+
// adapter-side error. Per D-07 we fall back silently.
|
|
228
|
+
_logPeerCallFailed({ peer: parsed.peer, role, errorClass: 'registry_returned_null' });
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Peer succeeded. Build a SessionResult that mirrors the local path's
|
|
233
|
+
// shape so downstream consumers (stage-handlers, transcript readers,
|
|
234
|
+
// tests) treat both paths uniformly. We do NOT write a transcript file
|
|
235
|
+
// for delegated calls in v1.27.0 — the peer broker (Plan 27-03) keeps
|
|
236
|
+
// its own logs and Plan 27-08 wires the events that observers need.
|
|
237
|
+
// The transcript_path field still points at the would-be path so any
|
|
238
|
+
// consumer that probes it sees a stable string (existsSync will be
|
|
239
|
+
// false, which is correct: the file isn't ours to write).
|
|
240
|
+
const finalText = _coerceFinalText(dispatchResult.result);
|
|
241
|
+
return {
|
|
242
|
+
status: 'completed',
|
|
243
|
+
transcript_path: transcriptPath,
|
|
244
|
+
turns: 1,
|
|
245
|
+
usage: { input_tokens: 0, output_tokens: 0, usd_cost: 0 },
|
|
246
|
+
...(finalText !== undefined ? { final_text: finalText } : {}),
|
|
247
|
+
tool_calls: [],
|
|
248
|
+
sanitizer: {
|
|
249
|
+
applied: [...sanitizer.applied],
|
|
250
|
+
removedSections: [...sanitizer.removedSections],
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Best-effort extract a final text string from the adapter's free-form result. */
|
|
256
|
+
function _coerceFinalText(result: unknown): string | undefined {
|
|
257
|
+
if (typeof result === 'string' && result.length > 0) return result;
|
|
258
|
+
if (result !== null && typeof result === 'object') {
|
|
259
|
+
const obj = result as Record<string, unknown>;
|
|
260
|
+
if (typeof obj['final_text'] === 'string' && obj['final_text'].length > 0) {
|
|
261
|
+
return obj['final_text'] as string;
|
|
262
|
+
}
|
|
263
|
+
if (typeof obj['text'] === 'string' && obj['text'].length > 0) {
|
|
264
|
+
return obj['text'] as string;
|
|
265
|
+
}
|
|
266
|
+
if (typeof obj['output'] === 'string' && obj['output'].length > 0) {
|
|
267
|
+
return obj['output'] as string;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
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.
|
|
281
|
+
*/
|
|
282
|
+
function _logPeerCallFailed(args: {
|
|
283
|
+
peer: string;
|
|
284
|
+
role: string;
|
|
285
|
+
errorClass: string;
|
|
286
|
+
message?: string;
|
|
287
|
+
}): 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}`);
|
|
302
|
+
}
|
|
303
|
+
|
|
89
304
|
/** Baseline retry backoff parameters (matches jittered-backoff defaults for
|
|
90
305
|
* the SDK-retry case; 1s base → 30s cap). */
|
|
91
306
|
const RETRY_BACKOFF = { baseMs: 1000, maxMs: 30_000 } as const;
|
|
@@ -118,6 +118,66 @@ export interface SessionRunnerOptions {
|
|
|
118
118
|
applied: readonly string[];
|
|
119
119
|
removedSections: readonly string[];
|
|
120
120
|
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Phase 27 (Plan 27-06) — peer-CLI delegation.
|
|
124
|
+
*
|
|
125
|
+
* Optional. When set to `<peer>-<role>` (e.g. `gemini-research`), the
|
|
126
|
+
* session-runner attempts to dispatch the call to the named peer-CLI
|
|
127
|
+
* via `scripts/lib/peer-cli/registry.cjs#dispatch` BEFORE invoking the
|
|
128
|
+
* local Anthropic SDK. The peer's response, when successful, becomes
|
|
129
|
+
* the SessionResult — no SDK call is made.
|
|
130
|
+
*
|
|
131
|
+
* Fallback (CONTEXT D-07): if the registry returns `null` (peer
|
|
132
|
+
* absent / opt-out / adapter error / dispatch error) OR throws, the
|
|
133
|
+
* session-runner silently retries with the local Anthropic SDK. The
|
|
134
|
+
* caller never sees the peer failure — failure is a measurement
|
|
135
|
+
* signal, not a cycle-breaker.
|
|
136
|
+
*
|
|
137
|
+
* Special values:
|
|
138
|
+
* - `none` → explicit opt-out; never delegate. Same as omitting the field.
|
|
139
|
+
* - undefined → default behavior; never delegate.
|
|
140
|
+
*
|
|
141
|
+
* The session-runner never reads agent frontmatter on its own. Callers
|
|
142
|
+
* (pipeline-runner, explore, discuss, etc.) are responsible for
|
|
143
|
+
* resolving the agent's `delegate_to:` frontmatter and passing it
|
|
144
|
+
* through this option.
|
|
145
|
+
*/
|
|
146
|
+
delegateTo?: string;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Phase 27 (Plan 27-06) — role hint for peer-CLI dispatch.
|
|
150
|
+
*
|
|
151
|
+
* Used only when `delegateTo` is set. Defaults to the role parsed out
|
|
152
|
+
* of `delegateTo` (e.g. `delegateTo: "gemini-research"` → role
|
|
153
|
+
* `"research"`). Provide explicitly when the caller wants to override
|
|
154
|
+
* the parsed value (rare).
|
|
155
|
+
*/
|
|
156
|
+
delegateRole?: string;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Phase 27 (Plan 27-06) — tier hint for peer-CLI dispatch.
|
|
160
|
+
*
|
|
161
|
+
* Currently advisory; the registry's capability matrix doesn't gate
|
|
162
|
+
* on tier. Used by adapters for telemetry and by Plan 27-08 events.
|
|
163
|
+
* Defaults to null (let the adapter pick).
|
|
164
|
+
*/
|
|
165
|
+
delegateTier?: string | null;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Phase 27 (Plan 27-06) — registry override for tests.
|
|
169
|
+
*
|
|
170
|
+
* Default loads `scripts/lib/peer-cli/registry.cjs` lazily on first
|
|
171
|
+
* delegation attempt. Tests inject a stub `dispatch()` to avoid
|
|
172
|
+
* spawning real peers. The override mirrors the registry's `dispatch`
|
|
173
|
+
* signature: `(role, tier, text, opts) => Promise<{result,peer,protocol} | null>`.
|
|
174
|
+
*/
|
|
175
|
+
registryOverride?: (
|
|
176
|
+
role: string,
|
|
177
|
+
tier: string | null,
|
|
178
|
+
text: string,
|
|
179
|
+
opts: { cwd?: string; [k: string]: unknown },
|
|
180
|
+
) => Promise<{ result: unknown; peer: string; protocol: 'acp' | 'asp' } | null>;
|
|
121
181
|
}
|
|
122
182
|
|
|
123
183
|
/**
|
|
@@ -14,10 +14,100 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { existsSync, statSync, readdirSync } from 'node:fs';
|
|
17
|
-
import { join, basename } from 'node:path';
|
|
17
|
+
import { join, basename, dirname, resolve } from 'node:path';
|
|
18
|
+
import { createRequire } from 'node:module';
|
|
18
19
|
|
|
19
20
|
import { readFrontmatter } from '../tests/helpers.ts';
|
|
20
21
|
|
|
22
|
+
// ── delegate_to capability matrix loader (Plan 27-06) ──────────────────────
|
|
23
|
+
//
|
|
24
|
+
// The `delegate_to: <peer>-<role> | none` field is validated against the
|
|
25
|
+
// capability matrix exported by scripts/lib/peer-cli/registry.cjs (Plan
|
|
26
|
+
// 27-05). Loading the .cjs from a .ts module under the strip-types loader
|
|
27
|
+
// requires createRequire — and we anchor it to the repo root so the
|
|
28
|
+
// validator survives being invoked from any cwd.
|
|
29
|
+
//
|
|
30
|
+
// Loading is lazy + defensive: if the registry module isn't on disk yet
|
|
31
|
+
// (e.g. during a fresh clone before Plan 27-05 lands, or in a partial
|
|
32
|
+
// checkout), we fall back to an inline literal mirror of the locked D-05
|
|
33
|
+
// capability matrix so this validator never crashes a CI run on a
|
|
34
|
+
// missing dependency. The literal mirror MUST stay in sync with
|
|
35
|
+
// registry.cjs's CAPABILITY_MATRIX — tests assert equivalence.
|
|
36
|
+
function _findRepoRootFromHere(): string {
|
|
37
|
+
// process.argv[1] is the validator script path under strip-types; walk
|
|
38
|
+
// up from its directory looking for package.json. Fall back to cwd.
|
|
39
|
+
const start: string = (() => {
|
|
40
|
+
const argv1 = process.argv[1];
|
|
41
|
+
if (typeof argv1 === 'string' && argv1.length > 0) return dirname(argv1);
|
|
42
|
+
return process.cwd();
|
|
43
|
+
})();
|
|
44
|
+
let dir = resolve(start);
|
|
45
|
+
for (let i = 0; i < 8; i++) {
|
|
46
|
+
if (existsSync(join(dir, 'package.json'))) return dir;
|
|
47
|
+
const parent = dirname(dir);
|
|
48
|
+
if (parent === dir) break;
|
|
49
|
+
dir = parent;
|
|
50
|
+
}
|
|
51
|
+
return process.cwd();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Locked D-05 capability matrix mirror — fallback when registry.cjs unloadable. */
|
|
55
|
+
const _DELEGATE_MATRIX_FALLBACK: Readonly<Record<string, readonly string[]>> = Object.freeze({
|
|
56
|
+
codex: Object.freeze(['execute']),
|
|
57
|
+
copilot: Object.freeze(['review', 'research']),
|
|
58
|
+
cursor: Object.freeze(['debug', 'plan']),
|
|
59
|
+
gemini: Object.freeze(['research', 'exploration']),
|
|
60
|
+
qwen: Object.freeze(['write']),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
let _delegateMatrixCache: Readonly<Record<string, readonly string[]>> | null = null;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Return the live capability matrix as a `peer -> roles[]` map. Cached
|
|
67
|
+
* after first call. Uses registry.cjs as the source of truth; falls back
|
|
68
|
+
* to the inline mirror only if the registry module fails to load.
|
|
69
|
+
*/
|
|
70
|
+
export function loadDelegateMatrix(): Readonly<Record<string, readonly string[]>> {
|
|
71
|
+
if (_delegateMatrixCache !== null) return _delegateMatrixCache;
|
|
72
|
+
try {
|
|
73
|
+
const root = _findRepoRootFromHere();
|
|
74
|
+
const req = createRequire(join(root, 'package.json'));
|
|
75
|
+
const reg = req(resolve(root, 'scripts/lib/peer-cli/registry.cjs')) as {
|
|
76
|
+
CAPABILITY_MATRIX?: Record<string, { roles: readonly string[] }>;
|
|
77
|
+
};
|
|
78
|
+
if (reg && typeof reg.CAPABILITY_MATRIX === 'object' && reg.CAPABILITY_MATRIX !== null) {
|
|
79
|
+
const out: Record<string, readonly string[]> = {};
|
|
80
|
+
for (const [peer, cap] of Object.entries(reg.CAPABILITY_MATRIX)) {
|
|
81
|
+
if (cap && Array.isArray(cap.roles)) {
|
|
82
|
+
out[peer] = Object.freeze([...cap.roles]);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
_delegateMatrixCache = Object.freeze(out);
|
|
86
|
+
return _delegateMatrixCache;
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
// fall through to fallback
|
|
90
|
+
}
|
|
91
|
+
_delegateMatrixCache = _DELEGATE_MATRIX_FALLBACK;
|
|
92
|
+
return _delegateMatrixCache;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Build the flat set of valid `<peer>-<role>` IDs from the capability
|
|
97
|
+
* matrix. e.g. `gemini-research`, `codex-execute`, etc. The literal
|
|
98
|
+
* string `"none"` is the explicit opt-out and is accepted separately.
|
|
99
|
+
*/
|
|
100
|
+
export function validDelegateIds(): readonly string[] {
|
|
101
|
+
const matrix = loadDelegateMatrix();
|
|
102
|
+
const out: string[] = [];
|
|
103
|
+
for (const [peer, roles] of Object.entries(matrix)) {
|
|
104
|
+
for (const role of roles) {
|
|
105
|
+
out.push(`${peer}-${role}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return Object.freeze(out.sort());
|
|
109
|
+
}
|
|
110
|
+
|
|
21
111
|
// Importing a type from generated.d.ts satisfies the Plan 20-00 rule that
|
|
22
112
|
// every Tier-1 TS file participates in the codegen graph. We don't use
|
|
23
113
|
// IntelSchema at runtime; the re-export keeps it visible for static checks.
|
|
@@ -42,6 +132,22 @@ export interface AgentFrontmatter {
|
|
|
42
132
|
'default-tier'?: 'haiku' | 'sonnet' | 'opus';
|
|
43
133
|
'reasoning-class'?: 'high' | 'medium' | 'low';
|
|
44
134
|
'size_budget'?: 'S' | 'M' | 'L' | 'XL';
|
|
135
|
+
/**
|
|
136
|
+
* Phase 27 (Plan 27-06) — peer-CLI delegation hint.
|
|
137
|
+
*
|
|
138
|
+
* Optional. Default unset = use local Anthropic call. Setting
|
|
139
|
+
* `delegate_to: gemini-research` tells session-runner "try delegate
|
|
140
|
+
* first, fall back to local on peer-absent / peer-error". Setting
|
|
141
|
+
* `delegate_to: none` explicitly opts out (e.g. security-sensitive
|
|
142
|
+
* agents). See agents/README.md "Peer-CLI delegation (delegate_to)"
|
|
143
|
+
* for the additive-superset rationale (CONTEXT D-06).
|
|
144
|
+
*
|
|
145
|
+
* Valid values are `<peer>-<role>` IDs that the peer-CLI registry
|
|
146
|
+
* capability matrix knows (e.g. `gemini-research`, `codex-execute`,
|
|
147
|
+
* `cursor-debug`, `cursor-plan`, `copilot-review`, `copilot-research`,
|
|
148
|
+
* `qwen-write`) plus the literal string `"none"`.
|
|
149
|
+
*/
|
|
150
|
+
'delegate_to'?: string;
|
|
45
151
|
}
|
|
46
152
|
|
|
47
153
|
const REQUIRED_FIELDS: readonly (keyof AgentFrontmatter)[] = [
|
|
@@ -169,6 +275,48 @@ export function validateReasoningClass(
|
|
|
169
275
|
return violations;
|
|
170
276
|
}
|
|
171
277
|
|
|
278
|
+
/**
|
|
279
|
+
* Validate the optional Phase 27 (Plan 27-06) `delegate_to` field. Returns
|
|
280
|
+
* an array of violation messages; empty means the agent passes.
|
|
281
|
+
*
|
|
282
|
+
* Rules (CONTEXT D-06):
|
|
283
|
+
* 1. `delegate_to` is OPTIONAL. Absence = use local Anthropic call.
|
|
284
|
+
* 2. If present, value MUST be a string.
|
|
285
|
+
* 3. The literal value `"none"` is accepted as the explicit opt-out.
|
|
286
|
+
* 4. Any other value MUST match a `<peer>-<role>` ID drawn from the
|
|
287
|
+
* peer-CLI capability matrix (Plan 27-05's registry.cjs is the
|
|
288
|
+
* source of truth; loadDelegateMatrix() resolves it lazily with
|
|
289
|
+
* a literal fallback if the registry is unloadable).
|
|
290
|
+
*
|
|
291
|
+
* The 26 v1.26 baseline agents do not carry this field; this helper
|
|
292
|
+
* returns `[]` for them. The validator runs trivially clean on them.
|
|
293
|
+
*/
|
|
294
|
+
export function validateDelegateTo(
|
|
295
|
+
fm: Record<string, unknown>,
|
|
296
|
+
agentName: string,
|
|
297
|
+
): string[] {
|
|
298
|
+
const violations: string[] = [];
|
|
299
|
+
const has = 'delegate_to' in fm && !isMissing(fm['delegate_to']);
|
|
300
|
+
if (!has) return violations;
|
|
301
|
+
|
|
302
|
+
const raw = fm['delegate_to'];
|
|
303
|
+
if (typeof raw !== 'string') {
|
|
304
|
+
violations.push(
|
|
305
|
+
`delegate_to: invalid value "${String(raw)}" for agent "${agentName}" — must be a string ("none" or "<peer>-<role>" e.g. "gemini-research")`,
|
|
306
|
+
);
|
|
307
|
+
return violations;
|
|
308
|
+
}
|
|
309
|
+
if (raw === 'none') return violations; // explicit opt-out
|
|
310
|
+
|
|
311
|
+
const valid = validDelegateIds();
|
|
312
|
+
if (!valid.includes(raw)) {
|
|
313
|
+
violations.push(
|
|
314
|
+
`delegate_to: invalid value "${raw}" for agent "${agentName}" — must be "none" or one of: ${valid.join(', ')} (peer-CLI capability matrix; see scripts/lib/peer-cli/registry.cjs)`,
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
return violations;
|
|
318
|
+
}
|
|
319
|
+
|
|
172
320
|
function walkMd(dir: string): string[] {
|
|
173
321
|
const out: string[] = [];
|
|
174
322
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
@@ -234,6 +382,16 @@ function main(): void {
|
|
|
234
382
|
console.log(`${f}:${msg}`);
|
|
235
383
|
violations++;
|
|
236
384
|
}
|
|
385
|
+
|
|
386
|
+
// Plan 27-06 — peer-CLI delegate_to validation (additive optional field).
|
|
387
|
+
const delegateViolations = validateDelegateTo(
|
|
388
|
+
fm as Record<string, unknown>,
|
|
389
|
+
agentName,
|
|
390
|
+
);
|
|
391
|
+
for (const msg of delegateViolations) {
|
|
392
|
+
console.log(`${f}:${msg}`);
|
|
393
|
+
violations++;
|
|
394
|
+
}
|
|
237
395
|
}
|
|
238
396
|
|
|
239
397
|
console.log(`summary: ${files.length} file(s) checked, ${violations} violation(s)`);
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: peer-cli-add
|
|
3
|
+
description: "Guided ladder for adding a brand-new peer (a peer not in the v1.27 capability matrix) to the gdd peer-CLI delegation layer. Verification ladder + adapter scaffolding + capability-matrix update + Windows quirks documented. Run when you discover a new peer CLI you want gdd to delegate to."
|
|
4
|
+
argument-hint: "<new-peer-id> <peer-binary> <protocol: acp|asp>"
|
|
5
|
+
tools: Read, Edit, Write, Bash, Grep
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<!-- Procedural pattern adapted from greenpolo/cc-multi-cli's `multi-cli-anything` skill (Apache 2.0). See NOTICE for full attribution. -->
|
|
9
|
+
|
|
10
|
+
# peer-cli-add
|
|
11
|
+
|
|
12
|
+
## Role
|
|
13
|
+
|
|
14
|
+
You add a brand-new peer-CLI to gdd's delegation layer. v1.27.0 ships 5 peers (codex, gemini, cursor, copilot, qwen). When the user wants a 6th — a peer that exists in the wild but isn't in our capability matrix yet — they run this skill. It walks them through a verification ladder (does the peer actually speak ACP or ASP?) and produces the 3-file footprint that integrates the peer cleanly.
|
|
15
|
+
|
|
16
|
+
## Invocation Contract
|
|
17
|
+
|
|
18
|
+
- **Required input**: `<new-peer-id>` (lowercase identifier, e.g., `aider`), `<peer-binary>` (the executable name, e.g., `aider` or `aider.cmd`), `<protocol>` (`acp` or `asp`).
|
|
19
|
+
- **Output**: a 3-file diff + a verification report.
|
|
20
|
+
|
|
21
|
+
## Procedure
|
|
22
|
+
|
|
23
|
+
### Step 1 — Verification ladder (no edits yet)
|
|
24
|
+
|
|
25
|
+
Before touching any code, confirm the peer actually speaks the protocol it claims.
|
|
26
|
+
|
|
27
|
+
#### 1a. Binary on PATH
|
|
28
|
+
|
|
29
|
+
`which <peer-binary>` (POSIX) or `where <peer-binary>` (Windows). If exit non-zero, stop and ask user to install the peer first.
|
|
30
|
+
|
|
31
|
+
#### 1b. Handshake test
|
|
32
|
+
|
|
33
|
+
Spawn the peer with the appropriate protocol entry point:
|
|
34
|
+
- ACP peers: `<peer-binary> acp` (or whatever the peer documents as its ACP entry — Gemini uses `gemini acp`; some peers use a flag).
|
|
35
|
+
- ASP peers: `<peer-binary> app-server` (Codex's convention; other ASP peers may differ).
|
|
36
|
+
|
|
37
|
+
Send an `initialize` JSON-RPC message over stdin with `protocolVersion: '2025-06-18'` (ACP) or `service_name: 'gdd_peer_delegation'` (ASP).
|
|
38
|
+
|
|
39
|
+
Capture the reply on stdout. If the reply is a valid JSON-RPC response with `result.protocolVersion` (ACP) or `result.threadId` (ASP), the peer speaks the protocol.
|
|
40
|
+
|
|
41
|
+
If no valid reply within 5 seconds, the peer either doesn't speak this protocol or uses a non-standard entry point. Stop and ask the user for the correct invocation.
|
|
42
|
+
|
|
43
|
+
#### 1c. Model-ID `-preview`-suffix trap
|
|
44
|
+
|
|
45
|
+
Many peers expose preview models with a `-preview` suffix (e.g., `gpt-5-preview` vs `gpt-5`). The suffix drifts: today's preview is tomorrow's GA. Capture the peer's current model list (most peers expose `<peer-binary> models` or similar). Note any model that has `-preview` in its name and document the parent name in the new entry's `provider_model_id` field — so the runtime-models.md entry can survive the suffix flipping.
|
|
46
|
+
|
|
47
|
+
#### 1d. Windows quirks
|
|
48
|
+
|
|
49
|
+
If the peer-binary ends in `.cmd` and the user is on Windows, confirm the spawn-cmd shell-escape logic from `scripts/lib/peer-cli/spawn-cmd.cjs` will pick it up (it should — that module already handles `.cmd` detection per Plan 27-03 / D-04). Document any other Windows-specific quirks in the new adapter's leading comment.
|
|
50
|
+
|
|
51
|
+
### Step 2 — Generate the adapter scaffold
|
|
52
|
+
|
|
53
|
+
Use the existing 5 adapters at `scripts/lib/peer-cli/adapters/{codex,gemini,cursor,copilot,qwen}.cjs` as templates. Pick the closest match to your new peer's protocol (ASP if `<protocol> = asp`, otherwise ACP).
|
|
54
|
+
|
|
55
|
+
Use the `Write` tool to create `scripts/lib/peer-cli/adapters/<new-peer-id>.cjs`:
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
'use strict';
|
|
59
|
+
|
|
60
|
+
const { createAcpClient } = require('../acp-client.cjs');
|
|
61
|
+
// OR for ASP peers: const { createAspClient } = require('../asp-client.cjs');
|
|
62
|
+
|
|
63
|
+
const ROLES_CLAIMED = ['<role-1>', '<role-2>']; // ASK USER which roles this peer claims
|
|
64
|
+
const ROLE_PREFIX = {
|
|
65
|
+
'<role-1>': '<prompt prefix or empty string>',
|
|
66
|
+
'<role-2>': '<prompt prefix or empty string>',
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
function claims(role) { return ROLES_CLAIMED.includes(role); }
|
|
70
|
+
|
|
71
|
+
async function dispatch({ command, args, cwd, env }, role, text, opts) {
|
|
72
|
+
if (!claims(role)) {
|
|
73
|
+
throw new Error(`<new-peer-id> does not claim role: ${role}`);
|
|
74
|
+
}
|
|
75
|
+
const client = createAcpClient({ command, args, cwd, env });
|
|
76
|
+
try {
|
|
77
|
+
await client.initialize({ protocolVersion: '2025-06-18' });
|
|
78
|
+
const prompt = (ROLE_PREFIX[role] || '') + text;
|
|
79
|
+
return await client.prompt(prompt, { onNotification: opts?.onNotification });
|
|
80
|
+
} finally {
|
|
81
|
+
await client.close();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = { claims, dispatch, ROLES_CLAIMED, ROLE_PREFIX, name: '<new-peer-id>', protocol: '<protocol>' };
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Replace placeholders with the user's input from Step 1's verification.
|
|
89
|
+
|
|
90
|
+
### Step 3 — Add `peerBinary` to runtimes.cjs
|
|
91
|
+
|
|
92
|
+
Edit `scripts/lib/install/runtimes.cjs` to add an entry for the new peer. Mirror the shape of the 5 existing peer entries. Add the `peerBinary` field with platform-aware resolution:
|
|
93
|
+
|
|
94
|
+
```js
|
|
95
|
+
{
|
|
96
|
+
id: '<new-peer-id>',
|
|
97
|
+
// ... existing fields per Phase 24 runtime matrix shape ...
|
|
98
|
+
peerBinary: process.platform === 'win32' ? '<peer-binary>.cmd' : '<peer-binary>',
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Step 4 — Add the capability-matrix entry
|
|
103
|
+
|
|
104
|
+
Edit `reference/peer-cli-capabilities.md`. Add a new row to the top capability matrix table AND a new per-peer section. Follow the existing format. Cite the verification evidence from Step 1.
|
|
105
|
+
|
|
106
|
+
### Step 5 — Update the registry capability matrix
|
|
107
|
+
|
|
108
|
+
Edit `scripts/lib/peer-cli/registry.cjs`. Add the new peer to the `CAPABILITY_MATRIX` constant (and `KNOWN_PEERS` if that's a separate list). Mirror the shape of the 5 existing entries.
|
|
109
|
+
|
|
110
|
+
### Step 6 — Verify the integration
|
|
111
|
+
|
|
112
|
+
Run, in this order, until each passes:
|
|
113
|
+
|
|
114
|
+
1. `npx tsc --noEmit` — clean.
|
|
115
|
+
2. `node --test tests/peer-cli-registry.test.cjs tests/peer-cli-adapters.test.cjs` — no regression on existing tests.
|
|
116
|
+
3. `node --test tests/reference-registry.test.cjs` — capability-matrix doc is in registry.json (if you added it).
|
|
117
|
+
4. `npm run validate:frontmatter` — no agent's `delegate_to:` field is broken by the new entry.
|
|
118
|
+
|
|
119
|
+
If any step fails, surface the error and offer to revert the changes.
|
|
120
|
+
|
|
121
|
+
### Step 7 — Surface a 3-file footprint summary
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
## peer-cli-add summary
|
|
125
|
+
|
|
126
|
+
Added peer: <new-peer-id> (protocol: <protocol>)
|
|
127
|
+
Roles claimed: <role-1>, <role-2>
|
|
128
|
+
|
|
129
|
+
Files modified:
|
|
130
|
+
✓ scripts/lib/peer-cli/adapters/<new-peer-id>.cjs (new)
|
|
131
|
+
✓ scripts/lib/install/runtimes.cjs (added peerBinary entry)
|
|
132
|
+
✓ reference/peer-cli-capabilities.md (added matrix row + per-peer section)
|
|
133
|
+
✓ scripts/lib/peer-cli/registry.cjs (added to CAPABILITY_MATRIX)
|
|
134
|
+
|
|
135
|
+
Verification:
|
|
136
|
+
✓ tsc clean
|
|
137
|
+
✓ existing peer-cli tests pass
|
|
138
|
+
✓ reference-registry round-trip valid
|
|
139
|
+
✓ frontmatter validator: 0 violations
|
|
140
|
+
|
|
141
|
+
Next steps:
|
|
142
|
+
- Run /gdd:peers to confirm the new peer shows up in the capability matrix.
|
|
143
|
+
- Run skills/peer-cli-customize/SKILL.md to wire delegate_to: <new-peer-id>-<role> on specific agents.
|
|
144
|
+
- Phase 23.5 bandit will need ~5 cycles of data before the posterior surfaces a recommendation for this peer.
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Edge cases
|
|
148
|
+
|
|
149
|
+
- **Peer speaks neither ACP nor ASP** — gdd v1.27 ships only those two protocols. Stop and document the gap in `.design/RESEARCH.md` for a future phase to consider adding a new protocol layer.
|
|
150
|
+
- **Peer claims a role no existing peer claims** (e.g., `translate`) — fine, capability matrix is open. But document the role in `reference/peer-cli-capabilities.md` so future peers can compete on it.
|
|
151
|
+
- **Peer claims ALL roles** (e.g., a generalist peer) — accept, but flag in the per-peer section. Generalist peers are usually weaker than specialist peers; the bandit will sort it out via measurement.
|
|
152
|
+
- **Peer name conflicts with an existing peer-id** — fail. Peer-IDs must be globally unique. Suggest a disambiguating suffix.
|
|
153
|
+
- **User wants to add a peer for testing only** — same flow, but suggest committing under a separate branch and not adding to the install-time detection nudge until the peer is production-ready.
|
|
154
|
+
|
|
155
|
+
## Cross-references
|
|
156
|
+
|
|
157
|
+
- `scripts/lib/peer-cli/registry.cjs` (Plan 27-05) — capability matrix data.
|
|
158
|
+
- `scripts/lib/peer-cli/adapters/*.cjs` (Plan 27-04) — adapter template.
|
|
159
|
+
- `scripts/lib/peer-cli/spawn-cmd.cjs` (Plan 27-03) — Windows .cmd handling.
|
|
160
|
+
- `reference/peer-cli-capabilities.md` (Plan 27-05) — capability-matrix doc.
|
|
161
|
+
- `skills/peer-cli-customize/SKILL.md` — once new peer is added, use customize to wire it on specific agents.
|
|
162
|
+
- `.planning/phases/27-peer-cli-delegation/CONTEXT.md` D-02, D-05 — decision lineage.
|
|
163
|
+
|
|
164
|
+
## Record
|
|
165
|
+
|
|
166
|
+
After execution, append one JSONL line to `.design/skill-records.jsonl`:
|
|
167
|
+
|
|
168
|
+
```json
|
|
169
|
+
{"skill": "peer-cli-add", "ts": "<ISO timestamp>", "new_peer": "<new-peer-id>", "protocol": "<protocol>", "roles_claimed": ["<role-1>"], "verification_passed": true}
|
|
170
|
+
```
|