agentopology 0.2.1 → 0.2.4

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 (79) hide show
  1. package/dist/analyzer/__tests__/observability-gap.test.d.ts +8 -0
  2. package/dist/analyzer/__tests__/observability-gap.test.d.ts.map +1 -0
  3. package/dist/analyzer/__tests__/observability-gap.test.js +132 -0
  4. package/dist/analyzer/__tests__/observability-gap.test.js.map +1 -0
  5. package/dist/analyzer/index.d.ts +1 -1
  6. package/dist/analyzer/index.d.ts.map +1 -1
  7. package/dist/analyzer/index.js +26 -0
  8. package/dist/analyzer/index.js.map +1 -1
  9. package/dist/bindings/__tests__/bindings.test.js +423 -0
  10. package/dist/bindings/__tests__/bindings.test.js.map +1 -1
  11. package/dist/bindings/__tests__/observability.test.d.ts +9 -0
  12. package/dist/bindings/__tests__/observability.test.d.ts.map +1 -0
  13. package/dist/bindings/__tests__/observability.test.js +136 -0
  14. package/dist/bindings/__tests__/observability.test.js.map +1 -0
  15. package/dist/bindings/__tests__/stub-generation.test.d.ts +16 -0
  16. package/dist/bindings/__tests__/stub-generation.test.d.ts.map +1 -0
  17. package/dist/bindings/__tests__/stub-generation.test.js +117 -0
  18. package/dist/bindings/__tests__/stub-generation.test.js.map +1 -0
  19. package/dist/bindings/claude-code.d.ts.map +1 -1
  20. package/dist/bindings/claude-code.js +188 -37
  21. package/dist/bindings/claude-code.js.map +1 -1
  22. package/dist/bindings/codex.d.ts.map +1 -1
  23. package/dist/bindings/codex.js +1 -12
  24. package/dist/bindings/codex.js.map +1 -1
  25. package/dist/bindings/cursor.d.ts.map +1 -1
  26. package/dist/bindings/cursor.js +0 -12
  27. package/dist/bindings/cursor.js.map +1 -1
  28. package/dist/bindings/gemini-cli.d.ts.map +1 -1
  29. package/dist/bindings/gemini-cli.js +1 -12
  30. package/dist/bindings/gemini-cli.js.map +1 -1
  31. package/dist/bindings/kiro.d.ts.map +1 -1
  32. package/dist/bindings/kiro.js +1 -12
  33. package/dist/bindings/kiro.js.map +1 -1
  34. package/dist/bindings/lib/__tests__/stub.test.d.ts +8 -0
  35. package/dist/bindings/lib/__tests__/stub.test.d.ts.map +1 -0
  36. package/dist/bindings/lib/__tests__/stub.test.js +66 -0
  37. package/dist/bindings/lib/__tests__/stub.test.js.map +1 -0
  38. package/dist/bindings/lib/stub.d.ts +56 -0
  39. package/dist/bindings/lib/stub.d.ts.map +1 -0
  40. package/dist/bindings/lib/stub.js +69 -0
  41. package/dist/bindings/lib/stub.js.map +1 -0
  42. package/dist/bindings/openclaw.d.ts.map +1 -1
  43. package/dist/bindings/openclaw.js +1 -12
  44. package/dist/bindings/openclaw.js.map +1 -1
  45. package/dist/bindings/types.d.ts +5 -0
  46. package/dist/bindings/types.d.ts.map +1 -1
  47. package/dist/bindings/types.js.map +1 -1
  48. package/dist/cli/index.js +89 -4
  49. package/dist/cli/index.js.map +1 -1
  50. package/dist/docs/content.d.ts.map +1 -1
  51. package/dist/docs/content.js +19 -5
  52. package/dist/docs/content.js.map +1 -1
  53. package/dist/parser/__tests__/parser.test.js +88 -3
  54. package/dist/parser/__tests__/parser.test.js.map +1 -1
  55. package/dist/parser/ast.d.ts +15 -0
  56. package/dist/parser/ast.d.ts.map +1 -1
  57. package/dist/parser/index.d.ts.map +1 -1
  58. package/dist/parser/index.js +12 -0
  59. package/dist/parser/index.js.map +1 -1
  60. package/dist/parser/validator.d.ts.map +1 -1
  61. package/dist/parser/validator.js +65 -9
  62. package/dist/parser/validator.js.map +1 -1
  63. package/dist/scaffold/__tests__/scaffold.test.js +20 -0
  64. package/dist/scaffold/__tests__/scaffold.test.js.map +1 -1
  65. package/dist/scaffold/__tests__/settings-merge.test.d.ts +15 -0
  66. package/dist/scaffold/__tests__/settings-merge.test.d.ts.map +1 -0
  67. package/dist/scaffold/__tests__/settings-merge.test.js +127 -0
  68. package/dist/scaffold/__tests__/settings-merge.test.js.map +1 -0
  69. package/dist/scaffold/incremental.d.ts.map +1 -1
  70. package/dist/scaffold/incremental.js +11 -2
  71. package/dist/scaffold/incremental.js.map +1 -1
  72. package/dist/scaffold/merge.d.ts +16 -0
  73. package/dist/scaffold/merge.d.ts.map +1 -1
  74. package/dist/scaffold/merge.js +77 -0
  75. package/dist/scaffold/merge.js.map +1 -1
  76. package/dist/scaffold/types.d.ts +2 -0
  77. package/dist/scaffold/types.d.ts.map +1 -1
  78. package/package.json +1 -1
  79. package/spec/grammar.md +9 -0
