@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
package/lib/comms/send-gate.mjs
CHANGED
|
@@ -92,6 +92,31 @@ export const BANNED_OPENERS = Object.freeze([
|
|
|
92
92
|
"Great question!",
|
|
93
93
|
]);
|
|
94
94
|
|
|
95
|
+
/* ───────────────────────────── persona discipline ────────────────────────── */
|
|
96
|
+
// Front-door session design (2026-09-08 §3.6): the agent is ONE persona to
|
|
97
|
+
// every human; parallel sessions, sub-sessions, subagents and workflows are
|
|
98
|
+
// internal workings and may never be named in outbound text, nor may "Claude
|
|
99
|
+
// Code" or "a language model". Kept in sync with scripts/hooks/pre-send-audit.sh
|
|
100
|
+
// PERSONA_PATTERNS so the PreToolUse hook and this in-process gate reject the
|
|
101
|
+
// same set. "subagent"/"sub-agent" is blocked only when followed by
|
|
102
|
+
// said|reported|session — the bare noun in an internal-process sentence is
|
|
103
|
+
// tolerated; attributing speech to a machine part is not.
|
|
104
|
+
export const PERSONA_PATTERNS = Object.freeze([
|
|
105
|
+
/claude\s+code/i,
|
|
106
|
+
/claude\s+sessions?/i,
|
|
107
|
+
/sub-sessions?/i,
|
|
108
|
+
/sub-?agents?\s+(?:said|reported|sessions?)/i,
|
|
109
|
+
/workflow\s+agents?/i,
|
|
110
|
+
/as\s+an\s+ai\b/i,
|
|
111
|
+
/language\s+model/i,
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
/** Where the persona screen reads the agent's identity + peer registry from. */
|
|
115
|
+
const PERSONA_FILES = Object.freeze({
|
|
116
|
+
agent: "config/agent.json",
|
|
117
|
+
peers: "state/session/peers.json",
|
|
118
|
+
});
|
|
119
|
+
|
|
95
120
|
/* ────────────────────────── recipient classification ─────────────────────── */
|
|
96
121
|
|
|
97
122
|
/**
|
|
@@ -262,6 +287,84 @@ export function screenBannedPhrases(text) {
|
|
|
262
287
|
return null;
|
|
263
288
|
}
|
|
264
289
|
|
|
290
|
+
function escapeRe(str) {
|
|
291
|
+
return String(str).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* (a2) Persona-leak screen. Pure; no IO. Returns a block reason (naming the
|
|
296
|
+
* offending phrase so the caller can rewrite in its own voice) or null.
|
|
297
|
+
*
|
|
298
|
+
* Session names are BOUNDED, not guessed: `<first>-main` is always internal,
|
|
299
|
+
* and any other name is internal only when it is in `sessionNames` (the
|
|
300
|
+
* registry `maestro session spawn` keeps in state/session/peers.json). A
|
|
301
|
+
* colleague called Marc-Antoine, the URL slug james-kirkland or the mailbox
|
|
302
|
+
* ethan-miller@ therefore pass, while `alex-main` / a registered `alex-deck`
|
|
303
|
+
* do not. Matching is word-bounded on [A-Za-z0-9_-] and case-insensitive.
|
|
304
|
+
*
|
|
305
|
+
* @param {string} text
|
|
306
|
+
* @param {{first?:string, sessionNames?:string[]}} [o]
|
|
307
|
+
* @returns {string|null}
|
|
308
|
+
*/
|
|
309
|
+
export function screenPersona(text, o = {}) {
|
|
310
|
+
const t = typeof text === "string" ? text : "";
|
|
311
|
+
if (!t) return null;
|
|
312
|
+
for (const re of PERSONA_PATTERNS) {
|
|
313
|
+
const m = re.exec(t);
|
|
314
|
+
if (m) {
|
|
315
|
+
return `persona leak "${m[0]}" — rewrite in your own voice (my team / a colleague); never name the machinery`;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const first = String(o.first || "").trim().toLowerCase().replace(/[^a-z0-9-]+/g, "");
|
|
319
|
+
const names = new Set();
|
|
320
|
+
if (first) names.add(`${first}-main`);
|
|
321
|
+
for (const n of Array.isArray(o.sessionNames) ? o.sessionNames : []) {
|
|
322
|
+
const v = String(n || "").trim().toLowerCase();
|
|
323
|
+
if (v) names.add(v);
|
|
324
|
+
}
|
|
325
|
+
for (const name of names) {
|
|
326
|
+
const re = new RegExp(`(^|[^A-Za-z0-9_-])(${escapeRe(name)})(?![A-Za-z0-9_-])`, "i");
|
|
327
|
+
const m = re.exec(t);
|
|
328
|
+
if (m) {
|
|
329
|
+
return `persona leak "${m[2]}" is an internal session name — say "my team" or "a colleague" instead`;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Resolve the persona inputs from disk (fail-open: anything unreadable is
|
|
337
|
+
* simply absent, and the name check is then skipped while the phrase check
|
|
338
|
+
* still runs). Explicit values win over the files.
|
|
339
|
+
*/
|
|
340
|
+
function personaContext(fs, agentRoot, { agentFirst, sessionNames }) {
|
|
341
|
+
let first = typeof agentFirst === "string" ? agentFirst : "";
|
|
342
|
+
if (!first) {
|
|
343
|
+
const a = readPolicy(fs, agentRoot, PERSONA_FILES.agent);
|
|
344
|
+
if (a.ok) {
|
|
345
|
+
try {
|
|
346
|
+
const doc = JSON.parse(a.text) || {};
|
|
347
|
+
first = String(doc.firstName || String(doc.fullName || "").split(/\s+/)[0] || "");
|
|
348
|
+
} catch { first = ""; }
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
let names = Array.isArray(sessionNames) ? sessionNames : null;
|
|
352
|
+
if (!names) {
|
|
353
|
+
names = [];
|
|
354
|
+
const p = readPolicy(fs, agentRoot, PERSONA_FILES.peers);
|
|
355
|
+
if (p.ok) {
|
|
356
|
+
try {
|
|
357
|
+
const doc = JSON.parse(p.text);
|
|
358
|
+
const list = Array.isArray(doc) ? doc : doc && Array.isArray(doc.peers) ? doc.peers : [];
|
|
359
|
+
for (const peer of list) {
|
|
360
|
+
if (peer && typeof peer === "object" && typeof peer.name === "string" && peer.name) names.push(peer.name);
|
|
361
|
+
}
|
|
362
|
+
} catch { /* a corrupt registry is an empty one */ }
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return { first, sessionNames: names };
|
|
366
|
+
}
|
|
367
|
+
|
|
265
368
|
/**
|
|
266
369
|
* (b) AI-disclosure screen. Resolves the active posture for the recipient's
|
|
267
370
|
* jurisdiction from ai-disclosure.yaml and enforces two things:
|
|
@@ -431,6 +534,8 @@ export function screenAllowlist(recipient, allowlist) {
|
|
|
431
534
|
* @param {boolean} [args.firstContact=true] is this the first message in the convo
|
|
432
535
|
* @param {string[]} [args.internalDomains] email domains treated as internal
|
|
433
536
|
* @param {string[]} [args.allowlist] recipient allowlist (opt-in)
|
|
537
|
+
* @param {string} [args.agentFirst] agent first name for the session-name check (default: agentRoot/config/agent.json)
|
|
538
|
+
* @param {string[]} [args.sessionNames] internal session names (default: agentRoot/state/session/peers.json)
|
|
434
539
|
* @param {object} [args.fs] injected { readFileSync, existsSync } for tests
|
|
435
540
|
* @returns {Promise<{allow:boolean, reason:string, redactedText?:string}>}
|
|
436
541
|
*/
|
|
@@ -443,6 +548,8 @@ export async function screenOutbound({
|
|
|
443
548
|
firstContact = true,
|
|
444
549
|
internalDomains = [],
|
|
445
550
|
allowlist = undefined,
|
|
551
|
+
agentFirst = undefined,
|
|
552
|
+
sessionNames = undefined,
|
|
446
553
|
fs: injectedFs = undefined,
|
|
447
554
|
} = {}) {
|
|
448
555
|
const fs = makeFs(injectedFs);
|
|
@@ -482,6 +589,14 @@ export async function screenOutbound({
|
|
|
482
589
|
const bannedReason = screenBannedPhrases(text);
|
|
483
590
|
if (bannedReason) return blocked(bannedReason);
|
|
484
591
|
|
|
592
|
+
// (a2) Persona discipline — pure over the identity + peer registry read
|
|
593
|
+
// here (fail-open: no identity → the name check is skipped, the
|
|
594
|
+
// phrase check still runs). This is what makes `maestro inbox reply`,
|
|
595
|
+
// the daemon's delivery and the org MCP tools persona-safe without a
|
|
596
|
+
// shell hook in the loop.
|
|
597
|
+
const personaReason = screenPersona(text, personaContext(fs, agentRoot, { agentFirst, sessionNames }));
|
|
598
|
+
if (personaReason) return blocked(personaReason);
|
|
599
|
+
|
|
485
600
|
// (b) AI-disclosure. The truthfulness invariant is enforced from the
|
|
486
601
|
// embedded denial list even without the file; posture resolution needs
|
|
487
602
|
// the file. Missing file → invariant still runs; posture defaults apply.
|
|
@@ -24,6 +24,7 @@ import { tmpdir } from "node:os";
|
|
|
24
24
|
import {
|
|
25
25
|
screenOutbound,
|
|
26
26
|
screenBannedPhrases,
|
|
27
|
+
screenPersona,
|
|
27
28
|
screenDisclosure,
|
|
28
29
|
screenBarriers,
|
|
29
30
|
screenAllowlist,
|
|
@@ -655,3 +656,115 @@ describe("screenOutbound — real-disk read path", () => {
|
|
|
655
656
|
}
|
|
656
657
|
});
|
|
657
658
|
});
|
|
659
|
+
|
|
660
|
+
/* ─── persona discipline (front-door session §3.6) ─────────────────────────
|
|
661
|
+
* The agent is one persona to every human. The machinery — Claude Code, a
|
|
662
|
+
* session, a sub-session, a subagent that "said", a workflow agent, the model,
|
|
663
|
+
* an internal session name — never appears in outbound text. This screen runs
|
|
664
|
+
* inside screenOutbound so EVERY lane gets it: the daemon's delivery
|
|
665
|
+
* (`maestro inbox reply`), lib/org/messaging.sendMessage, the org MCP tools
|
|
666
|
+
* (messaging_send, email_send, email_draft_send, org_rpc) and the adapters —
|
|
667
|
+
* not only the PreToolUse shell hook.
|
|
668
|
+
*/
|
|
669
|
+
|
|
670
|
+
describe("screenPersona (unit)", () => {
|
|
671
|
+
for (const [label, text] of [
|
|
672
|
+
["Claude Code", "I'll have Claude Code pick that up this afternoon."],
|
|
673
|
+
["Claude session", "A separate claude session is running the numbers."],
|
|
674
|
+
["sub-session", "I kicked off a sub-session for the deck."],
|
|
675
|
+
["subagent said", "The subagent said the deploy finished."],
|
|
676
|
+
["sub-agent reported", "My sub-agent reported two blockers."],
|
|
677
|
+
["subagent session", "The subagent session is still going."],
|
|
678
|
+
["workflow agent", "The workflow agent will file the summary."],
|
|
679
|
+
["language model", "Every language model has limits, mine included."],
|
|
680
|
+
]) {
|
|
681
|
+
it(`blocks "${label}" and names the phrase`, () => {
|
|
682
|
+
const reason = screenPersona(text);
|
|
683
|
+
assert.ok(reason, `expected a block for "${label}"`);
|
|
684
|
+
assert.match(reason, /persona leak/i);
|
|
685
|
+
assert.match(reason.toLowerCase(), new RegExp(label.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\s+/g, "\\s+")));
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
it("session names are BOUNDED: <first>-main always, other <first>-<x> only when registered as a peer", () => {
|
|
690
|
+
assert.ok(screenPersona("Check with alex-main for the latest.", { first: "alex" }));
|
|
691
|
+
assert.ok(screenPersona("Ask ALEX-MAIN.", { first: "alex" }), "case-insensitive");
|
|
692
|
+
assert.equal(screenPersona("alex-research has the draft ready.", { first: "alex" }), null, "unregistered peer name: not a session name");
|
|
693
|
+
assert.ok(screenPersona("alex-research has the draft ready.", { first: "alex", sessionNames: ["alex-research"] }));
|
|
694
|
+
assert.ok(screenPersona("alex-research has the draft ready.", { sessionNames: ["alex-research"] }), "names work without a first name");
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
it("does not block a colleague's hyphenated name, a URL slug or an email local part that starts with the first name", () => {
|
|
698
|
+
for (const [first, text] of [
|
|
699
|
+
["marc", "Marc-Antoine confirmed the numbers."],
|
|
700
|
+
["james", "See https://os.cohortapp.com/spaces/james-kirkland for the thread."],
|
|
701
|
+
["ethan", "Loop in ethan-miller@example.com on the invoice."],
|
|
702
|
+
["alex", "Alexandra-Maine sent the invoice."],
|
|
703
|
+
["alex", "alex-mainland is the venue."],
|
|
704
|
+
]) {
|
|
705
|
+
assert.equal(screenPersona(text, { first }), null, `"${text}" must pass for first=${first}`);
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
it("colleague phrasing, the bare noun 'subagents', and the calendar sense of 'session' all pass", () => {
|
|
710
|
+
for (const text of [
|
|
711
|
+
"One of my analysts is on it; I'll have the numbers by 3.",
|
|
712
|
+
"My team has the deck in review — sending the draft after lunch.",
|
|
713
|
+
"We use subagents internally for the heavy lifting.",
|
|
714
|
+
"The session on Thursday (the board session) starts at 9.",
|
|
715
|
+
"",
|
|
716
|
+
]) {
|
|
717
|
+
assert.equal(screenPersona(text, { first: "alex" }), null, `"${text}" must pass`);
|
|
718
|
+
}
|
|
719
|
+
assert.equal(screenPersona(null), null);
|
|
720
|
+
});
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
describe("screenOutbound — persona", () => {
|
|
724
|
+
const PERSONA_FILES = {
|
|
725
|
+
...GOOD_FILES,
|
|
726
|
+
"config/agent.json": JSON.stringify({ firstName: "Alex", fullName: "Alex Rivera" }),
|
|
727
|
+
"state/session/peers.json": JSON.stringify([{ name: "alex-deck", muxName: "maestro-alex-deck", purpose: "deck" }]),
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
it("blocks a persona leak for an internal Cohort recipient (the `maestro inbox reply` lane)", async () => {
|
|
731
|
+
const res = await screenOutbound({
|
|
732
|
+
channel: "cohort",
|
|
733
|
+
recipient: "clx0123456789abcdefghij",
|
|
734
|
+
text: "My sub-session is on it, check with alex-main.",
|
|
735
|
+
agentRoot: ROOT,
|
|
736
|
+
firstContact: false,
|
|
737
|
+
fs: memFs(PERSONA_FILES),
|
|
738
|
+
});
|
|
739
|
+
assert.equal(res.allow, false);
|
|
740
|
+
assert.match(res.reason, /persona leak/i);
|
|
741
|
+
assert.match(res.reason, /sub-session/i);
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
it("reads the first name from config/agent.json and the peer names from state/session/peers.json under agentRoot", async () => {
|
|
745
|
+
const fs = memFs(PERSONA_FILES);
|
|
746
|
+
const main = await screenOutbound({ channel: "cohort", recipient: "clx0123456789abcdefghij", text: "alex-main will chase it.", agentRoot: ROOT, firstContact: false, fs });
|
|
747
|
+
assert.equal(main.allow, false);
|
|
748
|
+
assert.match(main.reason, /alex-main/);
|
|
749
|
+
const peer = await screenOutbound({ channel: "cohort", recipient: "clx0123456789abcdefghij", text: "alex-deck has the slides.", agentRoot: ROOT, firstContact: false, fs });
|
|
750
|
+
assert.equal(peer.allow, false, "a registered peer name is a session name");
|
|
751
|
+
const other = await screenOutbound({ channel: "cohort", recipient: "clx0123456789abcdefghij", text: "alex-research is not a peer here.", agentRoot: ROOT, firstContact: false, fs });
|
|
752
|
+
assert.equal(other.allow, true, "an unregistered <first>-x is ordinary text");
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
it("explicit agentFirst / sessionNames override the disk read; no identity on disk → phrase gate still applies, name gate skipped", async () => {
|
|
756
|
+
const fs = memFs(GOOD_FILES);
|
|
757
|
+
const named = await screenOutbound({ channel: "cohort", recipient: "clx0123456789abcdefghij", text: "robin-main will chase it.", agentRoot: ROOT, firstContact: false, fs, agentFirst: "Robin" });
|
|
758
|
+
assert.equal(named.allow, false);
|
|
759
|
+
const none = await screenOutbound({ channel: "cohort", recipient: "clx0123456789abcdefghij", text: "robin-main will chase it.", agentRoot: ROOT, firstContact: false, fs });
|
|
760
|
+
assert.equal(none.allow, true, "no first name resolvable → the name check is skipped (fail-open on that one check)");
|
|
761
|
+
const phrase = await screenOutbound({ channel: "cohort", recipient: "clx0123456789abcdefghij", text: "Claude Code is on it.", agentRoot: ROOT, firstContact: false, fs });
|
|
762
|
+
assert.equal(phrase.allow, false, "the phrase gate needs no identity");
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
it("applies to external email too", async () => {
|
|
766
|
+
const res = await screenOutbound({ channel: "email", recipient: "graham@kpm.com", text: "Our workflow agent will send the summary. I am an AI assistant, for the record.", agentRoot: ROOT, fs: memFs(PERSONA_FILES) });
|
|
767
|
+
assert.equal(res.allow, false);
|
|
768
|
+
assert.match(res.reason, /persona leak/i);
|
|
769
|
+
});
|
|
770
|
+
});
|