@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,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* self-echo.test.mjs — the sender==self match that three production recurrences
|
|
3
|
+
* (2026-08-25 / 08-27 / 08-30) proved the old inline check could not make.
|
|
4
|
+
*
|
|
5
|
+
* Run: node --test scripts/daemon/lib/self-echo.test.mjs
|
|
6
|
+
*
|
|
7
|
+
* The load-bearing case is the REAL event shape: cohort inbox items carry the
|
|
8
|
+
* display name verbatim (`sender: "Isla Roselli"`), while the old filter
|
|
9
|
+
* compared against `fullName.toLowerCase().replace(/\s+/g, "-")` —
|
|
10
|
+
* "isla-roselli" — so the echo sailed through and the agent acked itself.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { test, describe, beforeEach } from "node:test";
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { tmpdir } from "node:os";
|
|
18
|
+
|
|
19
|
+
import { loadSelfIdentity, isSelfSender, normalizeName, logSelfEchoDrop, _resetSelfIdentity } from "./self-echo.mjs";
|
|
20
|
+
|
|
21
|
+
const ROOT = mkdtempSync(join(tmpdir(), "self-echo-test-"));
|
|
22
|
+
mkdirSync(join(ROOT, "config"), { recursive: true });
|
|
23
|
+
writeFileSync(join(ROOT, "config", "agent.json"), JSON.stringify({
|
|
24
|
+
firstName: "Isla",
|
|
25
|
+
fullName: "Isla Roselli",
|
|
26
|
+
memberId: "cmqh0tcml004ihhl6zev6ocb5",
|
|
27
|
+
email: "isla@example.com",
|
|
28
|
+
org: { cohort: { agentId: "A016" } },
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
beforeEach(() => _resetSelfIdentity());
|
|
32
|
+
|
|
33
|
+
const me = () => loadSelfIdentity(ROOT);
|
|
34
|
+
|
|
35
|
+
describe("normalizeName", () => {
|
|
36
|
+
test("folds case and the separator zoo to one comparable form", () => {
|
|
37
|
+
for (const v of ["Isla Roselli", "isla roselli", "isla-roselli", "Isla_Roselli", "isla.roselli", " Isla Roselli "]) {
|
|
38
|
+
assert.equal(normalizeName(v), "isla roselli", `"${v}" must normalise`);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("isSelfSender — the shapes real events actually carry", () => {
|
|
44
|
+
test("REGRESSION: the verbatim display name — the exact 25/27/30 Aug echo shape — matches", () => {
|
|
45
|
+
// Field-for-field the 30 Aug production echo (cmtfqjwvv01kype01t002t8jf):
|
|
46
|
+
const echo = {
|
|
47
|
+
id: "cohort-cmtfqjwvv01kype01t002t8jf",
|
|
48
|
+
service: "cohort",
|
|
49
|
+
channel: "dm-a016-a039",
|
|
50
|
+
sender: "Isla Roselli",
|
|
51
|
+
raw_ref: "cohort:dm:cmtfqjwvv01kype01t002t8jf:192025",
|
|
52
|
+
event_kind: "send",
|
|
53
|
+
is_dm: true,
|
|
54
|
+
};
|
|
55
|
+
assert.equal(isSelfSender(echo, me()), true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("the OLD filter's dashed form and other separator variants also match", () => {
|
|
59
|
+
for (const s of ["isla-roselli", "Isla_Roselli", "isla roselli"]) {
|
|
60
|
+
assert.equal(isSelfSender({ sender: s }, me()), true, `"${s}" is this agent`);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("REGRESSION: the BARE first name alone is NOT self — it collides with every namesake", () => {
|
|
65
|
+
// "Isla" as a display name is any colleague, contact or CC'd person who
|
|
66
|
+
// shares the first name. Matching it dropped their genuine inbound.
|
|
67
|
+
assert.equal(isSelfSender({ sender: "Isla" }, me()), false);
|
|
68
|
+
assert.equal(isSelfSender({ sender: "isla" }, me()), false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("REGRESSION: a NON-matching id EXONERATES a colliding display name", () => {
|
|
72
|
+
// A namesake — display name "Isla Roselli", but a DIFFERENT member cuid.
|
|
73
|
+
// The id positively identifies the sender as someone else; the name rung
|
|
74
|
+
// must never be reached, so this is NOT self and must not be dropped.
|
|
75
|
+
assert.equal(
|
|
76
|
+
isSelfSender({ sender: "Isla Roselli", sender_id: "cmDIFFERENT00000000000000" }, me()),
|
|
77
|
+
false,
|
|
78
|
+
"an id that resolves to someone else outranks the name collision",
|
|
79
|
+
);
|
|
80
|
+
assert.equal(
|
|
81
|
+
isSelfSender({ sender: "isla-roselli", author_id: "cmDIFFERENT00000000000000" }, me()),
|
|
82
|
+
false,
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("REGRESSION: a NON-matching email exonerates a colliding display name too", () => {
|
|
87
|
+
assert.equal(
|
|
88
|
+
isSelfSender({ sender: "Isla Roselli", sender_email: "other.isla@example.com" }, me()),
|
|
89
|
+
false,
|
|
90
|
+
);
|
|
91
|
+
// …while the OWN email still matches regardless of the display name.
|
|
92
|
+
assert.equal(
|
|
93
|
+
isSelfSender({ sender: "totally different name", sender_email: "isla@example.com" }, me()),
|
|
94
|
+
true,
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("a member cuid on any id-ish field matches (strongest evidence)", () => {
|
|
99
|
+
for (const item of [
|
|
100
|
+
{ sender: "someone", sender_id: "cmqh0tcml004ihhl6zev6ocb5" },
|
|
101
|
+
{ sender: "someone", author_id: "CMQH0TCML004IHHL6ZEV6OCB5" },
|
|
102
|
+
{ sender: "someone", from: { id: "cmqh0tcml004ihhl6zev6ocb5" } },
|
|
103
|
+
]) {
|
|
104
|
+
assert.equal(isSelfSender(item, me()), true);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("the org seat slug matches too", () => {
|
|
109
|
+
assert.equal(isSelfSender({ sender: "x", sender_id: "A016" }, me()), true);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("own email matches (gmail/orgmail lanes)", () => {
|
|
113
|
+
assert.equal(isSelfSender({ sender: "Isla via mail", sender_email: "isla@example.com" }, me()), true);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("genuine colleagues NEVER match — no false drops", () => {
|
|
117
|
+
for (const s of ["Jordan Blake", "Candace Wong", "Isla Fisher", "roselli", "isla2", "ceo"]) {
|
|
118
|
+
assert.equal(isSelfSender({ sender: s }, me()), false, `"${s}" must not be dropped`);
|
|
119
|
+
}
|
|
120
|
+
assert.equal(isSelfSender({ sender: "Candace Wong", sender_id: "cmOTHER000000000000000000" }, me()), false);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("an UNCONFIGURED identity matches nothing — a half-set-up repo drops no inbound", () => {
|
|
124
|
+
const bare = mkdtempSync(join(tmpdir(), "self-echo-bare-"));
|
|
125
|
+
try {
|
|
126
|
+
const id = loadSelfIdentity(bare);
|
|
127
|
+
assert.equal(isSelfSender({ sender: "Isla Roselli" }, id), false);
|
|
128
|
+
assert.equal(isSelfSender({ sender: "Agent" }, id), false);
|
|
129
|
+
} finally {
|
|
130
|
+
rmSync(bare, { recursive: true, force: true });
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("logSelfEchoDrop — every drop is auditable", () => {
|
|
136
|
+
test("appends one JSONL row per drop under logs/daemon/", () => {
|
|
137
|
+
const item = { id: "e-1", raw_ref: "cohort:dm:e-1:42", sender: "Isla Roselli", channel: "dm-a016-a039", service: "cohort", event_kind: "send" };
|
|
138
|
+
logSelfEchoDrop(item, { stage: "poll", service: "cohort", agentRoot: ROOT });
|
|
139
|
+
logSelfEchoDrop(item, { stage: "process", service: "cohort", agentRoot: ROOT });
|
|
140
|
+
const rows = readFileSync(join(ROOT, "logs", "daemon", "self-echo-drops.jsonl"), "utf-8")
|
|
141
|
+
.split("\n").filter(Boolean).map((l) => JSON.parse(l));
|
|
142
|
+
assert.equal(rows.length, 2);
|
|
143
|
+
assert.equal(rows[0].stage, "poll");
|
|
144
|
+
assert.equal(rows[1].stage, "process");
|
|
145
|
+
assert.equal(rows[0].raw_ref, "cohort:dm:e-1:42");
|
|
146
|
+
assert.equal(rows[0].sender, "Isla Roselli");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("never throws, even for a junk item", () => {
|
|
150
|
+
assert.doesNotThrow(() => logSelfEchoDrop(null, { agentRoot: ROOT }));
|
|
151
|
+
assert.doesNotThrow(() => logSelfEchoDrop(undefined, { agentRoot: join(ROOT, "no", "such", "deep") }));
|
|
152
|
+
});
|
|
153
|
+
});
|
|
@@ -32,6 +32,9 @@ const AGENT_DIR = process.env.AGENT_DIR || resolve(__dirname, "../..");
|
|
|
32
32
|
|
|
33
33
|
process.env.AGENT_DIR = AGENT_DIR;
|
|
34
34
|
process.env.AGENT_ROOT = AGENT_DIR;
|
|
35
|
+
// Inherited by every `claude --print` child: scripts/collective/hook-runner.mjs
|
|
36
|
+
// classifies those sessions as "daemon" (presence kind; no identity primer).
|
|
37
|
+
process.env.MAESTRO_DAEMON = "1";
|
|
35
38
|
|
|
36
39
|
// ---------------------------------------------------------------------------
|
|
37
40
|
// 0. Environment — BEFORE anything reads a credential
|
|
@@ -12,6 +12,7 @@ import { wrapExternalContent } from "../../lib/security/external-content.mjs";
|
|
|
12
12
|
import { isEnabled as orgEnabled } from "../../lib/org/client.mjs";
|
|
13
13
|
import { recall as orgRecall } from "../../lib/org/knowledge.mjs";
|
|
14
14
|
import { outcomeSourceShareable } from "./session-outcomes.mjs";
|
|
15
|
+
import { withParallelism } from "../../lib/prompts/parallelism.mjs";
|
|
15
16
|
|
|
16
17
|
const AGENT_REPO_DIR = process.env.AGENT_DIR || join(new URL(".", import.meta.url).pathname, "../..");
|
|
17
18
|
|
|
@@ -852,16 +853,23 @@ export async function buildPrompt(item, classResult, options = {}) {
|
|
|
852
853
|
const parts = [];
|
|
853
854
|
|
|
854
855
|
// 1. Persona block (cached) — rendered from config/agent.json, which the
|
|
855
|
-
// Cohort enrollment pull keeps current. Leads the
|
|
856
|
+
// Cohort enrollment pull keeps current. Leads the BODY and outranks the
|
|
856
857
|
// CLAUDE.md preamble below, because a scaffolded CLAUDE.md carries
|
|
857
858
|
// "identity not yet configured" placeholder prose that would otherwise
|
|
858
859
|
// contradict a perfectly good org record.
|
|
860
|
+
//
|
|
861
|
+
// THE PRECEDENCE SENTENCE NAMES ITSELF, NOT A POSITION. It used to say "the
|
|
862
|
+
// section above … anything below", which was true only while the persona was
|
|
863
|
+
// the first thing in the prompt. The parallelism directive is prepended at
|
|
864
|
+
// the end of this function (§10) and now leads, so "above" had two candidate
|
|
865
|
+
// referents and the directive sat outside the rule entirely — above it, not
|
|
866
|
+
// below. A self-naming sentence stays true wherever the block ends up.
|
|
859
867
|
const persona = loadPersona();
|
|
860
868
|
if (persona) {
|
|
861
869
|
parts.push(persona);
|
|
862
870
|
parts.push("");
|
|
863
871
|
parts.push(
|
|
864
|
-
"
|
|
872
|
+
"This section is your identity, resolved from your organisation's record of you. Where anything else in this prompt conflicts with it, this section wins.",
|
|
865
873
|
);
|
|
866
874
|
parts.push("");
|
|
867
875
|
}
|
|
@@ -1025,5 +1033,13 @@ export async function buildPrompt(item, classResult, options = {}) {
|
|
|
1025
1033
|
// 9. Audit instruction
|
|
1026
1034
|
parts.push(`Log all actions taken to logs/audit/${date}-actions.jsonl as a JSON line with: timestamp, action_type, target, result, session context.`);
|
|
1027
1035
|
|
|
1028
|
-
|
|
1036
|
+
// 10. The parallelism directive (WP-M7 mechanic 5). Every prompt this builder
|
|
1037
|
+
// produces is handed to a sub-session that may plan and dispatch work of its
|
|
1038
|
+
// own, and nothing else in the prompt ever says that fanning out is allowed —
|
|
1039
|
+
// which is why sessions handed six independent tasks ran them one at a time.
|
|
1040
|
+
// It LEADS the prompt because it governs how the session plans, before it has
|
|
1041
|
+
// read a word of the item. `withParallelism` is idempotent, so a prompt that
|
|
1042
|
+
// already carries it (composed by a caller, or handed on to `session spawn`,
|
|
1043
|
+
// which applies the same seam) is returned with exactly one copy.
|
|
1044
|
+
return withParallelism(parts.join("\n"));
|
|
1029
1045
|
}
|
|
@@ -10,14 +10,12 @@
|
|
|
10
10
|
* 2. ACKNOWLEDGEMENTS: for complex items that need a full session, an immediate
|
|
11
11
|
* holding message so the sender knows the work has started.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* concat and one HTTP call, which cannot time out, cannot be starved of
|
|
20
|
-
* memory, and cannot be refused by a spend cap.
|
|
13
|
+
* This one is generated by assurance.generateAck — one bespoke haiku line
|
|
14
|
+
* under a tight (~5s) cap — and on ANY generation failure it sends NOTHING.
|
|
15
|
+
* That is a deliberate inversion (CEO directive 2026-08-30) of the previous
|
|
16
|
+
* canned rotation ("On it." / "Looking now."), which itself replaced a 60s
|
|
17
|
+
* model spawn that lost its race 20 times in 32. The failure budget is now
|
|
18
|
+
* owned by the assurance sweep, not by boilerplate.
|
|
21
19
|
*
|
|
22
20
|
* Migrated off `@anthropic-ai/sdk` per CEO directive (Slack DM
|
|
23
21
|
* D099N1JGKRQ, 2026-04-27 09:38Z + 11:33Z): all agent daemon model
|
|
@@ -56,10 +54,11 @@ import { rungById } from "../../lib/execution/route.mjs";
|
|
|
56
54
|
// the property the acknowledgement path needed and did not have.
|
|
57
55
|
import { deliver, deliverWithRetry, resolveSlackChannel } from "./deliver.mjs";
|
|
58
56
|
import { startTyping, stopTyping } from "./typing-registry.mjs";
|
|
59
|
-
// The acknowledgement is
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
|
|
57
|
+
// The acknowledgement is GENERATED again — one bespoke haiku line under a
|
|
58
|
+
// ~5s cap — but with the failure mode inverted from the old 60s-spawn era:
|
|
59
|
+
// generation failure now means SILENCE (send nothing), never a canned
|
|
60
|
+
// placeholder. CEO directive 2026-08-30; see assurance.generateAck.
|
|
61
|
+
import { generateAck } from "./assurance.mjs";
|
|
63
62
|
// Shared message-craft doctrine (distil by default; format only when the message
|
|
64
63
|
// must be large; attach genuinely verbose content rather than dumping it). The
|
|
65
64
|
// SAME constant leads the full-session prompt in prompt-builder.mjs, so a reactive
|
|
@@ -695,11 +694,10 @@ export function setGenerateResponseForTests(fn) {
|
|
|
695
694
|
* Generate an ANSWER. Only an answer.
|
|
696
695
|
*
|
|
697
696
|
* This used to take an `isHolding` flag and, when set, spend a model call
|
|
698
|
-
* writing an acknowledgement. That branch
|
|
699
|
-
*
|
|
700
|
-
*
|
|
701
|
-
*
|
|
702
|
-
* seconds when it worked and total silence when it did not.
|
|
697
|
+
* writing an acknowledgement. That branch stays gone: acknowledgements live in
|
|
698
|
+
* assurance.generateAck (their own model, cap and silence-on-failure rules),
|
|
699
|
+
* and the flag is deliberately not kept "just in case", because keeping it is
|
|
700
|
+
* how it comes back.
|
|
703
701
|
*/
|
|
704
702
|
async function realGenerateResponse(item, classResult) {
|
|
705
703
|
const preamble = loadPreamble();
|
|
@@ -975,26 +973,29 @@ export async function sendQuickResponse(item, classResult, routed = null) {
|
|
|
975
973
|
* Acknowledge an ask that is about to become a long-running session, and return
|
|
976
974
|
* the text so the session's own prompt can see what the human was already told.
|
|
977
975
|
*
|
|
978
|
-
*
|
|
976
|
+
* THE ACK IS GENERATED AGAIN — WITH THE FAILURE MODE INVERTED.
|
|
979
977
|
*
|
|
980
|
-
*
|
|
981
|
-
* `claude --print` child under a 60-second hard cap to compose two sentences of
|
|
982
|
-
* courtesy. On a box at 95-99% memory that spawn lost its race 20 times in 32;
|
|
983
|
-
* every loss threw, was caught below, logged as `holding_message_error`, and
|
|
984
|
-
* told the requester nothing at all — while a session ran for a median of 14.7
|
|
985
|
-
* minutes behind a typing indicator the human reads as "she's replying".
|
|
978
|
+
* History, because this path has now swung twice and the reasons matter:
|
|
986
979
|
*
|
|
987
|
-
*
|
|
988
|
-
*
|
|
989
|
-
*
|
|
990
|
-
*
|
|
991
|
-
*
|
|
992
|
-
*
|
|
980
|
+
* 1. Originally `_generateResponse(item, classResult, true)` spawned a cold
|
|
981
|
+
* `claude --print` child under a 60-second cap. On a box at 95-99% memory
|
|
982
|
+
* it lost that race 20 times in 32, and every loss was silent — the
|
|
983
|
+
* requester got nothing while a 15-45 minute session ran behind a typing
|
|
984
|
+
* indicator.
|
|
985
|
+
* 2. The fix made the ack a canned rotation (assurance.composeAck: "On it." /
|
|
986
|
+
* "Looking now." / "On it — digging in now.") that could never fail.
|
|
987
|
+
* It also could never sound like anyone. In the 08-2026 self-echo
|
|
988
|
+
* incidents two agents traded those exact canned lines at each other, and
|
|
989
|
+
* the CEO's verdict (2026-08-30) was that a reflex string is worse than
|
|
990
|
+
* no ack at all.
|
|
993
991
|
*
|
|
994
|
-
*
|
|
995
|
-
*
|
|
996
|
-
* this
|
|
997
|
-
*
|
|
992
|
+
* So: assurance.generateAck produces ONE bespoke line from the actual thread
|
|
993
|
+
* via the cheapest model under a tight (~5s, env-tunable) cap, and on ANY
|
|
994
|
+
* generation failure this function sends NOTHING and says so in the log. The
|
|
995
|
+
* durability story is unchanged — the caller opened the obligation before
|
|
996
|
+
* calling this, the assurance sweep retries generation once within
|
|
997
|
+
* ASSURANCE_ACK_GRACE_MS, and the progress/failure notices still guarantee the
|
|
998
|
+
* human is never left in long-term silence.
|
|
998
999
|
*
|
|
999
1000
|
* @returns {{ sent: boolean, holdingText: string|null }} result
|
|
1000
1001
|
*/
|
|
@@ -1002,8 +1003,8 @@ export async function sendHoldingMessage(item, classResult) {
|
|
|
1002
1003
|
const startTime = Date.now();
|
|
1003
1004
|
|
|
1004
1005
|
try {
|
|
1005
|
-
|
|
1006
|
-
|
|
1006
|
+
// Channel + dedup gates FIRST — they are free, and a model call spent on a
|
|
1007
|
+
// message the dedup registry would refuse anyway is a pure waste.
|
|
1007
1008
|
if (item.service === "slack") {
|
|
1008
1009
|
const channel = resolveSlackChannel(item);
|
|
1009
1010
|
if (!channel) {
|
|
@@ -1020,9 +1021,19 @@ export async function sendHoldingMessage(item, classResult) {
|
|
|
1020
1021
|
}
|
|
1021
1022
|
}
|
|
1022
1023
|
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1024
|
+
const text = await generateAck(item, classResult);
|
|
1025
|
+
if (text == null) {
|
|
1026
|
+
// Deliberate silence — never a canned fallback. The obligation the caller
|
|
1027
|
+
// opened stays un-acked; the assurance sweep owns the (bounded) retry.
|
|
1028
|
+
console.warn(`[responder] holding ack generation failed for ${item.sender} — sending nothing (silence over boilerplate, by design)`);
|
|
1029
|
+
logResponse({ type: "holding_ack_generation_failed", sender: item.sender, service: item.service });
|
|
1030
|
+
counters.bump("send.blocked", { stage: "ack_generation", service: item.service || "unknown", kind: "holding" });
|
|
1031
|
+
return { sent: false, holdingText: null, error: "ack_generation_failed", genFailed: true };
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// Bounded retry on the SEND. A courtesy message that gives up on one
|
|
1035
|
+
// dropped socket is how silence happens; ~3 seconds of retry in front of a
|
|
1036
|
+
// waiting human is proportionate.
|
|
1026
1037
|
const delivered = await deliverWithRetry(item, text, { kind: "ack" });
|
|
1027
1038
|
const sendResult = {
|
|
1028
1039
|
sent: delivered.sent,
|
|
@@ -1053,7 +1064,7 @@ export async function sendHoldingMessage(item, classResult) {
|
|
|
1053
1064
|
service: item.service,
|
|
1054
1065
|
duration_ms: duration,
|
|
1055
1066
|
text_length: text.length,
|
|
1056
|
-
generated:
|
|
1067
|
+
generated: true,
|
|
1057
1068
|
attempts: delivered.attempts || 1,
|
|
1058
1069
|
...sendResult,
|
|
1059
1070
|
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scripts/daemon/sdk-version.mjs — which SDK version is the code THIS daemon
|
|
3
|
+
* process is running?
|
|
4
|
+
*
|
|
5
|
+
* The daemon runs from the agent dir's COPIED scripts/daemon, not from
|
|
6
|
+
* node_modules. autoupdate installs the next version into node_modules first
|
|
7
|
+
* and merges the files a minute or two later; a daemon that (re)starts inside
|
|
8
|
+
* that window would report the node_modules version while executing the old
|
|
9
|
+
* copies, and `maestro upgrade --verify` would go green on a lie. So the
|
|
10
|
+
* order of trust is the version of the COPIED tree first:
|
|
11
|
+
*
|
|
12
|
+
* 1. .maestro/shipped-manifest.json `sdkVersion` — written by the upgrade
|
|
13
|
+
* that copied the files (2.12+), i.e. the version of scripts/daemon/*;
|
|
14
|
+
* 2. node_modules/@cohortapp/agent-sdk/package.json — a pre-2.12 manifest
|
|
15
|
+
* has no sdkVersion; the window above is then the only inaccuracy left;
|
|
16
|
+
* 3. the package this file itself ships in (a framework checkout).
|
|
17
|
+
*
|
|
18
|
+
* Read once at daemon start, never again (the dashboard must say what the
|
|
19
|
+
* process IS, not what node_modules became meanwhile).
|
|
20
|
+
*
|
|
21
|
+
* @module scripts/daemon/sdk-version
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
"use strict";
|
|
25
|
+
|
|
26
|
+
import { readFileSync } from "node:fs";
|
|
27
|
+
import { join } from "node:path";
|
|
28
|
+
|
|
29
|
+
export const SDK_PACKAGE = "@cohortapp/agent-sdk";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Pure over an injected reader. `readJson(path)` returns the parsed file or
|
|
33
|
+
* throws / returns null; every source is fail-open.
|
|
34
|
+
* @param {{agentDir:string, ownPackagePath?:string, readJson?:(p:string)=>any}} o
|
|
35
|
+
* @returns {{version:string, source:"manifest"|"node_modules"|"package"|"none"}}
|
|
36
|
+
*/
|
|
37
|
+
export function resolveRunningSdkVersion(o = {}) {
|
|
38
|
+
const readJson = o.readJson || ((p) => JSON.parse(readFileSync(p, "utf8")));
|
|
39
|
+
const tryRead = (p) => { try { return readJson(p); } catch { return null; /* absent/corrupt → next source */ } };
|
|
40
|
+
const manifest = tryRead(join(o.agentDir, ".maestro", "shipped-manifest.json"));
|
|
41
|
+
if (manifest && typeof manifest.sdkVersion === "string" && manifest.sdkVersion) return { version: manifest.sdkVersion, source: "manifest" };
|
|
42
|
+
const dep = tryRead(join(o.agentDir, "node_modules", ...SDK_PACKAGE.split("/"), "package.json"));
|
|
43
|
+
if (dep && dep.name === SDK_PACKAGE && typeof dep.version === "string") return { version: dep.version, source: "node_modules" };
|
|
44
|
+
if (o.ownPackagePath) {
|
|
45
|
+
const own = tryRead(o.ownPackagePath);
|
|
46
|
+
if (own && own.name === SDK_PACKAGE && typeof own.version === "string") return { version: own.version, source: "package" };
|
|
47
|
+
}
|
|
48
|
+
return { version: "", source: "none" };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default { resolveRunningSdkVersion, SDK_PACKAGE };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sdk-version.test.mjs — the daemon's own-version resolution prefers the
|
|
3
|
+
* version of the COPIED tree (the manifest) over node_modules.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { test } from "node:test";
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import { resolveRunningSdkVersion, SDK_PACKAGE } from "./sdk-version.mjs";
|
|
9
|
+
|
|
10
|
+
function reader(files) {
|
|
11
|
+
return (p) => { if (p in files) return files[p]; throw new Error(`ENOENT ${p}`); };
|
|
12
|
+
}
|
|
13
|
+
const A = "/seat/ivy-ai";
|
|
14
|
+
const MANIFEST = `${A}/.maestro/shipped-manifest.json`;
|
|
15
|
+
const DEP = `${A}/node_modules/@cohortapp/agent-sdk/package.json`;
|
|
16
|
+
const OWN = "/sdk/package.json";
|
|
17
|
+
|
|
18
|
+
test("the manifest's sdkVersion wins: node_modules may already hold the NEXT version while the copied scripts are still the old one", () => {
|
|
19
|
+
const r = resolveRunningSdkVersion({ agentDir: A, ownPackagePath: OWN, readJson: reader({
|
|
20
|
+
[MANIFEST]: { version: 1, sdkVersion: "2.11.17" },
|
|
21
|
+
[DEP]: { name: SDK_PACKAGE, version: "2.12.0" },
|
|
22
|
+
}) });
|
|
23
|
+
assert.deepEqual(r, { version: "2.11.17", source: "manifest" });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("a pre-2.12 manifest (no sdkVersion) or none → node_modules; then the package this file ships in; then empty", () => {
|
|
27
|
+
assert.deepEqual(resolveRunningSdkVersion({ agentDir: A, readJson: reader({ [MANIFEST]: { version: 1, files: [] }, [DEP]: { name: SDK_PACKAGE, version: "2.11.15" } }) }), { version: "2.11.15", source: "node_modules" });
|
|
28
|
+
assert.deepEqual(resolveRunningSdkVersion({ agentDir: A, ownPackagePath: OWN, readJson: reader({ [OWN]: { name: SDK_PACKAGE, version: "2.12.0" } }) }), { version: "2.12.0", source: "package" });
|
|
29
|
+
assert.deepEqual(resolveRunningSdkVersion({ agentDir: A, ownPackagePath: OWN, readJson: reader({ [OWN]: { name: "ivy-ai", version: "1.0.0" } }) }), { version: "", source: "none" }, "an agent's own package.json is not the SDK");
|
|
30
|
+
assert.deepEqual(resolveRunningSdkVersion({ agentDir: A, readJson: () => { throw new Error("EACCES"); } }), { version: "", source: "none" }, "fail-open");
|
|
31
|
+
});
|
|
@@ -6,10 +6,20 @@
|
|
|
6
6
|
# recipient allowlist sanity check, and enforces hourly + daily rate caps.
|
|
7
7
|
#
|
|
8
8
|
# Usage: pre-send-audit.sh <channel_type>
|
|
9
|
-
# channel_type: slack | slack-draft | gmail | whatsapp
|
|
9
|
+
# channel_type: slack | slack-draft | gmail | whatsapp | cohort
|
|
10
|
+
#
|
|
11
|
+
# `cohort` is the PreToolUse channel `maestro global-setup` wires into
|
|
12
|
+
# ~/.claude/settings.json for EVERY mcp__cohort outbound tool (messaging_send,
|
|
13
|
+
# messaging_send_voice_note, email_send, email_draft_send, org_rpc) — the
|
|
14
|
+
# persona gate below (section a2) is what the front-door session relies on to
|
|
15
|
+
# never expose its own machinery to a human (design spec 2026-09-08 §3.6). The
|
|
16
|
+
# same patterns run in-process in lib/comms/send-gate.mjs (screenPersona) for
|
|
17
|
+
# the lanes no hook fronts: `maestro inbox reply`, the daemon's delivery, the
|
|
18
|
+
# adapters. Keep the two lists in sync.
|
|
10
19
|
#
|
|
11
20
|
# Stdin: Claude Code PreToolUse JSON payload:
|
|
12
|
-
# { "tool_name": "...", "tool_input": { "text"|"message"|"body": "...",
|
|
21
|
+
# { "tool_name": "...", "tool_input": { "text"|"message"|"body"|"bodyText": "...",
|
|
22
|
+
# "subject": "...", "params": { …an org_rpc body… },
|
|
13
23
|
# "to"|"recipient"|"channel": "..." }, ... }
|
|
14
24
|
#
|
|
15
25
|
# Exit codes:
|
|
@@ -33,7 +43,7 @@ deny() {
|
|
|
33
43
|
}
|
|
34
44
|
|
|
35
45
|
CHANNEL_TYPE="${1:-}"
|
|
36
|
-
[ -n "$CHANNEL_TYPE" ] || deny "Channel type required (slack|slack-draft|gmail|whatsapp)"
|
|
46
|
+
[ -n "$CHANNEL_TYPE" ] || deny "Channel type required (slack|slack-draft|gmail|whatsapp|cohort)"
|
|
37
47
|
|
|
38
48
|
# jq is mandatory — without it we cannot inspect content, so refuse to send.
|
|
39
49
|
command -v jq >/dev/null 2>&1 || deny "jq not available — cannot screen outbound content (fail closed)"
|
|
@@ -50,15 +60,20 @@ fi
|
|
|
50
60
|
# Extract the outbound text. Claude Code wraps the tool args under
|
|
51
61
|
# tool_input; for robustness we also look at the top level. We concatenate
|
|
52
62
|
# every plausible content field so a banned phrase anywhere is caught.
|
|
63
|
+
# `params` is where org_rpc carries an outbound body; `bodyText`/`subject` are
|
|
64
|
+
# the mail-desk (email_draft_send / email_send) content fields.
|
|
53
65
|
CONTENT=$(printf '%s' "$TOOL_INPUT" | jq -r '
|
|
54
|
-
[ .tool_input // {} , . ]
|
|
66
|
+
[ .tool_input // {} , ( .tool_input.params? | if type=="object" then . else {} end ) , . ]
|
|
55
67
|
| map(
|
|
56
68
|
( .text? // empty ),
|
|
57
69
|
( .message? // empty ),
|
|
58
70
|
( .body? // empty ),
|
|
71
|
+
( .bodyText? // empty ),
|
|
72
|
+
( .subject? // empty ),
|
|
59
73
|
( .content? // empty ),
|
|
60
74
|
( .blocks? | if type=="array" or type=="object" then tojson else empty end )
|
|
61
75
|
)
|
|
76
|
+
| map(if type=="string" then . else tojson end)
|
|
62
77
|
| flatten
|
|
63
78
|
| map(select(. != null and . != ""))
|
|
64
79
|
| join("\n")
|
|
@@ -110,6 +125,84 @@ if printf '%s' "$CONTENT" | grep -Eiq '(^|[[:cntrl:]])[[:space:]]*(Certainly!|Ab
|
|
|
110
125
|
deny "Banned filler opener (Certainly!/Absolutely!/Great question!) — see policies/communication-style.md"
|
|
111
126
|
fi
|
|
112
127
|
|
|
128
|
+
# --- (a2) persona-leak screen ----------------------------------------------
|
|
129
|
+
# The agent is one persona to the humans it talks to; parallel sessions,
|
|
130
|
+
# sub-sessions, subagents and workflows are internal workings. Outbound text
|
|
131
|
+
# may not name them, and may not name Claude Code / a language model. The
|
|
132
|
+
# denial carries the offending phrase so the session rewrites in its own voice
|
|
133
|
+
# ("my team", "a colleague", "one of my analysts").
|
|
134
|
+
#
|
|
135
|
+
# Patterns are case-insensitive extended regexes. "subagent"/"sub-agent" is
|
|
136
|
+
# blocked only when it is followed by said|reported|session — the bare noun in
|
|
137
|
+
# an internal-process sentence is tolerated, the attribution of speech to a
|
|
138
|
+
# machine part is not.
|
|
139
|
+
PERSONA_PATTERNS=(
|
|
140
|
+
'claude[[:space:]]+code'
|
|
141
|
+
'claude[[:space:]]+sessions?'
|
|
142
|
+
'sub-sessions?'
|
|
143
|
+
'sub-?agents?[[:space:]]+(said|reported|sessions?)'
|
|
144
|
+
'workflow[[:space:]]+agents?'
|
|
145
|
+
'as[[:space:]]+an[[:space:]]+ai\b'
|
|
146
|
+
'language[[:space:]]+model'
|
|
147
|
+
)
|
|
148
|
+
for pat in "${PERSONA_PATTERNS[@]}"; do
|
|
149
|
+
hit=$(printf '%s' "$CONTENT" | grep -Eio -- "$pat" 2>/dev/null | head -n 1) || true
|
|
150
|
+
if [ -n "$hit" ]; then
|
|
151
|
+
deny "Persona leak in outbound message: \"$hit\" — rewrite in your own voice (my team / a colleague), never name the machinery"
|
|
152
|
+
fi
|
|
153
|
+
done
|
|
154
|
+
|
|
155
|
+
# Session names are internal identifiers and must never reach a human — but
|
|
156
|
+
# the rule is BOUNDED, not guessed: `<first>-main` is always internal; any
|
|
157
|
+
# other name is internal only when it is a registered peer
|
|
158
|
+
# ($AGENT_ROOT/state/session/peers.json, written by `maestro session spawn`,
|
|
159
|
+
# an array or {peers:[…]} of {name,…}) or listed in MAESTRO_SESSION_NAMES
|
|
160
|
+
# (comma-separated). A colleague called Marc-Antoine, the slug james-kirkland
|
|
161
|
+
# in a URL or the mailbox ethan-miller@ are ordinary text.
|
|
162
|
+
#
|
|
163
|
+
# The first name resolves, in order, from MAESTRO_AGENT_FIRST,
|
|
164
|
+
# $AGENT_ROOT/config/agent.json (firstName, else the first word of fullName)
|
|
165
|
+
# and the machine pointer ~/.claude/maestro-agent.json (agentName). When none
|
|
166
|
+
# resolves the `<first>-main` check is skipped — the phrase gate above still
|
|
167
|
+
# applies — because a wrong guess would block legitimate text.
|
|
168
|
+
FIRST="${MAESTRO_AGENT_FIRST:-}"
|
|
169
|
+
if [ -z "$FIRST" ]; then
|
|
170
|
+
for cand in "${AGENT_ROOT:-}/config/agent.json" "${AGENT_DIR:-}/config/agent.json" "$HOME/.claude/maestro-agent.json"; do
|
|
171
|
+
[ -f "$cand" ] || continue
|
|
172
|
+
FIRST=$(jq -r '(.firstName // .agentName // .fullName // "") | tostring | split(" ")[0] | ascii_downcase' "$cand" 2>/dev/null) || FIRST=""
|
|
173
|
+
[ -n "$FIRST" ] && [ "$FIRST" != "null" ] && break
|
|
174
|
+
FIRST=""
|
|
175
|
+
done
|
|
176
|
+
fi
|
|
177
|
+
FIRST=$(printf '%s' "$FIRST" | tr '[:upper:]' '[:lower:]' | tr -cd 'a-z0-9-')
|
|
178
|
+
|
|
179
|
+
SESSION_NAMES=()
|
|
180
|
+
[ -n "$FIRST" ] && SESSION_NAMES+=("${FIRST}-main")
|
|
181
|
+
for peers in "${AGENT_ROOT:-}/state/session/peers.json" "${AGENT_DIR:-}/state/session/peers.json"; do
|
|
182
|
+
[ -f "$peers" ] || continue
|
|
183
|
+
# A corrupt registry is an empty one: this must never fail the whole send closed.
|
|
184
|
+
while IFS= read -r pn; do
|
|
185
|
+
[ -n "$pn" ] && SESSION_NAMES+=("$pn")
|
|
186
|
+
done < <(jq -r '(if type=="array" then . elif type=="object" then (.peers // []) else [] end) | .[] | select(type=="object") | (.name // empty) | tostring | ascii_downcase' "$peers" 2>/dev/null || true)
|
|
187
|
+
break
|
|
188
|
+
done
|
|
189
|
+
if [ -n "${MAESTRO_SESSION_NAMES:-}" ]; then
|
|
190
|
+
IFS=',' read -r -a extra_names <<< "$MAESTRO_SESSION_NAMES"
|
|
191
|
+
# bash 3.2 (macOS) + set -u: an empty array expands as "unbound" — use the ${a[@]+"${a[@]}"} idiom.
|
|
192
|
+
for en in ${extra_names[@]+"${extra_names[@]}"}; do
|
|
193
|
+
en=$(printf '%s' "$en" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]')
|
|
194
|
+
[ -n "$en" ] && SESSION_NAMES+=("$en")
|
|
195
|
+
done
|
|
196
|
+
fi
|
|
197
|
+
for sname in ${SESSION_NAMES[@]+"${SESSION_NAMES[@]}"}; do
|
|
198
|
+
sname_re=$(printf '%s' "$sname" | sed 's/[.[\*^$()+?{|]/\\&/g')
|
|
199
|
+
# Word-bounded on [A-Za-z0-9_-]: "alex-main" hits, "alex-mainland" / "alex-main-street" do not.
|
|
200
|
+
name_hit=$(printf '%s' "$CONTENT" | grep -Eio -- "(^|[^[:alnum:]_-])${sname_re}([^[:alnum:]_-]|$)" 2>/dev/null | head -n 1 | sed -E 's/^[^[:alnum:]_-]//; s/[^[:alnum:]_-]$//') || true
|
|
201
|
+
if [ -n "$name_hit" ]; then
|
|
202
|
+
deny "Persona leak in outbound message: \"$name_hit\" is an internal session name — say \"my team\" or \"a colleague\" instead"
|
|
203
|
+
fi
|
|
204
|
+
done
|
|
205
|
+
|
|
113
206
|
# --- (b) recipient allowlist sanity check ----------------------------------
|
|
114
207
|
# Only enforced when a recipient is present AND an allowlist file exists.
|
|
115
208
|
# Allowlist file: one entry (channel id, email, handle) per line, '#' comments.
|