@@ -1,6 +1,7 @@
1
1
  import { describe, it, expect } from "vitest";
2
2
  import { parse } from "../../parser/index.js";
3
3
  import { bindings, claudeCodeBinding, kiroBinding, cursorBinding, } from "../index.js";
4
+ import { isStubContent, STUB_MARKER } from "../lib/stub.js";
4
5
  // ---------------------------------------------------------------------------
5
6
  // Minimal but valid .at topology exercising most features
6
7
  // ---------------------------------------------------------------------------
@@ -244,6 +245,246 @@ describe("claude-code binding", () => {
244
245
  const advisoryWrapper = files.find((f) => f.path.includes("gate-soft-review.sh"));
245
246
  expect(advisoryWrapper).toBeUndefined();
246
247
  });
248
+ it("gate stub contains the AGENTOPOLOGY_STUB marker (#4)", () => {
249
+ // The gate's `run` script is a stub generated by shellStub() in the
250
+ // shared lib. It must carry the marker so tooling can detect it.
251
+ const stub = files.find((f) => f.path.endsWith("/scripts/lint.sh"));
252
+ expect(stub).toBeDefined();
253
+ expect(stub.content).toContain(STUB_MARKER);
254
+ expect(isStubContent(stub.content)).toBe(true);
255
+ });
256
+ });
257
+ // ---------------------------------------------------------------------------
258
+ // claude-code — Issue #3: SubagentStop matcher targeting
259
+ //
260
+ // Per https://code.claude.com/docs/en/hooks.md, the SubagentStop hook's
261
+ // `matcher` field is matched against agent_type (the registered subagent
262
+ // name). Only agent and group nodes scaffold a `.claude/agents/<name>/AGENT.md`
263
+ // file and therefore register as subagent_types. Human, action, and
264
+ // orchestrator targets cannot be matched and would produce dead config that
265
+ // never fires. The scaffolder must skip those hooks (and warn) but still
266
+ // generate the wrapper script so the orchestrator can invoke it directly.
267
+ // ---------------------------------------------------------------------------
268
+ describe("claude-code binding — gate SubagentStop targeting (#3)", () => {
269
+ // Build a topology where one gate targets an agent and another targets a
270
+ // human node — proves both branches of isRegisteredSubagentType.
271
+ const GATE_TARGETING_SOURCE = `
272
+ topology gate-target-test : [pipeline] {
273
+ meta {
274
+ version: "1.0.0"
275
+ description: "Gate targeting test"
276
+ }
277
+
278
+ orchestrator { model: opus handles: [intake] }
279
+ action intake { kind: external source: "user" }
280
+
281
+ agent writer {
282
+ model: sonnet
283
+ phase: 1
284
+ tools: [Read, Write]
285
+ reads: ["workspace/input.md"]
286
+ writes: ["workspace/draft.md"]
287
+ permissions: autonomous
288
+ }
289
+
290
+ human approver {
291
+ description: "Approve the draft before publishing"
292
+ timeout: 1h
293
+ on-timeout: skip
294
+ }
295
+
296
+ agent publisher {
297
+ model: sonnet
298
+ phase: 3
299
+ tools: [Read, Bash]
300
+ reads: ["workspace/draft.md"]
301
+ writes: ["workspace/published.md"]
302
+ permissions: autonomous
303
+ }
304
+
305
+ flow {
306
+ intake -> writer
307
+ writer -> approver
308
+ approver -> publisher
309
+ }
310
+
311
+ gates {
312
+ gate gate-on-agent {
313
+ after: writer
314
+ before: approver
315
+ run: "scripts/check-draft.sh"
316
+ on-fail: bounce-back
317
+ behavior: blocking
318
+ }
319
+ gate gate-on-human {
320
+ after: approver
321
+ before: publisher
322
+ run: "scripts/check-approval.sh"
323
+ on-fail: halt
324
+ behavior: blocking
325
+ }
326
+ }
327
+ }
328
+ `;
329
+ const ast = parse(GATE_TARGETING_SOURCE);
330
+ // Capture warnings emitted during scaffold so tests can assert on them.
331
+ const originalWarn = console.warn;
332
+ const warnings = [];
333
+ console.warn = (...args) => {
334
+ warnings.push(args.map(String).join(" "));
335
+ };
336
+ let files;
337
+ try {
338
+ files = claudeCodeBinding.scaffold(ast);
339
+ }
340
+ finally {
341
+ console.warn = originalWarn;
342
+ }
343
+ const settingsFile = files.find((f) => f.path === ".claude/settings.json");
344
+ const settings = JSON.parse(settingsFile.content);
345
+ it("emits SubagentStop hook when gate.after is an agent", () => {
346
+ expect(settings.hooks.SubagentStop).toBeDefined();
347
+ const agentHook = settings.hooks.SubagentStop.find((h) => h.matcher === "writer");
348
+ expect(agentHook).toBeDefined();
349
+ expect(JSON.stringify(agentHook)).toContain("gate-gate-on-agent");
350
+ });
351
+ it("does NOT emit SubagentStop hook when gate.after is a human node", () => {
352
+ const humanHook = (settings.hooks?.SubagentStop ?? []).find((h) => h.matcher === "approver");
353
+ expect(humanHook).toBeUndefined();
354
+ });
355
+ it("warns at scaffold time when a blocking gate targets a non-agent node", () => {
356
+ const humanWarning = warnings.find((w) => w.includes("gate-on-human") && w.includes("approver"));
357
+ expect(humanWarning).toBeDefined();
358
+ expect(humanWarning).toContain("non-agent");
359
+ });
360
+ it("still generates the gate wrapper script for non-agent-targeted gate", () => {
361
+ // The wrapper must exist so the orchestrator/playbook can invoke it
362
+ // manually at the right step, even when no hook fires.
363
+ const wrapper = files.find((f) => f.path.endsWith("/scripts/gate-gate-on-human.sh"));
364
+ expect(wrapper).toBeDefined();
365
+ });
366
+ it("wrapper script for non-agent-targeted gate documents the limitation", () => {
367
+ const wrapper = files.find((f) => f.path.endsWith("/scripts/gate-gate-on-human.sh"));
368
+ expect(wrapper.content).toContain("NOT wired as a SubagentStop hook");
369
+ expect(wrapper.content).toContain("orchestrator playbook");
370
+ });
371
+ it("wrapper script for agent-targeted gate documents SubagentStop enforcement", () => {
372
+ const wrapper = files.find((f) => f.path.endsWith("/scripts/gate-gate-on-agent.sh"));
373
+ expect(wrapper.content).toContain("SubagentStop hook on agent \"writer\"");
374
+ expect(wrapper.content).toContain("Exit 2 blocks");
375
+ });
376
+ it("scaffolded hook entry uses agent name (not 'Agent' or arbitrary) as matcher", () => {
377
+ // Sanity check: matchers must be exactly the agent id, since Claude Code
378
+ // matches them against agent_type, not against the tool name.
379
+ const hooks = settings.hooks.SubagentStop ?? [];
380
+ for (const h of hooks) {
381
+ // Each matcher must correspond to an agent or group node in the AST.
382
+ const matcher = h.matcher;
383
+ expect(matcher).toBeTruthy();
384
+ const matchingNode = ast.nodes.find((n) => (n.type === "agent" || n.type === "group") && n.id === matcher);
385
+ expect(matchingNode, `matcher "${matcher}" must resolve to an agent or group node`).toBeDefined();
386
+ }
387
+ });
388
+ });
389
+ // ---------------------------------------------------------------------------
390
+ // claude-code — Integration test: youtube-flywheel.at fixture (#3)
391
+ //
392
+ // The real-world failure from agentopology-content: gates with after: human
393
+ // produced dead SubagentStop entries. This integration test pins the fix
394
+ // against the actual fixture and asserts the right hooks fire and the wrong
395
+ // ones don't.
396
+ // ---------------------------------------------------------------------------
397
+ describe("claude-code binding — youtube-flywheel integration (#3)", () => {
398
+ const YOUTUBE_FLYWHEEL_SOURCE = `
399
+ topology youtube-flywheel : [pipeline, fan-out, human-gate] {
400
+ meta { version: "1.0.0" }
401
+
402
+ orchestrator { model: opus handles: [start] }
403
+ action start { kind: inline }
404
+
405
+ agent briefer {
406
+ model: opus
407
+ phase: 4
408
+ tools: [Read, Write]
409
+ reads: ["workspace/idea.json"]
410
+ writes: ["workspace/brief.json"]
411
+ permissions: supervised
412
+ }
413
+
414
+ human approve-brief {
415
+ description: "Review the brief before render"
416
+ timeout: 8h
417
+ on-timeout: skip
418
+ }
419
+
420
+ agent renderer {
421
+ model: sonnet
422
+ phase: 6
423
+ tools: [Read, Write, Bash]
424
+ reads: ["workspace/brief.json"]
425
+ writes: ["workspace/render/"]
426
+ permissions: autonomous
427
+ }
428
+
429
+ flow {
430
+ start -> briefer
431
+ briefer -> approve-brief
432
+ approve-brief -> renderer
433
+ }
434
+
435
+ gates {
436
+ gate brief-schema-valid {
437
+ after: briefer
438
+ before: approve-brief
439
+ run: "scripts/validate-brief.sh"
440
+ on-fail: bounce-back
441
+ behavior: blocking
442
+ }
443
+ gate render-quality {
444
+ after: renderer
445
+ before: start
446
+ run: "scripts/qa-render.sh"
447
+ on-fail: bounce-back
448
+ behavior: blocking
449
+ }
450
+ gate platform-credentials {
451
+ after: approve-brief
452
+ before: renderer
453
+ run: "scripts/check-platform-auth.sh"
454
+ on-fail: halt
455
+ behavior: blocking
456
+ }
457
+ }
458
+ }
459
+ `;
460
+ const ast = parse(YOUTUBE_FLYWHEEL_SOURCE);
461
+ const originalWarn = console.warn;
462
+ const warnings = [];
463
+ console.warn = (...args) => {
464
+ warnings.push(args.map(String).join(" "));
465
+ };
466
+ let files;
467
+ try {
468
+ files = claudeCodeBinding.scaffold(ast);
469
+ }
470
+ finally {
471
+ console.warn = originalWarn;
472
+ }
473
+ const settingsFile = files.find((f) => f.path === ".claude/settings.json");
474
+ const settings = JSON.parse(settingsFile.content);
475
+ it("emits SubagentStop hooks for the two agent-targeted gates", () => {
476
+ const matchers = (settings.hooks?.SubagentStop ?? []).map((h) => h.matcher);
477
+ expect(matchers).toContain("briefer");
478
+ expect(matchers).toContain("renderer");
479
+ });
480
+ it("does NOT emit a SubagentStop hook for the gate targeting the human 'approve-brief'", () => {
481
+ const matchers = (settings.hooks?.SubagentStop ?? []).map((h) => h.matcher);
482
+ expect(matchers).not.toContain("approve-brief");
483
+ });
484
+ it("warns at scaffold time about the platform-credentials gate", () => {
485
+ const w = warnings.find((m) => m.includes("platform-credentials") && m.includes("approve-brief"));
486
+ expect(w).toBeDefined();
487
+ });
247
488
  });
