switchroom 0.18.3 → 0.18.7
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/agent-scheduler/index.js +3 -1
- package/dist/auth-broker/index.js +3 -1
- package/dist/cli/autoaccept-poll.js +140 -33
- package/dist/cli/notion-write-pretool.mjs +3 -1
- package/dist/cli/switchroom.js +386 -128
- package/dist/host-control/main.js +4 -2
- package/dist/vault/approvals/kernel-server.js +3 -1
- package/dist/vault/broker/server.js +38 -8
- package/package.json +3 -3
- package/profiles/_base/cron-session.sh.hbs +55 -16
- package/profiles/_base/start.sh.hbs +35 -16
- package/profiles/default/CLAUDE.md.hbs +1 -1
- package/skills/switchroom-release/SKILL.md +78 -0
- package/telegram-plugin/auth-snapshot-format.ts +15 -1
- package/telegram-plugin/dist/bridge/bridge.js +22 -0
- package/telegram-plugin/dist/gateway/gateway.js +2852 -1032
- package/telegram-plugin/dist/server.js +24 -0
- package/telegram-plugin/gateway/always-allow-persist-queue.ts +438 -0
- package/telegram-plugin/gateway/approval-timeout-inbound-builders.ts +150 -0
- package/telegram-plugin/gateway/clean-shutdown-marker.ts +68 -20
- package/telegram-plugin/gateway/gateway.ts +1331 -151
- package/telegram-plugin/gateway/inbound-spool.ts +2 -1
- package/telegram-plugin/gateway/inject-handler.test.ts +19 -0
- package/telegram-plugin/gateway/inject-handler.ts +17 -0
- package/telegram-plugin/gateway/ipc-protocol.ts +44 -2
- package/telegram-plugin/gateway/ipc-server.ts +40 -0
- package/telegram-plugin/gateway/model-command.ts +212 -51
- package/telegram-plugin/gateway/pending-card-expiry.ts +98 -0
- package/telegram-plugin/gateway/pending-card-store.ts +173 -0
- package/telegram-plugin/gateway/pending-inbound-buffer.ts +12 -2
- package/telegram-plugin/gateway/resolve-person.ts +304 -0
- package/telegram-plugin/gateway/resume-inbound-builder.ts +240 -2
- package/telegram-plugin/gateway/session-model-source.ts +73 -0
- package/telegram-plugin/gateway/unhandled-rejection-policy.ts +21 -1
- package/telegram-plugin/gateway/worker-feed-dispatch.ts +24 -1
- package/telegram-plugin/hooks/silent-end-scan.mjs +164 -40
- package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +30 -7
- package/telegram-plugin/model-label.ts +69 -0
- package/telegram-plugin/operator-events.ts +45 -0
- package/telegram-plugin/pending-work-progress.ts +42 -7
- package/telegram-plugin/permission-diff.ts +128 -0
- package/telegram-plugin/quota-bar-format.ts +360 -0
- package/telegram-plugin/registry/subagents-schema.ts +80 -1
- package/telegram-plugin/registry/subagents.test.ts +90 -0
- package/telegram-plugin/session-tail.ts +28 -0
- package/telegram-plugin/silent-end.ts +49 -4
- package/telegram-plugin/subagent-watcher.ts +249 -46
- package/telegram-plugin/tests/always-allow-persist-queue.test.ts +529 -0
- package/telegram-plugin/tests/approval-timeout-inbound-builders.test.ts +94 -0
- package/telegram-plugin/tests/auth-snapshot-format.test.ts +21 -0
- package/telegram-plugin/tests/button-tap-turn-gated.test.ts +263 -0
- package/telegram-plugin/tests/gateway-boot-marker-clear.test.ts +3 -3
- package/telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts +85 -27
- package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +4 -2
- package/telegram-plugin/tests/ipc-server-query-pending-permission.test.ts +157 -0
- package/telegram-plugin/tests/mental-model-propose-callback-gate.test.ts +8 -5
- package/telegram-plugin/tests/model-command.test.ts +202 -42
- package/telegram-plugin/tests/model-label.test.ts +64 -0
- package/telegram-plugin/tests/operator-events.test.ts +17 -0
- package/telegram-plugin/tests/pending-card-durability-wiring.test.ts +202 -0
- package/telegram-plugin/tests/pending-card-expiry.test.ts +190 -0
- package/telegram-plugin/tests/pending-card-store.test.ts +173 -0
- package/telegram-plugin/tests/pending-work-progress.test.ts +116 -3
- package/telegram-plugin/tests/permission-diff.test.ts +111 -0
- package/telegram-plugin/tests/quota-bar-format.test.ts +444 -0
- package/telegram-plugin/tests/resolve-person.test.ts +290 -0
- package/telegram-plugin/tests/resume-inbound-builder.test.ts +286 -0
- package/telegram-plugin/tests/session-model-source.test.ts +67 -0
- package/telegram-plugin/tests/session-tail.test.ts +64 -0
- package/telegram-plugin/tests/silent-end-interrupt-stop-integration.test.ts +53 -0
- package/telegram-plugin/tests/silent-end-interrupt-stop-scan.test.ts +138 -0
- package/telegram-plugin/tests/silent-end.test.ts +46 -1
- package/telegram-plugin/tests/subagent-tracker-hooks.test.ts +39 -0
- package/telegram-plugin/tests/subagent-watcher-boot-promotion-replay.test.ts +107 -4
- package/telegram-plugin/tests/subagent-watcher-handback-gaps.test.ts +42 -4
- package/telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts +47 -0
- package/telegram-plugin/tests/subagent-watcher-terminated-ids-cap.test.ts +150 -0
- package/telegram-plugin/tests/subagent-watcher.test.ts +115 -0
- package/telegram-plugin/tests/tool-activity-summary.test.ts +37 -0
- package/telegram-plugin/tests/typing-wrap.test.ts +23 -0
- package/telegram-plugin/tests/unhandled-rejection-policy.test.ts +19 -0
- package/telegram-plugin/tests/worker-activity-feed.test.ts +108 -0
- package/telegram-plugin/tests/worker-feed-dispatch.test.ts +126 -0
- package/telegram-plugin/tool-activity-summary.ts +22 -2
- package/telegram-plugin/typing-wrap.ts +72 -25
- package/telegram-plugin/worker-activity-feed.ts +229 -15
- package/profiles/default/CLAUDE.md +0 -116
- package/telegram-plugin/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
- package/vendor/hindsight-memory/scripts/__pycache__/directive_verify.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/__pycache__/drain_pending.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/__pycache__/recall.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/__pycache__/retain.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/__pycache__/session_end.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/bank.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/client.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/config.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/content.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/daemon.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/directives.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/gateway_ipc.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/llm.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/pending.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/state.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/switchroom_envelope.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/__init__.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_config_client_casts.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_config_client_casts.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_capture_nudge.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_capture_nudge.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_verify.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_verify.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directives.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directives.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_gateway_ipc.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_gateway_ipc.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_context_slice.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_context_slice.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_integration.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_integration.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_tag_filters.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_tag_filters.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_topic_filter.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_topic_filter.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_trivial_skip.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_trivial_skip.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_retain_window.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_retain_window.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_sender_routing.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_sender_routing.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_switchroom_envelope.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/conftest.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_bank.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_bank.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_client.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_client.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_config.cpython-313-pytest-9.0.3.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_config.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_config.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_content.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_content.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_drain_pending.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_drain_pending.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_hooks.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_hooks.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_manifest.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_manifest.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_pending.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_pending.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_recall_exit_codes.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_recall_exit_codes.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_session_end_pending.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_session_end_pending.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_state.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_state.cpython-313.pyc +0 -0
|
@@ -49,9 +49,11 @@ function setup(): Capture {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
async function flush(): Promise<void> {
|
|
52
|
-
// Allow the fire-and-forget promise chain in tick() to settle.
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
// Allow the fire-and-forget promise chain in tick() to settle. The chain
|
|
53
|
+
// is `.then(editMessage).then(emitMetric).catch(...)` with an async
|
|
54
|
+
// editMessage, so it spans several microtask hops — a single macrotask
|
|
55
|
+
// boundary (setTimeout 0) drains them all deterministically.
|
|
56
|
+
await new Promise((r) => setTimeout(r, 0))
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
describe('pending-work-progress', () => {
|
|
@@ -558,4 +560,115 @@ describe('pending-work-progress', () => {
|
|
|
558
560
|
await flush()
|
|
559
561
|
expect(cap.edits).toHaveLength(1)
|
|
560
562
|
})
|
|
563
|
+
|
|
564
|
+
// ─── review finding #2: transport-hiccup classification ────────────────────
|
|
565
|
+
// A best-effort liveness surface must not log a scary "edit failed" warning
|
|
566
|
+
// for recoverable/permanent transport outcomes, and must stop retrying a
|
|
567
|
+
// dead anchor. "not modified" counts as success; "gone" clears state; 429 /
|
|
568
|
+
// transient leave state for the next tick — all silently.
|
|
569
|
+
it('#2: "message is not modified" counts as a successful edit (no warning, metric emitted)', async () => {
|
|
570
|
+
const cap = setup()
|
|
571
|
+
const errs: string[] = []
|
|
572
|
+
const origStderr = process.stderr.write.bind(process.stderr)
|
|
573
|
+
process.stderr.write = ((s: string) => { errs.push(s); return true }) as typeof process.stderr.write
|
|
574
|
+
try {
|
|
575
|
+
let calls = 0
|
|
576
|
+
__setDepsForTests({
|
|
577
|
+
editMessage: async () => {
|
|
578
|
+
calls++
|
|
579
|
+
if (calls === 1) throw new Error('Bad Request: message is not modified')
|
|
580
|
+
cap.edits.push({} as PendingProgressEditCtx)
|
|
581
|
+
},
|
|
582
|
+
emitMetric: (e) => cap.metrics.push(e),
|
|
583
|
+
nowMs: () => cap.now,
|
|
584
|
+
})
|
|
585
|
+
startTurn(KEY)
|
|
586
|
+
noteAsyncDispatch(KEY)
|
|
587
|
+
noteOutbound(KEY, { messageId: 100, text: 'working' })
|
|
588
|
+
noteTurnEnd(KEY)
|
|
589
|
+
|
|
590
|
+
cap.now = EDIT_INTERVAL_MS
|
|
591
|
+
__tickForTests(cap.now)
|
|
592
|
+
// The .catch hop needs an extra microtask drain beyond the 2-await flush.
|
|
593
|
+
await flush()
|
|
594
|
+
await Promise.resolve()
|
|
595
|
+
// The not-modified outcome is success — an `edited` metric was emitted.
|
|
596
|
+
expect(cap.metrics.some((m) => m.kind === 'pending_progress_edited')).toBe(true)
|
|
597
|
+
// No scary "edit failed" stderr for a transport-class outcome.
|
|
598
|
+
expect(errs.some((e) => e.includes('edit failed'))).toBe(false)
|
|
599
|
+
} finally {
|
|
600
|
+
process.stderr.write = origStderr
|
|
601
|
+
}
|
|
602
|
+
})
|
|
603
|
+
|
|
604
|
+
it('#2: a gone anchor (message not found) clears state so the tick stops retrying it, silently', async () => {
|
|
605
|
+
const cap = setup()
|
|
606
|
+
const errs: string[] = []
|
|
607
|
+
const origStderr = process.stderr.write.bind(process.stderr)
|
|
608
|
+
process.stderr.write = ((s: string) => { errs.push(s); return true }) as typeof process.stderr.write
|
|
609
|
+
try {
|
|
610
|
+
let calls = 0
|
|
611
|
+
__setDepsForTests({
|
|
612
|
+
editMessage: async () => {
|
|
613
|
+
calls++
|
|
614
|
+
throw new Error('Bad Request: message to edit not found')
|
|
615
|
+
},
|
|
616
|
+
emitMetric: (e) => cap.metrics.push(e),
|
|
617
|
+
nowMs: () => cap.now,
|
|
618
|
+
})
|
|
619
|
+
startTurn(KEY)
|
|
620
|
+
noteAsyncDispatch(KEY)
|
|
621
|
+
noteOutbound(KEY, { messageId: 100, text: 'working' })
|
|
622
|
+
noteTurnEnd(KEY)
|
|
623
|
+
|
|
624
|
+
cap.now = EDIT_INTERVAL_MS
|
|
625
|
+
__tickForTests(cap.now)
|
|
626
|
+
await flush()
|
|
627
|
+
await Promise.resolve()
|
|
628
|
+
expect(calls).toBe(1)
|
|
629
|
+
// No warning for a transport-gone outcome.
|
|
630
|
+
expect(errs.some((e) => e.includes('edit failed'))).toBe(false)
|
|
631
|
+
|
|
632
|
+
// Next tick — the anchor was cleared, so no retry against the dead id.
|
|
633
|
+
cap.now = EDIT_INTERVAL_MS * 3
|
|
634
|
+
__tickForTests(cap.now)
|
|
635
|
+
await flush()
|
|
636
|
+
await Promise.resolve()
|
|
637
|
+
expect(calls).toBe(1) // no second attempt
|
|
638
|
+
} finally {
|
|
639
|
+
process.stderr.write = origStderr
|
|
640
|
+
}
|
|
641
|
+
})
|
|
642
|
+
|
|
643
|
+
it('#2: a 429 / transient error leaves state intact for the next tick, silently', async () => {
|
|
644
|
+
const cap = setup()
|
|
645
|
+
const errs: string[] = []
|
|
646
|
+
const origStderr = process.stderr.write.bind(process.stderr)
|
|
647
|
+
process.stderr.write = ((s: string) => { errs.push(s); return true }) as typeof process.stderr.write
|
|
648
|
+
try {
|
|
649
|
+
let calls = 0
|
|
650
|
+
__setDepsForTests({
|
|
651
|
+
editMessage: async () => {
|
|
652
|
+
calls++
|
|
653
|
+
throw { error_code: 429, parameters: { retry_after: 1 }, message: 'Too Many Requests' }
|
|
654
|
+
},
|
|
655
|
+
emitMetric: (e) => cap.metrics.push(e),
|
|
656
|
+
nowMs: () => cap.now,
|
|
657
|
+
})
|
|
658
|
+
startTurn(KEY)
|
|
659
|
+
noteAsyncDispatch(KEY)
|
|
660
|
+
noteOutbound(KEY, { messageId: 100, text: 'working' })
|
|
661
|
+
noteTurnEnd(KEY)
|
|
662
|
+
|
|
663
|
+
cap.now = EDIT_INTERVAL_MS
|
|
664
|
+
__tickForTests(cap.now)
|
|
665
|
+
await flush()
|
|
666
|
+
await Promise.resolve()
|
|
667
|
+
// State survives — the anchor is still there to retry next tick.
|
|
668
|
+
expect(__getStateForTests(KEY)?.anchorMessageId).toBe(100)
|
|
669
|
+
expect(errs.some((e) => e.includes('edit failed'))).toBe(false)
|
|
670
|
+
} finally {
|
|
671
|
+
process.stderr.write = origStderr
|
|
672
|
+
}
|
|
673
|
+
})
|
|
561
674
|
})
|
|
@@ -178,6 +178,89 @@ describe('synthesizeAllowRuleDiff — structural cases', () => {
|
|
|
178
178
|
}
|
|
179
179
|
})
|
|
180
180
|
|
|
181
|
+
it('(a2) multi-line flow list (clerk\'s exact shape, #2973): appends before the closing ]', () => {
|
|
182
|
+
// Mirrors switchroom.yaml:507-514 — `allow:` has an empty inline
|
|
183
|
+
// remainder, `[` opens on the NEXT line, one entry per line, `]`
|
|
184
|
+
// closes on its own line with no trailing comma on the last entry.
|
|
185
|
+
const cfg = cfgWith(
|
|
186
|
+
[
|
|
187
|
+
' clerk:',
|
|
188
|
+
' topic_name: clerk',
|
|
189
|
+
' purpose: clerk',
|
|
190
|
+
' tools:',
|
|
191
|
+
' allow:',
|
|
192
|
+
' [',
|
|
193
|
+
' all,',
|
|
194
|
+
' mcp__ms-365__*,',
|
|
195
|
+
' mcp__ms-365__verify-login,',
|
|
196
|
+
' mcp__ms-365__list-mail-messages',
|
|
197
|
+
' ]',
|
|
198
|
+
' model: opus',
|
|
199
|
+
].join('\n'),
|
|
200
|
+
)
|
|
201
|
+
const diff = synthesizeAllowRuleDiff({ agentName: 'clerk', rule: 'Skill(calendar)', configText: cfg })
|
|
202
|
+
expect(diff).not.toBeNull()
|
|
203
|
+
expect(diff).toContain('--- a/switchroom.yaml')
|
|
204
|
+
expect(diff).toContain('+++ b/switchroom.yaml')
|
|
205
|
+
const res = applyViaValidator(cfg, diff!)
|
|
206
|
+
expect(res).toMatchObject({ ok: true })
|
|
207
|
+
if (res.ok) {
|
|
208
|
+
expect(allowListFor(res.postApplyContent, 'clerk')).toEqual([
|
|
209
|
+
'all',
|
|
210
|
+
'mcp__ms-365__*',
|
|
211
|
+
'mcp__ms-365__verify-login',
|
|
212
|
+
'mcp__ms-365__list-mail-messages',
|
|
213
|
+
'Skill(calendar)',
|
|
214
|
+
])
|
|
215
|
+
}
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('(a2) multi-line flow list: last entry already comma-terminated', () => {
|
|
219
|
+
const cfg = cfgWith(
|
|
220
|
+
[
|
|
221
|
+
' clerk:',
|
|
222
|
+
' topic_name: clerk',
|
|
223
|
+
' purpose: clerk',
|
|
224
|
+
' tools:',
|
|
225
|
+
' allow:',
|
|
226
|
+
' [',
|
|
227
|
+
' Read,',
|
|
228
|
+
' Grep,',
|
|
229
|
+
' ]',
|
|
230
|
+
' model: opus',
|
|
231
|
+
].join('\n'),
|
|
232
|
+
)
|
|
233
|
+
const diff = synthesizeAllowRuleDiff({ agentName: 'clerk', rule: 'Bash', configText: cfg })
|
|
234
|
+
expect(diff).not.toBeNull()
|
|
235
|
+
const res = applyViaValidator(cfg, diff!)
|
|
236
|
+
expect(res).toMatchObject({ ok: true })
|
|
237
|
+
if (res.ok) {
|
|
238
|
+
expect(allowListFor(res.postApplyContent, 'clerk')).toEqual(['Read', 'Grep', 'Bash'])
|
|
239
|
+
}
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('(a2) multi-line flow list: empty list across lines gets first entry', () => {
|
|
243
|
+
const cfg = cfgWith(
|
|
244
|
+
[
|
|
245
|
+
' clerk:',
|
|
246
|
+
' topic_name: clerk',
|
|
247
|
+
' purpose: clerk',
|
|
248
|
+
' tools:',
|
|
249
|
+
' allow:',
|
|
250
|
+
' [',
|
|
251
|
+
' ]',
|
|
252
|
+
' model: opus',
|
|
253
|
+
].join('\n'),
|
|
254
|
+
)
|
|
255
|
+
const diff = synthesizeAllowRuleDiff({ agentName: 'clerk', rule: 'Bash', configText: cfg })
|
|
256
|
+
expect(diff).not.toBeNull()
|
|
257
|
+
const res = applyViaValidator(cfg, diff!)
|
|
258
|
+
expect(res).toMatchObject({ ok: true })
|
|
259
|
+
if (res.ok) {
|
|
260
|
+
expect(allowListFor(res.postApplyContent, 'clerk')).toEqual(['Bash'])
|
|
261
|
+
}
|
|
262
|
+
})
|
|
263
|
+
|
|
181
264
|
it('multi-agent: only the target agent is touched', () => {
|
|
182
265
|
const cfg = cfgWith(
|
|
183
266
|
[
|
|
@@ -250,6 +333,34 @@ describe('extractAddedAllowRule — round-trips the synthesized diff', () => {
|
|
|
250
333
|
expect(extractAddedAllowRule(diff)).toBe('Skill(mail)')
|
|
251
334
|
})
|
|
252
335
|
|
|
336
|
+
it('multi-line flow-list append (clerk\'s exact shape, #2973)', () => {
|
|
337
|
+
const cfg = cfgWith(
|
|
338
|
+
[
|
|
339
|
+
' clerk:',
|
|
340
|
+
' topic_name: clerk',
|
|
341
|
+
' purpose: clerk',
|
|
342
|
+
' tools:',
|
|
343
|
+
' allow:',
|
|
344
|
+
' [',
|
|
345
|
+
' all,',
|
|
346
|
+
' mcp__ms-365__verify-login',
|
|
347
|
+
' ]',
|
|
348
|
+
' model: opus',
|
|
349
|
+
].join('\n'),
|
|
350
|
+
)
|
|
351
|
+
const diff = synthesizeAllowRuleDiff({ agentName: 'clerk', rule: 'Skill(calendar)', configText: cfg })!
|
|
352
|
+
expect(extractAddedAllowRule(diff)).toBe('Skill(calendar)')
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
it('multi-line flow-list first-entry insert (empty list across lines)', () => {
|
|
356
|
+
const cfg = cfgWith(
|
|
357
|
+
[' clerk:', ' topic_name: clerk', ' purpose: clerk',
|
|
358
|
+
' tools:', ' allow:', ' [', ' ]', ' model: opus'].join('\n'),
|
|
359
|
+
)
|
|
360
|
+
const diff = synthesizeAllowRuleDiff({ agentName: 'clerk', rule: 'Bash', configText: cfg })!
|
|
361
|
+
expect(extractAddedAllowRule(diff)).toBe('Bash')
|
|
362
|
+
})
|
|
363
|
+
|
|
253
364
|
it('absent tools.allow (case c) — extracts the single added rule', () => {
|
|
254
365
|
const cfg = cfgWith([' carrie:', ' topic_name: carrie',
|
|
255
366
|
' purpose: carrie', ' model: sonnet', ' role: assistant'].join('\n'))
|