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.
- package/dist/analyzer/__tests__/observability-gap.test.d.ts +8 -0
- package/dist/analyzer/__tests__/observability-gap.test.d.ts.map +1 -0
- package/dist/analyzer/__tests__/observability-gap.test.js +132 -0
- package/dist/analyzer/__tests__/observability-gap.test.js.map +1 -0
- package/dist/analyzer/index.d.ts +1 -1
- package/dist/analyzer/index.d.ts.map +1 -1
- package/dist/analyzer/index.js +26 -0
- package/dist/analyzer/index.js.map +1 -1
- package/dist/bindings/__tests__/bindings.test.js +423 -0
- package/dist/bindings/__tests__/bindings.test.js.map +1 -1
- package/dist/bindings/__tests__/observability.test.d.ts +9 -0
- package/dist/bindings/__tests__/observability.test.d.ts.map +1 -0
- package/dist/bindings/__tests__/observability.test.js +136 -0
- package/dist/bindings/__tests__/observability.test.js.map +1 -0
- package/dist/bindings/__tests__/stub-generation.test.d.ts +16 -0
- package/dist/bindings/__tests__/stub-generation.test.d.ts.map +1 -0
- package/dist/bindings/__tests__/stub-generation.test.js +117 -0
- package/dist/bindings/__tests__/stub-generation.test.js.map +1 -0
- package/dist/bindings/claude-code.d.ts.map +1 -1
- package/dist/bindings/claude-code.js +188 -37
- package/dist/bindings/claude-code.js.map +1 -1
- package/dist/bindings/codex.d.ts.map +1 -1
- package/dist/bindings/codex.js +1 -12
- package/dist/bindings/codex.js.map +1 -1
- package/dist/bindings/cursor.d.ts.map +1 -1
- package/dist/bindings/cursor.js +0 -12
- package/dist/bindings/cursor.js.map +1 -1
- package/dist/bindings/gemini-cli.d.ts.map +1 -1
- package/dist/bindings/gemini-cli.js +1 -12
- package/dist/bindings/gemini-cli.js.map +1 -1
- package/dist/bindings/kiro.d.ts.map +1 -1
- package/dist/bindings/kiro.js +1 -12
- package/dist/bindings/kiro.js.map +1 -1
- package/dist/bindings/lib/__tests__/stub.test.d.ts +8 -0
- package/dist/bindings/lib/__tests__/stub.test.d.ts.map +1 -0
- package/dist/bindings/lib/__tests__/stub.test.js +66 -0
- package/dist/bindings/lib/__tests__/stub.test.js.map +1 -0
- package/dist/bindings/lib/stub.d.ts +56 -0
- package/dist/bindings/lib/stub.d.ts.map +1 -0
- package/dist/bindings/lib/stub.js +69 -0
- package/dist/bindings/lib/stub.js.map +1 -0
- package/dist/bindings/openclaw.d.ts.map +1 -1
- package/dist/bindings/openclaw.js +1 -12
- package/dist/bindings/openclaw.js.map +1 -1
- package/dist/bindings/types.d.ts +5 -0
- package/dist/bindings/types.d.ts.map +1 -1
- package/dist/bindings/types.js.map +1 -1
- package/dist/cli/index.js +89 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/docs/content.d.ts.map +1 -1
- package/dist/docs/content.js +19 -5
- package/dist/docs/content.js.map +1 -1
- package/dist/parser/__tests__/parser.test.js +88 -3
- package/dist/parser/__tests__/parser.test.js.map +1 -1
- package/dist/parser/ast.d.ts +15 -0
- package/dist/parser/ast.d.ts.map +1 -1
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/parser/index.js +12 -0
- package/dist/parser/index.js.map +1 -1
- package/dist/parser/validator.d.ts.map +1 -1
- package/dist/parser/validator.js +65 -9
- package/dist/parser/validator.js.map +1 -1
- package/dist/scaffold/__tests__/scaffold.test.js +20 -0
- package/dist/scaffold/__tests__/scaffold.test.js.map +1 -1
- package/dist/scaffold/__tests__/settings-merge.test.d.ts +15 -0
- package/dist/scaffold/__tests__/settings-merge.test.d.ts.map +1 -0
- package/dist/scaffold/__tests__/settings-merge.test.js +127 -0
- package/dist/scaffold/__tests__/settings-merge.test.js.map +1 -0
- package/dist/scaffold/incremental.d.ts.map +1 -1
- package/dist/scaffold/incremental.js +11 -2
- package/dist/scaffold/incremental.js.map +1 -1
- package/dist/scaffold/merge.d.ts +16 -0
- package/dist/scaffold/merge.d.ts.map +1 -1
- package/dist/scaffold/merge.js +77 -0
- package/dist/scaffold/merge.js.map +1 -1
- package/dist/scaffold/types.d.ts +2 -0
- package/dist/scaffold/types.d.ts.map +1 -1
- package/package.json +1 -1
- 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
|