248
489
  // ---------------------------------------------------------------------------
249
490
  // claude-code — Exotic features: groups, humans, circuit breakers, scale,
@@ -2086,4 +2327,186 @@ topology debate-test : [fan-out, debate, pipeline] {
2086
2327
  });
2087
2328
  });
2088
2329
  });
2330
+ // ---------------------------------------------------------------------------
2331
+ // claude-code — Inline orchestrator delegation mode (#7)
2332
+ //
2333
+ // When orchestrator { delegation: inline } is declared, no subagent is ever
2334
+ // spawned — the orchestrator drives every agent step in its own session. So
2335
+ // SubagentStop hooks would be dead config even when the matcher names a real
2336
+ // agent. The binding must suppress those hooks and emit a playbook cheat
2337
+ // sheet instead.
2338
+ // ---------------------------------------------------------------------------
2339
+ describe("claude-code binding — inline orchestrator delegation (#7)", () => {
2340
+ const INLINE_SOURCE = `
2341
+ topology inline-test : [pipeline] {
2342
+ meta { version: "1.0.0" description: "Inline-orchestrator topology" }
2343
+ orchestrator {
2344
+ model: opus
2345
+ delegation: inline
2346
+ handles: [start]
2347
+ }
2348
+ action start { kind: inline }
2349
+ agent writer {
2350
+ model: sonnet
2351
+ phase: 1
2352
+ tools: [Read, Write]
2353
+ reads: ["workspace/input.md"]
2354
+ writes: ["workspace/draft.md"]
2355
+ permissions: autonomous
2356
+ }
2357
+ agent publisher {
2358
+ model: sonnet
2359
+ phase: 2
2360
+ tools: [Read, Bash]
2361
+ reads: ["workspace/draft.md"]
2362
+ writes: ["workspace/published.md"]
2363
+ permissions: autonomous
2364
+ }
2365
+ flow {
2366
+ start -> writer
2367
+ writer -> publisher
2368
+ }
2369
+ gates {
2370
+ gate quality-check {
2371
+ after: writer
2372
+ before: publisher
2373
+ run: "scripts/check.sh"
2374
+ on-fail: bounce-back
2375
+ behavior: blocking
2376
+ }
2377
+ }
2378
+ triggers {
2379
+ command run { pattern: "/run" }
2380
+ }
2381
+ hooks {
2382
+ hook log-finish {
2383
+ on: SubagentStop
2384
+ matcher: writer
2385
+ run: "scripts/log.sh"
2386
+ type: command
2387
+ }
2388
+ }
2389
+ }
2390
+ `;
2391
+ const ast = parse(INLINE_SOURCE);
2392
+ const originalWarn = console.warn;
2393
+ const warnings = [];
2394
+ console.warn = (...args) => {
2395
+ warnings.push(args.map(String).join(" "));
2396
+ };
2397
+ let files;
2398
+ try {
2399
+ files = claudeCodeBinding.scaffold(ast);
2400
+ }
2401
+ finally {
2402
+ console.warn = originalWarn;
2403
+ }
2404
+ it("does NOT emit any SubagentStop entries in settings.json (gates + global hooks suppressed)", () => {
2405
+ const settingsFile = files.find((f) => f.path === ".claude/settings.json");
2406
+ if (!settingsFile)
2407
+ return; // no hooks/permissions → no settings file
2408
+ const settings = JSON.parse(settingsFile.content);
2409
+ const stop = settings.hooks?.SubagentStop ?? [];
2410
+ expect(stop).toEqual([]);
2411
+ });
2412
+ it("warns once per blocking gate that the hook is suppressed", () => {
2413
+ const gateWarn = warnings.find((w) => w.includes("quality-check") && w.includes('"inline"'));
2414
+ expect(gateWarn).toBeDefined();
2415
+ });
2416
+ it("warns when an explicit global SubagentStop hook is declared in inline mode", () => {
2417
+ const hookWarn = warnings.find((w) => w.includes("log-finish") && w.includes('"inline"'));
2418
+ expect(hookWarn).toBeDefined();
2419
+ });
2420
+ it("still generates the gate wrapper script (for orchestrator to invoke)", () => {
2421
+ const wrapper = files.find((f) => f.path.endsWith("/scripts/gate-quality-check.sh"));
2422
+ expect(wrapper).toBeDefined();
2423
+ expect(wrapper.content).toContain('orchestrator.delegation is "inline"');
2424
+ expect(wrapper.content).toContain("playbook");
2425
+ });
2426
+ it("emits a 'Gates to invoke' cheat sheet in the trigger playbook", () => {
2427
+ const playbook = files.find((f) => f.path === ".claude/commands/run.md");
2428
+ expect(playbook).toBeDefined();
2429
+ expect(playbook.content).toContain("Gates to invoke (inline-orchestrator)");
2430
+ expect(playbook.content).toContain("bash .claude/skills/inline-test/scripts/gate-quality-check.sh");
2431
+ expect(playbook.content).toContain("after `writer`");
2432
+ });
2433
+ });
2434
+ describe("claude-code binding — default delegation: subagent (backward compat)", () => {
2435
+ // No `delegation:` field declared → treat as "subagent" → hooks fire as before.
2436
+ const SUBAGENT_SOURCE = `
2437
+ topology subagent-test : [pipeline] {
2438
+ meta { version: "1.0.0" }
2439
+ orchestrator { model: opus handles: [start] }
2440
+ action start { kind: inline }
2441
+ agent writer {
2442
+ model: sonnet
2443
+ phase: 1
2444
+ tools: [Read, Write]
2445
+ reads: ["workspace/input.md"]
2446
+ writes: ["workspace/draft.md"]
2447
+ permissions: autonomous
2448
+ }
2449
+ flow {
2450
+ start -> writer
2451
+ }
2452
+ gates {
2453
+ gate quality-check {
2454
+ after: writer
2455
+ run: "scripts/check.sh"
2456
+ on-fail: bounce-back
2457
+ behavior: blocking
2458
+ }
2459
+ }
2460
+ }
2461
+ `;
2462
+ const ast = parse(SUBAGENT_SOURCE);
2463
+ const files = claudeCodeBinding.scaffold(ast);
2464
+ const settings = JSON.parse(files.find((f) => f.path === ".claude/settings.json").content);
2465
+ it("emits SubagentStop hook (default behavior preserved)", () => {
2466
+ const hooks = settings.hooks?.SubagentStop ?? [];
2467
+ expect(hooks.length).toBe(1);
2468
+ expect(hooks[0].matcher).toBe("writer");
2469
+ });
2470
+ it("wrapper script header documents the SubagentStop enforcement", () => {
2471
+ const wrapper = files.find((f) => f.path.endsWith("/scripts/gate-quality-check.sh"));
2472
+ expect(wrapper.content).toContain('SubagentStop hook on agent "writer"');
2473
+ expect(wrapper.content).toContain("Exit 2 blocks");
2474
+ });
2475
+ });
2476
+ // ---------------------------------------------------------------------------
2477
+ // Cross-binding: AGENTOPOLOGY_STUB marker uniformity (#4)
2478
+ //
2479
+ // All bindings that scaffold gate scripts must emit them via the shared
2480
+ // shellStub() util — which embeds the AGENTOPOLOGY_STUB marker. This test
2481
+ // loops over every binding and verifies that at least one stub file in its
2482
+ // scaffold output is detectable by isStubContent().
2483
+ // ---------------------------------------------------------------------------
2484
+ describe("Stub marker uniformity across bindings (#4)", () => {
2485
+ // The baseline TOPOLOGY_SOURCE at the top of this file has gate
2486
+ // `quality-check` whose `run: "scripts/lint.sh"` becomes a stub. Bindings
2487
+ // that scaffold gates (5 of 7) must produce a stub for it. The two that
2488
+ // don't (copilot-cli, cursor) have no gate-script generation and are
2489
+ // excluded.
2490
+ const stubProducers = ["claude-code", "codex", "gemini-cli", "kiro", "openclaw"];
2491
+ for (const bindingName of stubProducers) {
2492
+ it(`${bindingName} emits at least one stub with the AGENTOPOLOGY_STUB marker`, () => {
2493
+ const files = scaffoldBinding(bindingName);
2494
+ const stubs = files.filter((f) => isStubContent(f.content));
2495
+ expect(stubs.length, `${bindingName} should emit at least one stub`).toBeGreaterThan(0);
2496
+ // Every stub must contain the literal marker string (pinned by the
2497
+ // STUB_MARKER constant — see lib/stub.ts).
2498
+ for (const s of stubs) {
2499
+ expect(s.content).toContain(STUB_MARKER);
2500
+ }
2501
+ });
2502
+ }
2503
+ it("copilot-cli produces NO stubs (no shell-script scaffolds)", () => {
2504
+ // Sanity check — confirms the two non-stub-producing bindings stay
2505
+ // in their respective lanes. If this fails, copilot-cli probably
2506
+ // grew a gate-script path and needs the same marker treatment.
2507
+ const files = scaffoldBinding("copilot-cli");
2508
+ const stubs = files.filter((f) => isStubContent(f.content));
2509
+ expect(stubs).toEqual([]);
2510
+ });
2511
+ });
2089
2512
  //# sourceMappingURL=bindings.test.js.map