@cohortapp/agent-sdk 2.11.15 → 2.13.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/.env.example +37 -22
- package/README.md +2 -0
- package/bin/maestro.mjs +117 -39
- package/bin/maestro.test.mjs +175 -5
- package/docs/guides/front-door-session.md +313 -0
- package/docs/guides/mac-mini.md +100 -28
- package/docs/guides/org-onboarding.md +1 -1
- package/docs/guides/setup-wizard.md +9 -5
- package/docs/runbooks/cohort-cutover.md +11 -1
- package/docs/runbooks/mac-mini-bootstrap.md +38 -63
- package/lib/cadence-bus-requeue.test.mjs +83 -0
- package/lib/cadence-bus.mjs +43 -7
- package/lib/channels/inbox-item.mjs +59 -2
- package/lib/cli/board.mjs +285 -0
- package/lib/cli/board.test.mjs +227 -0
- package/lib/cli/design.mjs +185 -0
- package/lib/cli/design.test.mjs +270 -0
- package/lib/cli/doctor-checks.mjs +441 -0
- package/lib/cli/doctor-checks.test.mjs +336 -0
- package/lib/cli/global-setup-extras.mjs +454 -0
- package/lib/cli/global-setup-extras.test.mjs +462 -0
- package/lib/cli/inbox.mjs +304 -0
- package/lib/cli/inbox.test.mjs +230 -0
- package/lib/cli/session-ack.mjs +63 -0
- package/lib/cli/session-ack.test.mjs +63 -0
- package/lib/cli/session.mjs +760 -0
- package/lib/cli/session.test.mjs +613 -0
- package/lib/collective/global-config.mjs +209 -6
- package/lib/collective/global-config.test.mjs +145 -0
- package/lib/collective/global-skills.mjs +145 -0
- package/lib/collective/global-skills.test.mjs +126 -0
- package/lib/collective/presence.mjs +4 -3
- package/lib/collective/vendor-skills.mjs +305 -0
- package/lib/collective/vendor-skills.test.mjs +306 -0
- package/lib/comms/send-gate.mjs +115 -0
- package/lib/comms/send-gate.test.mjs +113 -0
- package/lib/design/design-md.mjs +793 -0
- package/lib/design/design-md.test.mjs +318 -0
- package/lib/design/fixtures/DESIGN.golden.md +238 -0
- package/lib/design/fixtures/PRODUCT.golden.md +67 -0
- package/lib/design/fixtures/foundation.json +133 -0
- package/lib/design/refresh-gate.mjs +154 -0
- package/lib/design/refresh-gate.test.mjs +144 -0
- package/lib/design/write.mjs +275 -0
- package/lib/design/write.test.mjs +241 -0
- package/lib/feature-init.mjs +2 -2
- package/lib/mcp/server.test.mjs +9 -4
- package/lib/model-router/spawn.test.mjs +21 -0
- package/lib/org/board-mine-cache.mjs +99 -0
- package/lib/org/board-mine-cache.test.mjs +53 -0
- package/lib/org/board.mjs +11 -0
- package/lib/org/board.test.mjs +11 -1
- package/lib/org/client.mjs +36 -0
- package/lib/org/client.test.mjs +46 -0
- package/lib/org/inbound/directedness.mjs +18 -2
- package/lib/org/inbound/directedness.test.mjs +58 -0
- package/lib/org/inbound/index.mjs +8 -1
- package/lib/org/inbound/index.test.mjs +22 -0
- package/lib/org/mesh-directives.test.mjs +110 -0
- package/lib/org/mesh.mjs +61 -1
- package/lib/org/protocol.checksum +1 -1
- package/lib/org/protocol.mjs +52 -0
- package/lib/org/protocol.test.mjs +12 -1
- package/lib/org/registry.mjs +3 -2
- package/lib/org/tool-surface.mjs +120 -0
- package/lib/org/tool-surface.test.mjs +118 -5
- package/lib/prompts/parallelism.mjs +79 -0
- package/lib/prompts/parallelism.test.mjs +177 -0
- package/lib/security/external-content.mjs +1 -1
- package/lib/security/external-content.test.mjs +17 -0
- package/lib/session/config.mjs +137 -0
- package/lib/session/config.test.mjs +92 -0
- package/lib/session/feed-core.mjs +229 -0
- package/lib/session/feed-core.test.mjs +198 -0
- package/lib/session/first-run.mjs +126 -0
- package/lib/session/first-run.test.mjs +121 -0
- package/lib/session/frontdoor.mjs +266 -0
- package/lib/session/frontdoor.test.mjs +205 -0
- package/lib/session/handoffs.mjs +295 -0
- package/lib/session/handoffs.test.mjs +183 -0
- package/lib/session/identity.mjs +220 -0
- package/lib/session/identity.test.mjs +180 -0
- package/lib/session/inbox-claims.mjs +434 -0
- package/lib/session/inbox-claims.test.mjs +286 -0
- package/lib/session/launch-args.mjs +161 -0
- package/lib/session/launch-args.test.mjs +157 -0
- package/lib/session/liveness.mjs +174 -0
- package/lib/session/liveness.test.mjs +100 -0
- package/lib/session/status-summary.mjs +172 -0
- package/lib/session/status-summary.test.mjs +118 -0
- package/lib/session-permissions.mjs +39 -3
- package/lib/session-permissions.test.mjs +20 -0
- package/lib/setup/claude-probe.mjs +161 -24
- package/lib/setup/claude-probe.test.mjs +187 -0
- package/lib/setup/sections/learning.mjs +2 -1
- package/lib/setup/sections/model.mjs +104 -24
- package/lib/setup/sections/model.test.mjs +240 -0
- package/lib/setup/sections/org.mjs +27 -2
- package/lib/setup/sections/org.test.mjs +35 -2
- package/lib/setup/sections/verify.mjs +5 -0
- package/lib/setup/state.mjs +30 -10
- package/lib/setup/state.test.mjs +24 -1
- package/lib/singleton.js +11 -3
- package/lib/singleton.test.mjs +16 -0
- package/lib/subagents/lock.mjs +1 -1
- package/lib/telemetry/collect.mjs +270 -6
- package/lib/telemetry/collect.test.mjs +196 -1
- package/lib/upgrade/global-refresh.mjs +108 -0
- package/lib/upgrade/global-refresh.test.mjs +65 -0
- package/lib/upgrade/launchd-reconcile.mjs +327 -0
- package/lib/upgrade/launchd-reconcile.test.mjs +272 -0
- package/lib/upgrade/post-steps.mjs +151 -0
- package/lib/upgrade/post-steps.test.mjs +200 -0
- package/lib/upgrade/verify.mjs +215 -0
- package/lib/upgrade/verify.test.mjs +164 -0
- package/lib/voice/outbound.mjs +3 -2
- package/lib/voice/post-call-brief.mjs +2 -1
- package/lib/voice/session-rotation.mjs +6 -1
- package/lib/voice/session-rotation.test.mjs +114 -0
- package/package.json +3 -3
- package/plugins/maestro-skills/plugin.json +25 -1
- package/plugins/maestro-skills/skills/board-work.md +63 -0
- package/plugins/maestro-skills/skills/cohort-design.md +153 -0
- package/plugins/maestro-skills/skills/inbound-triage.md +80 -0
- package/plugins/maestro-skills/skills/main-session.md +102 -0
- package/plugins/maestro-skills/skills/peer-sessions.md +65 -0
- package/plugins/maestro-skills/skills/persona-discipline.md +75 -0
- package/plugins/maestro-skills/vendor/emilkowalski/LICENSE +21 -0
- package/plugins/maestro-skills/vendor/emilkowalski/UPSTREAM.json +70 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/animate/RECIPES.md +324 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/animate/SKILL.md +199 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/animation-vocabulary/SKILL.md +173 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/apple-design/SKILL.md +282 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/emil-design-eng/SKILL.md +674 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/find-animation-opportunities/SKILL.md +132 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/improve-animations/AUDIT.md +115 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/improve-animations/PLAN-TEMPLATE.md +73 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/improve-animations/SKILL.md +101 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/prototype/PICKER.md +197 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/prototype/SKILL.md +90 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/review-animations/SKILL.md +112 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/review-animations/STANDARDS.md +187 -0
- package/plugins/maestro-skills/vendor/impeccable/LICENSE +191 -0
- package/plugins/maestro-skills/vendor/impeccable/NOTICE.md +11 -0
- package/plugins/maestro-skills/vendor/impeccable/SKILL.md +86 -0
- package/plugins/maestro-skills/vendor/impeccable/UPSTREAM.json +201 -0
- package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-asset-producer.md +42 -0
- package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-documenter.md +29 -0
- package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-finish-reviewer.md +43 -0
- package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-manual-edit-applier.md +97 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/adapt.md +312 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/adapt.native.md +58 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/android.md +46 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/animate.md +89 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/audit.md +136 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/audit.native.md +139 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/bolder.md +33 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/clarify.md +94 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/colorize.md +86 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/craft-floor.md +44 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/craft.md +5 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/critique.md +806 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/degraded/asset-producer.md +37 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/degraded/documenter.md +24 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/degraded/finish-reviewer.md +38 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/degraded/manual-edit-applier.md +92 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/delight.md +70 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/distill.md +111 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/doctor.md +54 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/document.md +416 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/extract.md +69 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/harden.md +336 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/hooks.md +111 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/init.md +131 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/ios.md +51 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/layout.md +84 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/live-setup.md +104 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/live.md +325 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/new-work.md +147 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/onboard.md +234 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/operate.md +61 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/optimize.md +258 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/overdrive.md +127 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/polish.md +105 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/quieter.md +99 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/routing.md +24 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/shape.md +59 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/typeset.md +80 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/visualize.md +46 -0
- package/plugins/maestro-skills/vendor/taste-skill/LICENSE +21 -0
- package/plugins/maestro-skills/vendor/taste-skill/UPSTREAM.json +37 -0
- package/plugins/maestro-skills/vendor/taste-skill/skills/minimalist-skill/SKILL.md +85 -0
- package/plugins/maestro-skills/vendor/taste-skill/skills/redesign-skill/SKILL.md +178 -0
- package/plugins/maestro-skills/vendor/taste-skill/skills/soft-skill/SKILL.md +98 -0
- package/plugins/maestro-skills/vendor/taste-skill/skills/taste-skill/SKILL.md +1206 -0
- package/plugins/maestro-skills/vendor/unlazy/LICENSE +21 -0
- package/plugins/maestro-skills/vendor/unlazy/SECURITY.md +72 -0
- package/plugins/maestro-skills/vendor/unlazy/SKILL.md +104 -0
- package/plugins/maestro-skills/vendor/unlazy/UPSTREAM.json +94 -0
- package/plugins/maestro-skills/vendor/unlazy/references/dispatch.md +82 -0
- package/plugins/maestro-skills/vendor/unlazy/references/gates.md +149 -0
- package/plugins/maestro-skills/vendor/unlazy/references/method.md +49 -0
- package/plugins/maestro-skills/vendor/unlazy/references/orchestration.md +107 -0
- package/plugins/maestro-skills/vendor/unlazy/references/parallel.md +133 -0
- package/plugins/maestro-skills/vendor/unlazy/references/token-economy.md +48 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/dispatch-check.mjs +139 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/gate-check.mjs +960 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/gate-lint.mjs +245 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/lib/check-supervisor.mjs +46 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/lib/dispatch.mjs +293 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/lib/gates.mjs +953 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/lib/process-tree.mjs +161 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/lib/regex-worker.mjs +9 -0
- package/plugins/maestro-skills/vendor/unlazy/templates/PLAN.md +116 -0
- package/plugins/maestro-skills/vendor/unlazy/templates/gates-leaf.md +51 -0
- package/plugins/maestro-skills/vendor/unlazy/templates/gates-node.md +51 -0
- package/scaffold/CLAUDE.md +24 -0
- package/scripts/ci/check-durable-write-seam.mjs +147 -0
- package/scripts/ci/check-durable-write-seam.test.mjs +90 -0
- package/scripts/ci/check-skill-packs.mjs +388 -0
- package/scripts/ci/check-skill-packs.test.mjs +495 -0
- package/scripts/ci/check.mjs +6 -0
- package/scripts/collective/hook-runner.mjs +39 -4
- package/scripts/collective/hook-runner.test.mjs +85 -2
- package/scripts/daemon/agent-daemon-board-mine.test.mjs +96 -0
- package/scripts/daemon/agent-daemon-design.test.mjs +238 -0
- package/scripts/daemon/agent-daemon-frontdoor.test.mjs +60 -0
- package/scripts/daemon/agent-daemon.mjs +249 -10
- package/scripts/daemon/agent-daemon.test.mjs +73 -0
- package/scripts/daemon/assurance-e2e.test.mjs +141 -6
- package/scripts/daemon/assurance.mjs +461 -37
- package/scripts/daemon/assurance.test.mjs +408 -43
- package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +393 -0
- package/scripts/daemon/cadence-consumer.mjs +289 -89
- package/scripts/daemon/cadence-handlers.mjs +53 -0
- package/scripts/daemon/classifier.mjs +1 -1
- package/scripts/daemon/dispatcher-resume.test.mjs +166 -0
- package/scripts/daemon/dispatcher.mjs +127 -19
- package/scripts/daemon/health.mjs +12 -1
- package/scripts/daemon/inbox-deferral-session.test.mjs +49 -0
- package/scripts/daemon/inbox-deferral.mjs +6 -0
- package/scripts/daemon/lib/self-echo.mjs +201 -0
- package/scripts/daemon/lib/self-echo.test.mjs +153 -0
- package/scripts/daemon/maestro-daemon.mjs +3 -0
- package/scripts/daemon/prompt-builder.mjs +19 -3
- package/scripts/daemon/responder.mjs +51 -40
- package/scripts/daemon/sdk-version.mjs +51 -0
- package/scripts/daemon/sdk-version.test.mjs +31 -0
- package/scripts/hooks/pre-send-audit.sh +97 -4
- package/scripts/hooks/pre-send-audit.test.mjs +140 -1
- package/scripts/local-triggers/autoupdate.sh +243 -19
- package/scripts/local-triggers/autoupdate.test.mjs +518 -0
- package/scripts/local-triggers/generate-plists.sh +24 -1
- package/scripts/local-triggers/generate-plists.test.mjs +49 -11
- package/scripts/org/send-orgmail.first-contact.test.mjs +102 -0
- package/scripts/org/send-orgmail.mjs +27 -3
- package/scripts/poller/inbox-privilege-injection.test.mjs +167 -0
- package/scripts/poller/slack-poller.mjs +13 -1
- package/scripts/poller/utils.mjs +46 -1
- package/scripts/poller-launchd/install.sh +19 -11
- package/scripts/poller-launchd/install.test.mjs +243 -0
- package/scripts/poller-launchd/launchd-poller-wrapper.sh +92 -0
- package/scripts/poller-launchd/migrate.sh +66 -0
- package/scripts/poller-launchd/poller.plist.template +4 -2
- package/scripts/session/feed.mjs +237 -0
- package/scripts/session/feed.test.mjs +196 -0
- package/scripts/session/supervisor-sh.test.mjs +218 -0
- package/scripts/session/supervisor.mjs +328 -0
- package/scripts/session/supervisor.sh +141 -0
- package/scripts/session/supervisor.test.mjs +482 -0
- package/scripts/setup/configure-macos.sh +250 -55
- package/scripts/setup/configure-macos.test.mjs +306 -0
- package/scripts/setup/init-agent.sh +112 -7
- package/scripts/setup/init-agent.test.mjs +220 -1
- package/scripts/vendor/skill-packs.mjs +354 -0
- package/scripts/vendor/sync-skill-packs.mjs +242 -0
- package/scripts/vendor/sync-skill-packs.test.mjs +103 -0
- package/scripts/watchdog/memory-watchdog.sh +37 -1
- package/scripts/watchdog/memory-watchdog.test.mjs +64 -0
- package/scripts/setup/boot-claude-session.sh +0 -94
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cadence-consumer-frontdoor.test.mjs — the consumer with a live main session
|
|
3
|
+
* (design §3.4) and the F8/F9 cadence fixes.
|
|
4
|
+
*
|
|
5
|
+
* - an escalate/guarded tick is HANDED to a live session (handoff file +
|
|
6
|
+
* rendered prompt on disk + tick processed as "handed-to-session"), never
|
|
7
|
+
* spawned; inline ticks keep running in the consumer
|
|
8
|
+
* - a session that is not live → legacy spawn, unchanged
|
|
9
|
+
* - a handoff past its deadline is re-enqueued with metadata.handoffTimedOut
|
|
10
|
+
* and that tick is NOT handed off again (legacy spawn)
|
|
11
|
+
* - per-cadence in-flight guard: a second tick for a cadence that already
|
|
12
|
+
* has an open handoff / running sub-session is deferred, not spawned twice
|
|
13
|
+
* - a deferred tick is re-keyed to the tail and tickOnce keeps scanning
|
|
14
|
+
* past the deferral instead of breaking (head-of-line block)
|
|
15
|
+
*
|
|
16
|
+
* Every test drives tickOnce() by hand (pollMs is an hour) with an injected
|
|
17
|
+
* spawner, front-door state and clock. No real process is ever launched.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { test } from "node:test";
|
|
21
|
+
import assert from "node:assert/strict";
|
|
22
|
+
import { promises as fsp } from "node:fs";
|
|
23
|
+
import { writeFileSync, mkdirSync, readFileSync, existsSync, readdirSync, chmodSync } from "node:fs";
|
|
24
|
+
import { tmpdir } from "node:os";
|
|
25
|
+
import { join, dirname } from "node:path";
|
|
26
|
+
|
|
27
|
+
import { enqueueTick, listInbox, busDepth, getBusPaths } from "../../lib/cadence-bus.mjs";
|
|
28
|
+
import { listHandoffs, handoffPaths, writeHandoff, ackHandoff, DEFAULT_HANDOFF_DEADLINE_MS, DEFAULT_HANDOFF_RETENTION_MS } from "../../lib/session/handoffs.mjs";
|
|
29
|
+
import { startConsumer as startConsumerRaw, renderCadencePromptBody } from "./cadence-consumer.mjs";
|
|
30
|
+
import { withParallelism, countParallelism, PARALLELISM_DIRECTIVE } from "../../lib/prompts/parallelism.mjs";
|
|
31
|
+
|
|
32
|
+
const ADMIT_ALL = { admit: () => ({ decision: "ADMIT", reason: "test", snapshot: {} }), defaultDeps: () => ({}) };
|
|
33
|
+
const RATE_OK = { checkRateLimit: () => ({ allowed: true, retryAt: null }), classifyStderr: () => false, recordRateLimit: () => {}, recordSuccess: () => {} };
|
|
34
|
+
const BUDGET_OK = { dailyStatus: () => ({ essentialOnly: false }) };
|
|
35
|
+
const LIVE = () => ({ frontDoor: "session", sessionLive: true });
|
|
36
|
+
const NOT_LIVE = () => ({ frontDoor: "session", sessionLive: false });
|
|
37
|
+
|
|
38
|
+
function startConsumer(opts = {}) {
|
|
39
|
+
return startConsumerRaw({ governor: ADMIT_ALL, rateGuard: RATE_OK, budgetGuard: BUDGET_OK, pollMs: 3_600_000, heartbeatMs: 3_600_000, recoveryMs: 3_600_000, budgetEscalateMs: 0, ...opts });
|
|
40
|
+
}
|
|
41
|
+
async function makeAgentRoot() {
|
|
42
|
+
const p = join(tmpdir(), `cadence-frontdoor-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
|
|
43
|
+
await fsp.mkdir(p, { recursive: true });
|
|
44
|
+
writeFileSync(join(p, "config.json"), "{}");
|
|
45
|
+
mkdirSync(join(p, "config"), { recursive: true });
|
|
46
|
+
writeFileSync(join(p, "config", "agent.json"), JSON.stringify({ firstName: "Nova", lastName: "Reyes", name: "Nova Reyes" }));
|
|
47
|
+
return p;
|
|
48
|
+
}
|
|
49
|
+
async function rmRoot(p) { try { await fsp.rm(p, { recursive: true, force: true }); } catch { /* */ } }
|
|
50
|
+
function plantPrompt(root, cadence, body = `# ${cadence} for {{agent.firstName}}`) {
|
|
51
|
+
const path = join(root, "schedules/triggers", `${cadence}.md`);
|
|
52
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
53
|
+
writeFileSync(path, body);
|
|
54
|
+
return path;
|
|
55
|
+
}
|
|
56
|
+
function processedEvents(root) {
|
|
57
|
+
const dir = getBusPaths(root).processed;
|
|
58
|
+
const out = [];
|
|
59
|
+
if (!existsSync(dir)) return out;
|
|
60
|
+
for (const day of readdirSync(dir)) {
|
|
61
|
+
for (const f of readdirSync(join(dir, day))) {
|
|
62
|
+
if (f.endsWith(".json")) out.push(JSON.parse(readFileSync(join(dir, day, f), "utf-8")));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
test("live session: an escalate tick is handed to the session — handoff + rendered prompt on disk, no spawn", async () => {
|
|
69
|
+
const root = await makeAgentRoot();
|
|
70
|
+
plantPrompt(root, "daily-morning-brief");
|
|
71
|
+
const spawns = [];
|
|
72
|
+
const consumer = startConsumer({
|
|
73
|
+
agentRoot: root,
|
|
74
|
+
frontDoorState: LIVE,
|
|
75
|
+
spawnSession: async (o) => { spawns.push(o); return { ok: true, exit_code: 0, duration_ms: 1 }; },
|
|
76
|
+
});
|
|
77
|
+
try {
|
|
78
|
+
const t = enqueueTick({ cadence: "daily-morning-brief", source: "launchd", agentRoot: root });
|
|
79
|
+
const r = await consumer.tickOnce();
|
|
80
|
+
assert.equal(r.processed, 1);
|
|
81
|
+
assert.equal(spawns.length, 0, "must not spawn a sub-session while the session is live");
|
|
82
|
+
const open = listHandoffs(root);
|
|
83
|
+
assert.equal(open.length, 1);
|
|
84
|
+
assert.equal(open[0].tickId, t.id);
|
|
85
|
+
assert.equal(open[0].cadence, "daily-morning-brief");
|
|
86
|
+
assert.equal(open[0].mode, "escalate");
|
|
87
|
+
assert.ok(open[0].deadlineAt, "carries a deadline");
|
|
88
|
+
assert.ok(existsSync(open[0].promptPath), "the rendered prompt is on disk");
|
|
89
|
+
assert.equal(
|
|
90
|
+
readFileSync(open[0].promptPath, "utf-8"),
|
|
91
|
+
withParallelism("# daily-morning-brief for Nova"),
|
|
92
|
+
"identity tokens rendered, and the handed prompt is byte-identical to what a sub-session would have been given",
|
|
93
|
+
);
|
|
94
|
+
assert.equal(busDepth(root).inbox, 0);
|
|
95
|
+
assert.equal(busDepth(root).claimed, 0);
|
|
96
|
+
const done = processedEvents(root).find((e) => e.id === t.id);
|
|
97
|
+
assert.equal(done.result.decision, "handed-to-session");
|
|
98
|
+
assert.equal(consumer.getStats().handed_off, 1);
|
|
99
|
+
assert.equal(consumer.getStats().last_decision, "handed-to-session");
|
|
100
|
+
} finally { await consumer.stop(); await rmRoot(root); }
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("live session: inline ticks still run in the consumer; not-live → legacy spawn", async () => {
|
|
104
|
+
const root = await makeAgentRoot();
|
|
105
|
+
plantPrompt(root, "daily-morning-brief");
|
|
106
|
+
const spawns = [];
|
|
107
|
+
const live = { on: true };
|
|
108
|
+
const consumer = startConsumer({
|
|
109
|
+
agentRoot: root,
|
|
110
|
+
frontDoorState: () => ({ frontDoor: "session", sessionLive: live.on }),
|
|
111
|
+
spawnSession: async (o) => { spawns.push(o); return { ok: true, exit_code: 0, duration_ms: 1 }; },
|
|
112
|
+
});
|
|
113
|
+
try {
|
|
114
|
+
enqueueTick({ cadence: "cadence-bus-heartbeat", source: "manual", agentRoot: root });
|
|
115
|
+
await consumer.tickOnce();
|
|
116
|
+
assert.equal(consumer.getStats().inline, 1);
|
|
117
|
+
assert.deepEqual(listHandoffs(root), [], "inline never hands off");
|
|
118
|
+
|
|
119
|
+
live.on = false;
|
|
120
|
+
enqueueTick({ cadence: "daily-morning-brief", source: "launchd", agentRoot: root });
|
|
121
|
+
await consumer.tickOnce();
|
|
122
|
+
assert.equal(spawns.length, 1, "session not live → the legacy lane spawns");
|
|
123
|
+
assert.deepEqual(listHandoffs(root), []);
|
|
124
|
+
} finally { await consumer.stop(); await rmRoot(root); }
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("handoff timeout: a handoff past its deadline is re-enqueued with handoffTimedOut and then spawned, not handed off again", async () => {
|
|
128
|
+
const root = await makeAgentRoot();
|
|
129
|
+
plantPrompt(root, "daily-morning-brief");
|
|
130
|
+
const spawns = [];
|
|
131
|
+
let now = Date.parse("2026-09-08T10:00:00.000Z");
|
|
132
|
+
const consumer = startConsumer({
|
|
133
|
+
agentRoot: root,
|
|
134
|
+
now: () => now,
|
|
135
|
+
frontDoorState: LIVE,
|
|
136
|
+
spawnSession: async (o) => { spawns.push(o); return { ok: true, exit_code: 0, duration_ms: 1 }; },
|
|
137
|
+
});
|
|
138
|
+
try {
|
|
139
|
+
writeHandoff(root, { tickId: "evt-stuck", cadence: "daily-morning-brief", mode: "escalate", promptPath: "/x", metadata: { reason: "guard:escalate" } }, { now });
|
|
140
|
+
now += DEFAULT_HANDOFF_DEADLINE_MS + 1;
|
|
141
|
+
await consumer.tickOnce();
|
|
142
|
+
assert.equal(consumer.getStats().handoff_timeouts, 1);
|
|
143
|
+
assert.deepEqual(listHandoffs(root), [], "the stuck handoff is retired");
|
|
144
|
+
assert.equal(JSON.parse(readFileSync(join(handoffPaths(root).done, "evt-stuck.json"), "utf-8")).status, "expired");
|
|
145
|
+
// The re-enqueued tick ran on the legacy lane in that same tick (or the next).
|
|
146
|
+
if (spawns.length === 0) await consumer.tickOnce();
|
|
147
|
+
assert.equal(spawns.length, 1, "timed-out handoff → legacy spawn even though the session is live");
|
|
148
|
+
assert.equal(spawns[0].cadence, "daily-morning-brief");
|
|
149
|
+
const ev = processedEvents(root).find((e) => e.source === "handoff-timeout");
|
|
150
|
+
assert.equal(ev.metadata.handoffTimedOut, true);
|
|
151
|
+
assert.equal(ev.metadata.handoffTickId, "evt-stuck");
|
|
152
|
+
assert.equal(ev.result.decision, "escalated");
|
|
153
|
+
assert.deepEqual(listHandoffs(root), [], "never handed off a second time");
|
|
154
|
+
} finally { await consumer.stop(); await rmRoot(root); }
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("per-cadence in-flight guard: a cadence with an open handoff is deferred (re-keyed), a different cadence proceeds", async () => {
|
|
158
|
+
const root = await makeAgentRoot();
|
|
159
|
+
plantPrompt(root, "daily-morning-brief");
|
|
160
|
+
plantPrompt(root, "meeting-action-capture");
|
|
161
|
+
const consumer = startConsumer({ agentRoot: root, frontDoorState: LIVE, spawnSession: async () => ({ ok: true, exit_code: 0 }) });
|
|
162
|
+
try {
|
|
163
|
+
const a = enqueueTick({ cadence: "daily-morning-brief", source: "launchd", agentRoot: root });
|
|
164
|
+
await consumer.tickOnce();
|
|
165
|
+
assert.equal(listHandoffs(root).length, 1);
|
|
166
|
+
await new Promise((r) => setTimeout(r, 2));
|
|
167
|
+
const a2 = enqueueTick({ cadence: "daily-morning-brief", source: "launchd", agentRoot: root });
|
|
168
|
+
await new Promise((r) => setTimeout(r, 2));
|
|
169
|
+
enqueueTick({ cadence: "meeting-action-capture", source: "launchd", agentRoot: root });
|
|
170
|
+
await consumer.tickOnce();
|
|
171
|
+
const open = listHandoffs(root);
|
|
172
|
+
assert.deepEqual(open.map((h) => h.cadence).sort(), ["daily-morning-brief", "meeting-action-capture"], "second cadence handed off; first not duplicated");
|
|
173
|
+
assert.equal(open.filter((h) => h.cadence === "daily-morning-brief").length, 1, "the same cadence is never in flight twice");
|
|
174
|
+
const inbox = listInbox(root);
|
|
175
|
+
assert.equal(inbox.length, 1, "the duplicate tick waits at the tail");
|
|
176
|
+
const waiting = JSON.parse(readFileSync(join(getBusPaths(root).inbox, `${inbox[0]}.json`), "utf-8"));
|
|
177
|
+
assert.equal(waiting.cadence, "daily-morning-brief");
|
|
178
|
+
assert.equal(waiting.metadata.originalId, a2.id);
|
|
179
|
+
assert.equal(waiting.metadata.deferrals, 1);
|
|
180
|
+
assert.notEqual(inbox[0], a2.id, "re-keyed, not requeued under its own id");
|
|
181
|
+
void a;
|
|
182
|
+
} finally { await consumer.stop(); await rmRoot(root); }
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("per-cadence in-flight guard: a running sub-session for a cadence blocks a second spawn of the same cadence", async () => {
|
|
186
|
+
const root = await makeAgentRoot();
|
|
187
|
+
plantPrompt(root, "daily-morning-brief");
|
|
188
|
+
let release;
|
|
189
|
+
const gate = new Promise((r) => { release = r; });
|
|
190
|
+
const spawns = [];
|
|
191
|
+
const consumer = startConsumer({
|
|
192
|
+
agentRoot: root,
|
|
193
|
+
frontDoorState: NOT_LIVE,
|
|
194
|
+
spawnSession: async (o) => { spawns.push(o.cadence); await gate; return { ok: true, exit_code: 0 }; },
|
|
195
|
+
});
|
|
196
|
+
try {
|
|
197
|
+
enqueueTick({ cadence: "daily-morning-brief", source: "launchd", agentRoot: root });
|
|
198
|
+
const first = consumer.tickOnce(); // holds the spawn open
|
|
199
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
200
|
+
assert.equal(spawns.length, 1);
|
|
201
|
+
await new Promise((r) => setTimeout(r, 2));
|
|
202
|
+
enqueueTick({ cadence: "daily-morning-brief", source: "launchd", agentRoot: root });
|
|
203
|
+
await consumer.tickOnce();
|
|
204
|
+
assert.equal(spawns.length, 1, "no second spawn while the first is running");
|
|
205
|
+
assert.equal(listInbox(root).length, 1, "the second tick is deferred to the tail");
|
|
206
|
+
release();
|
|
207
|
+
await first;
|
|
208
|
+
} finally { await consumer.stop(); await rmRoot(root); }
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("head-of-line: a circuit-open deferral is re-keyed to the tail and tickOnce keeps scanning to the next cadence", async () => {
|
|
212
|
+
const root = await makeAgentRoot();
|
|
213
|
+
plantPrompt(root, "weekly-strategic-memo");
|
|
214
|
+
plantPrompt(root, "daily-morning-brief");
|
|
215
|
+
const spawns = [];
|
|
216
|
+
const consumer = startConsumer({
|
|
217
|
+
agentRoot: root,
|
|
218
|
+
frontDoorState: NOT_LIVE,
|
|
219
|
+
backoffSchedule: [0, 0, 0],
|
|
220
|
+
circuitThreshold: 1,
|
|
221
|
+
circuitDurationMs: 60_000,
|
|
222
|
+
maxAttempts: 1,
|
|
223
|
+
spawnSession: async (o) => {
|
|
224
|
+
spawns.push(o.cadence);
|
|
225
|
+
if (o.cadence === "weekly-strategic-memo") return { ok: false, exit_code: 1, error: "fail", duration_ms: 1 };
|
|
226
|
+
return { ok: true, exit_code: 0, duration_ms: 1 };
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
try {
|
|
230
|
+
enqueueTick({ cadence: "weekly-strategic-memo", source: "launchd", agentRoot: root });
|
|
231
|
+
await consumer.tickOnce(); // fails once → circuit opens for weekly-strategic-memo
|
|
232
|
+
assert.equal(consumer.getStats().spawn_failures, 1);
|
|
233
|
+
|
|
234
|
+
const held = enqueueTick({ cadence: "weekly-strategic-memo", source: "launchd", agentRoot: root });
|
|
235
|
+
await new Promise((r) => setTimeout(r, 2));
|
|
236
|
+
enqueueTick({ cadence: "daily-morning-brief", source: "launchd", agentRoot: root });
|
|
237
|
+
const r = await consumer.tickOnce();
|
|
238
|
+
// Before: the circuit skip broke the loop and daily-morning-brief waited a
|
|
239
|
+
// whole poll behind a tick that could not run. Now both are visited.
|
|
240
|
+
assert.equal(r.processed, 2, `both ticks visited in one tick (stats=${JSON.stringify(consumer.getStats())})`);
|
|
241
|
+
assert.deepEqual(spawns, ["weekly-strategic-memo", "daily-morning-brief"]);
|
|
242
|
+
assert.equal(consumer.getStats().skipped_circuit_open, 1);
|
|
243
|
+
const inbox = listInbox(root);
|
|
244
|
+
assert.equal(inbox.length, 1, "the held tick is back in the inbox, once");
|
|
245
|
+
assert.notEqual(inbox[0], held.id, "under a NEW id (tail), not its own");
|
|
246
|
+
const on = JSON.parse(readFileSync(join(getBusPaths(root).inbox, `${inbox[0]}.json`), "utf-8"));
|
|
247
|
+
assert.equal(on.metadata.originalId, held.id);
|
|
248
|
+
assert.equal(on.metadata.deferrals, 1);
|
|
249
|
+
// A further tick with ONLY the held cadence queued does not spin: one visit, one re-key, then it stops.
|
|
250
|
+
const r2 = await consumer.tickOnce();
|
|
251
|
+
assert.equal(r2.processed, 1);
|
|
252
|
+
assert.equal(listInbox(root).length, 1);
|
|
253
|
+
assert.equal(JSON.parse(readFileSync(join(getBusPaths(root).inbox, `${listInbox(root)[0]}.json`), "utf-8")).metadata.deferrals, 2);
|
|
254
|
+
} finally { await consumer.stop(); await rmRoot(root); }
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("a broken front-door reader fails open to the legacy lane", async () => {
|
|
258
|
+
const root = await makeAgentRoot();
|
|
259
|
+
plantPrompt(root, "daily-morning-brief");
|
|
260
|
+
const spawns = [];
|
|
261
|
+
const consumer = startConsumer({
|
|
262
|
+
agentRoot: root,
|
|
263
|
+
frontDoorState: () => { throw new Error("boom"); },
|
|
264
|
+
spawnSession: async (o) => { spawns.push(o); return { ok: true, exit_code: 0 }; },
|
|
265
|
+
});
|
|
266
|
+
try {
|
|
267
|
+
enqueueTick({ cadence: "daily-morning-brief", source: "launchd", agentRoot: root });
|
|
268
|
+
await consumer.tickOnce();
|
|
269
|
+
assert.equal(spawns.length, 1);
|
|
270
|
+
assert.deepEqual(listHandoffs(root), []);
|
|
271
|
+
} finally { await consumer.stop(); await rmRoot(root); }
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("handoff timeout: when the re-enqueue fails the handoff stays OPEN and is retried by the next sweep — the cadence run is never lost", async () => {
|
|
275
|
+
if (process.getuid && process.getuid() === 0) return; // root ignores directory modes
|
|
276
|
+
const root = await makeAgentRoot();
|
|
277
|
+
plantPrompt(root, "daily-morning-brief");
|
|
278
|
+
let now = Date.parse("2026-09-08T10:00:00.000Z");
|
|
279
|
+
const consumer = startConsumer({ agentRoot: root, now: () => now, frontDoorState: LIVE, spawnSession: async () => ({ ok: true, exit_code: 0, duration_ms: 1 }) });
|
|
280
|
+
try {
|
|
281
|
+
writeHandoff(root, { tickId: "evt-stuck", cadence: "daily-morning-brief", mode: "escalate", promptPath: "/x" }, { now });
|
|
282
|
+
now += DEFAULT_HANDOFF_DEADLINE_MS + 1;
|
|
283
|
+
const inbox = getBusPaths(root).inbox;
|
|
284
|
+
chmodSync(inbox, 0o500); // the bus inbox is unwritable for a minute
|
|
285
|
+
try {
|
|
286
|
+
await consumer.tickOnce();
|
|
287
|
+
assert.deepEqual(listHandoffs(root).map((h) => h.tickId), ["evt-stuck"], "not retired while the tick could not be re-enqueued");
|
|
288
|
+
assert.equal(existsSync(join(handoffPaths(root).done, "evt-stuck.json")), false);
|
|
289
|
+
assert.equal(consumer.getStats().handoff_timeouts, 0);
|
|
290
|
+
} finally { chmodSync(inbox, 0o755); }
|
|
291
|
+
await consumer.tickOnce();
|
|
292
|
+
assert.deepEqual(listHandoffs(root), [], "retired once the re-enqueue succeeded");
|
|
293
|
+
assert.equal(consumer.getStats().handoff_timeouts, 1);
|
|
294
|
+
assert.equal(JSON.parse(readFileSync(join(handoffPaths(root).done, "evt-stuck.json"), "utf-8")).status, "expired");
|
|
295
|
+
} finally { await consumer.stop(); await rmRoot(root); }
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("circuit/backoff skip: re-keyed to the tail but NOT counted as a retry or a deferral in the health stats", async () => {
|
|
299
|
+
const root = await makeAgentRoot();
|
|
300
|
+
plantPrompt(root, "weekly-strategic-memo");
|
|
301
|
+
const consumer = startConsumer({
|
|
302
|
+
agentRoot: root,
|
|
303
|
+
frontDoorState: NOT_LIVE,
|
|
304
|
+
backoffSchedule: [0, 0, 0],
|
|
305
|
+
circuitThreshold: 1,
|
|
306
|
+
circuitDurationMs: 60_000,
|
|
307
|
+
maxAttempts: 1,
|
|
308
|
+
spawnSession: async () => ({ ok: false, exit_code: 1, error: "fail", duration_ms: 1 }),
|
|
309
|
+
});
|
|
310
|
+
try {
|
|
311
|
+
enqueueTick({ cadence: "weekly-strategic-memo", source: "launchd", agentRoot: root });
|
|
312
|
+
await consumer.tickOnce(); // opens the circuit
|
|
313
|
+
const before = consumer.getStats();
|
|
314
|
+
enqueueTick({ cadence: "weekly-strategic-memo", source: "launchd", agentRoot: root });
|
|
315
|
+
for (let i = 0; i < 5; i++) await consumer.tickOnce(); // five polls inside the circuit window
|
|
316
|
+
const after = consumer.getStats();
|
|
317
|
+
assert.equal(after.skipped_circuit_open, 5);
|
|
318
|
+
assert.equal(after.retries, before.retries, "a held-back cadence was never attempted — not a retry");
|
|
319
|
+
assert.equal(after.deferred, before.deferred, "nor a governance deferral");
|
|
320
|
+
assert.equal(listInbox(root).length, 1, "the held tick is still waiting, once");
|
|
321
|
+
} finally { await consumer.stop(); await rmRoot(root); }
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
test("handoff ledger hygiene: the sweep prunes done/ records and prompts past retention, at most once an hour", async () => {
|
|
325
|
+
const root = await makeAgentRoot();
|
|
326
|
+
let now = Date.parse("2026-09-08T10:00:00.000Z");
|
|
327
|
+
const consumer = startConsumer({ agentRoot: root, now: () => now, frontDoorState: LIVE, spawnSession: async () => ({ ok: true, exit_code: 0 }) });
|
|
328
|
+
try {
|
|
329
|
+
const paths = handoffPaths(root);
|
|
330
|
+
mkdirSync(paths.prompts, { recursive: true });
|
|
331
|
+
writeFileSync(join(paths.prompts, "evt-done.md"), "# p");
|
|
332
|
+
writeHandoff(root, { tickId: "evt-done", cadence: "a", promptPath: join(paths.prompts, "evt-done.md") }, { now });
|
|
333
|
+
ackHandoff(root, "evt-done", { now });
|
|
334
|
+
now += DEFAULT_HANDOFF_RETENTION_MS + 1000;
|
|
335
|
+
await consumer.tickOnce();
|
|
336
|
+
assert.equal(existsSync(join(paths.done, "evt-done.json")), false, "pruned by the sweep");
|
|
337
|
+
assert.equal(existsSync(join(paths.prompts, "evt-done.md")), false);
|
|
338
|
+
} finally { await consumer.stop(); await rmRoot(root); }
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// ── the parallelism directive on the escalate/guarded lane (WP-M7 mechanic 5) ──
|
|
342
|
+
//
|
|
343
|
+
// The spec puts the directive on "every escalate/guarded sub-session prompt".
|
|
344
|
+
// Cadence trigger prompts never pass through `prompt-builder.mjs#buildPrompt`
|
|
345
|
+
// — they are read straight off schedules/triggers/<name>.md — so this lane has
|
|
346
|
+
// its own seam (`renderCadencePromptBody`) and needs its own pins. Without it
|
|
347
|
+
// an `escalate` tick whose trigger asks for six independent audits spawns a
|
|
348
|
+
// sub-session with no standing permission to fan out.
|
|
349
|
+
|
|
350
|
+
test("escalate: the spawned sub-session prompt leads with the parallelism directive, exactly once", async () => {
|
|
351
|
+
const root = await makeAgentRoot();
|
|
352
|
+
plantPrompt(root, "daily-morning-brief", "# daily-morning-brief for {{agent.firstName}}\n\nAudit six things.");
|
|
353
|
+
const spawns = [];
|
|
354
|
+
const consumer = startConsumer({
|
|
355
|
+
agentRoot: root,
|
|
356
|
+
frontDoorState: NOT_LIVE,
|
|
357
|
+
spawnSession: async (o) => { spawns.push(o); return { ok: true, exit_code: 0, duration_ms: 1 }; },
|
|
358
|
+
});
|
|
359
|
+
try {
|
|
360
|
+
enqueueTick({ cadence: "daily-morning-brief", source: "launchd", agentRoot: root });
|
|
361
|
+
await consumer.tickOnce();
|
|
362
|
+
assert.equal(spawns.length, 1);
|
|
363
|
+
// The injected spawner is handed the promptPath, not the body — so assert
|
|
364
|
+
// on the seam the real spawner uses to turn that path into argv.
|
|
365
|
+
const body = renderCadencePromptBody(root, readFileSync(join(root, spawns[0].promptPath), "utf-8"));
|
|
366
|
+
assert.ok(body.startsWith(PARALLELISM_DIRECTIVE), "the directive leads");
|
|
367
|
+
assert.equal(countParallelism(body), 1);
|
|
368
|
+
assert.ok(body.endsWith("# daily-morning-brief for Nova\n\nAudit six things."), "and the trigger body follows, tokens rendered");
|
|
369
|
+
} finally { await consumer.stop(); await rmRoot(root); }
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test("a trigger that already carries the directive is not given a second copy", () => {
|
|
373
|
+
const body = renderCadencePromptBody("/nonexistent-agent-root", withParallelism("# already-primed"));
|
|
374
|
+
assert.equal(countParallelism(body), 1, "idempotent — a repeated directive reads as emphasis and erodes the file-scope rule");
|
|
375
|
+
assert.equal(body, withParallelism("# already-primed"));
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
test("the handed-off prompt and the spawned prompt are the same bytes", async () => {
|
|
379
|
+
const root = await makeAgentRoot();
|
|
380
|
+
const raw = "# weekly-strategic-memo for {{agent.firstName}}";
|
|
381
|
+
plantPrompt(root, "weekly-strategic-memo", raw);
|
|
382
|
+
const consumer = startConsumer({ agentRoot: root, frontDoorState: LIVE, spawnSession: async () => ({ ok: true, exit_code: 0 }) });
|
|
383
|
+
try {
|
|
384
|
+
enqueueTick({ cadence: "weekly-strategic-memo", source: "launchd", agentRoot: root });
|
|
385
|
+
await consumer.tickOnce();
|
|
386
|
+
const open = listHandoffs(root);
|
|
387
|
+
assert.equal(open.length, 1);
|
|
388
|
+
const handed = readFileSync(open[0].promptPath, "utf-8");
|
|
389
|
+
assert.equal(handed, renderCadencePromptBody(root, raw), "one seam, two lanes");
|
|
390
|
+
assert.equal(countParallelism(handed), 1);
|
|
391
|
+
assert.ok(handed.startsWith(PARALLELISM_DIRECTIVE));
|
|
392
|
+
} finally { await consumer.stop(); await rmRoot(root); }
|
|
393
|
+
});
|