@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
|
@@ -42,6 +42,12 @@ function loadAgent() {
|
|
|
42
42
|
return _agent;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
// Self-echo guard (25/27/30 Aug recurrences): the poll lane re-ingested this
|
|
46
|
+
// agent's OWN outbound sends as fresh inbound and fired holding acks at them.
|
|
47
|
+
// The old inline check compared `sender` against "isla-roselli" while real
|
|
48
|
+
// events carry "Isla Roselli" — it never matched. The match now lives in one
|
|
49
|
+
// tested module and every drop is audited to logs/daemon/self-echo-drops.jsonl.
|
|
50
|
+
import { isSelfSender, logSelfEchoDrop } from "./lib/self-echo.mjs";
|
|
45
51
|
import { pollSlack } from "../poller/slack-poller.mjs";
|
|
46
52
|
import { pollGmail } from "../poller/gmail-poller.mjs";
|
|
47
53
|
import { pollCalendar } from "../poller/calendar-poller.mjs";
|
|
@@ -71,6 +77,7 @@ import {
|
|
|
71
77
|
openObligations,
|
|
72
78
|
escalate,
|
|
73
79
|
closeObligation,
|
|
80
|
+
isPidAlive,
|
|
74
81
|
} from "./assurance.mjs";
|
|
75
82
|
import { recordPoll, recordClassification, recordSession, writeHealthDashboard } from "./health.mjs";
|
|
76
83
|
import { acquireLock, releaseLock, updateLock, scanStaleLocks, acquireThreadLock, claimRequest, hasActiveClaim, sweepStaleItemClaims, sanitiseItemId } from "./session-lock.mjs";
|
|
@@ -85,7 +92,15 @@ const RUNG_MECHANISM = Object.fromEntries(RUNGS.map((r) => [r.id, r.mechanism]))
|
|
|
85
92
|
// work and land it in the org's shared, ACL'd store so the fleet's memory
|
|
86
93
|
// compounds. Fail-open + best-effort — never blocks or throws into the hot path;
|
|
87
94
|
// local SQLite/interaction logs remain the fail-open cache.
|
|
88
|
-
import { isEnabled as orgEnabled, loadOrgConfig, configFromAgent, electResponder, fetchSelfProfile } from "../../lib/org/client.mjs";
|
|
95
|
+
import { isEnabled as orgEnabled, loadOrgConfig, configFromAgent, electResponder, fetchSelfProfile, listMineResult as orgListMineResult } from "../../lib/org/client.mjs";
|
|
96
|
+
// board.mine cache (design §3.5, WP-M6): the daemon writes state/org/board-mine.json
|
|
97
|
+
// every 5 min; the SessionStart primer, session_status and `maestro board mine`
|
|
98
|
+
// read it. The when/what is pure (lib/org/board-mine-cache.mjs); the I/O is here.
|
|
99
|
+
import { shouldRefresh as boardMineShouldRefresh, renderRefresh as renderBoardMineRefresh, cacheTs as boardMineCacheTs, BOARD_MINE_REL } from "../../lib/org/board-mine-cache.mjs";
|
|
100
|
+
import { writeJsonAtomic as writeJsonAtomicFile } from "../../lib/fs-atomic.mjs";
|
|
101
|
+
import { brandingGetFoundation } from "../../lib/org/client.mjs";
|
|
102
|
+
import { shouldRefreshDesign, foundationVersion, isUnversionedFoundation, DESIGN_REFRESH_INTERVAL_MS } from "../../lib/design/refresh-gate.mjs";
|
|
103
|
+
import { syncDesign, readFoundationState, workspaceName as designWorkspaceName } from "../../lib/design/write.mjs";
|
|
89
104
|
import { remember as orgRemember } from "../../lib/org/knowledge.mjs";
|
|
90
105
|
import { sweepSessionOutcomes, resultTextFromStdout } from "./session-outcomes.mjs";
|
|
91
106
|
import { recordWorkStep as orgRecordWorkStep, cohortChannelId, cohortMessageId } from "../../lib/org/work-ledger.mjs";
|
|
@@ -117,6 +132,13 @@ import { getHookBus } from "../../lib/hooks/bus.mjs";
|
|
|
117
132
|
// classifier path below is the mechanism the ladder's `react` effect drives,
|
|
118
133
|
// not a parallel universe.
|
|
119
134
|
import { processOne } from "../../lib/execution/pipeline.mjs";
|
|
135
|
+
// FRONT DOOR (design §3.1/§3.3). When the seat's main session is live, the
|
|
136
|
+
// daemon leaves Cohort inbox items in place for the session to claim instead
|
|
137
|
+
// of spawning `claude --print`; not live → today's dispatch, unchanged. The
|
|
138
|
+
// gate is pure (lib/session/frontdoor.mjs); the assurance sweep reopens a
|
|
139
|
+
// session claim that was neither replied nor done within 20 min.
|
|
140
|
+
import { makeFrontDoorGate } from "../../lib/session/frontdoor.mjs";
|
|
141
|
+
import { sweepSessionInboxForDaemon } from "../../lib/session/inbox-claims.mjs";
|
|
120
142
|
import { defaultEffects, scheduleToQueue } from "../../lib/execution/effects.mjs";
|
|
121
143
|
import { loadReactObligations } from "../../lib/execution/match.mjs";
|
|
122
144
|
import { policyFor } from "../../lib/execution/surface-policy.mjs";
|
|
@@ -227,8 +249,13 @@ async function poll() {
|
|
|
227
249
|
* cohort path can run on its own fast interval (pollCohortFast) without waiting
|
|
228
250
|
* on the 60s full poll. Returns the count of new items processed. Never throws.
|
|
229
251
|
*/
|
|
252
|
+
const _frontDoorGate = makeFrontDoorGate({ agentRoot: AGENT_REPO_DIR });
|
|
230
253
|
async function pollService(svc) {
|
|
231
254
|
try {
|
|
255
|
+
// Front door: a live main session owns this service's inbox — leave it.
|
|
256
|
+
const fd = _frontDoorGate.check(svc.name);
|
|
257
|
+
if (fd.changed) console.log(`[daemon] front door for ${svc.name}: ${fd.dispatch ? "daemon dispatches" : "main session owns intake"} (${fd.reason})`);
|
|
258
|
+
if (!fd.dispatch) return 0;
|
|
232
259
|
const result = await svc.fn();
|
|
233
260
|
// Dedup by raw_ref first: the same Slack message arrives via multiple paths
|
|
234
261
|
// (channel scan, thread scan, events-server JSON) with different item IDs.
|
|
@@ -236,12 +263,18 @@ async function pollService(svc) {
|
|
|
236
263
|
// once regardless of path.
|
|
237
264
|
const seenRefs = new Set();
|
|
238
265
|
const newItems = result.items.filter((item) => {
|
|
239
|
-
//
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
266
|
+
// SELF-ECHO DROP (ingestion guard #1). The agent's own outbound, echoed
|
|
267
|
+
// back by a lane, is not inbound. Dropped LOUDLY (audit log) and the inbox
|
|
268
|
+
// file is retired — without the rename the same echo was re-scanned and
|
|
269
|
+
// re-considered every tick forever. The old check here compared `sender`
|
|
270
|
+
// against `fullName` with spaces folded to dashes, which never matched the
|
|
271
|
+
// real event shape ("Isla Roselli") — the exact reason the 25/27/30 Aug
|
|
272
|
+
// self-reply loops got past it.
|
|
273
|
+
if (isSelfSender(item)) {
|
|
274
|
+
logSelfEchoDrop(item, { stage: "poll", service: svc.name });
|
|
275
|
+
markProcessed(item, svc.name);
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
245
278
|
|
|
246
279
|
const lockKey = item.raw_ref || item.id || `${svc.name}-${Date.now()}`;
|
|
247
280
|
if (seenRefs.has(lockKey)) return false;
|
|
@@ -278,7 +311,7 @@ async function pollService(svc) {
|
|
|
278
311
|
// against the 60s poll's own cohort scan (kept as a safety net).
|
|
279
312
|
let _cohortPoller = null;
|
|
280
313
|
let _cohortFastRunning = false;
|
|
281
|
-
async function pollCohortFast() {
|
|
314
|
+
export async function pollCohortFast() {
|
|
282
315
|
if (_cohortFastRunning) return 0;
|
|
283
316
|
if (!_existsSync(join(AGENT_REPO_DIR, "config/org.yaml"))) return 0;
|
|
284
317
|
if (!_cohortPoller) _cohortPoller = makeInboxScanPoller("cohort", { agentRoot: AGENT_REPO_DIR });
|
|
@@ -339,6 +372,18 @@ async function processItemTraced(item, service, itemId, trace_id, deps = {}) {
|
|
|
339
372
|
// regression-prone line in the file.
|
|
340
373
|
enrichItem(item, service);
|
|
341
374
|
|
|
375
|
+
// ── SELF-ECHO DROP (guard #2, the choke point) ───────────────────────────
|
|
376
|
+
// Every entry path — the 60s poll, the fast cohort scan, crash recovery —
|
|
377
|
+
// crosses this line, so an echo that slipped past a lane's own filter is
|
|
378
|
+
// still stopped before it can be classified, elected on, acked or answered.
|
|
379
|
+
// Retiring the file is part of the drop: an un-retired echo is re-delivered
|
|
380
|
+
// next poll and re-dropped forever, which is noise pretending to be safety.
|
|
381
|
+
if (isSelfSender(item)) {
|
|
382
|
+
logSelfEchoDrop(item, { stage: "process", service });
|
|
383
|
+
markProcessed(item, service);
|
|
384
|
+
return { ok: true, path: "filtered", reason: "self_echo" };
|
|
385
|
+
}
|
|
386
|
+
|
|
342
387
|
// ── THE EXECUTION LADDER, at the front of the chain ──────────────────────
|
|
343
388
|
// Returns `{ handled }`. `handled:true` means the ladder took responsibility
|
|
344
389
|
// for this item — it ignored it (journalled), queued it, delegated it,
|
|
@@ -899,6 +944,20 @@ export async function answerItem(item, service, itemId, trace_id, deps = {}, rou
|
|
|
899
944
|
const ackVerdict = _claudeAvailable()
|
|
900
945
|
? shouldAcknowledge({ willSpawnSession: true, item, source: "inbox" })
|
|
901
946
|
: { ack: false, reason: "claude-unavailable" };
|
|
947
|
+
// ── DISPATCH GATE: emitter-class inbound spawns NOTHING ─────────────────
|
|
948
|
+
// `emitterClass: true` means the gate read the inbound and recognised output
|
|
949
|
+
// this seat's own machinery class produces — a peer agent's ack-shaped
|
|
950
|
+
// one-liner, or another daemon's narrator line (progress/failure/terminal).
|
|
951
|
+
// Suppressing only the ack while still spawning would keep the expensive
|
|
952
|
+
// half of the 25/27/30 Aug loop: a 15-45 minute session burned answering a
|
|
953
|
+
// reflex. The gate already audited the refusal
|
|
954
|
+
// (logs/daemon/dispatch-gate-drops.jsonl); retire the item and stop here.
|
|
955
|
+
if (!ackVerdict.ack && ackVerdict.emitterClass) {
|
|
956
|
+
console.log(`[daemon] dispatch gate: no ack, no session for ${itemId} (${ackVerdict.reason})`);
|
|
957
|
+
counters.bump("dispatch.gate_refused", { service, reason: ackVerdict.reason });
|
|
958
|
+
markProcessed(item, service);
|
|
959
|
+
return { ok: true, path: "filtered", reason: ackVerdict.reason };
|
|
960
|
+
}
|
|
902
961
|
try {
|
|
903
962
|
// The DEBT is opened whether or not we can speak. Those are two different
|
|
904
963
|
// questions and conflating them is what made a whole class of ask
|
|
@@ -1712,6 +1771,127 @@ export function _resetDaemonOrgCfgCache() {
|
|
|
1712
1771
|
_cachedDaemonOrgCfg = undefined;
|
|
1713
1772
|
}
|
|
1714
1773
|
|
|
1774
|
+
/**
|
|
1775
|
+
* Refresh state/org/board-mine.json from hq `board.mine` when the cache is
|
|
1776
|
+
* 5 min old (or absent). Fail-open end to end: an un-enrolled seat writes
|
|
1777
|
+
* nothing; a FAILED read (hq unreachable) keeps the previous items and marks
|
|
1778
|
+
* the document `stale: true` with `itemsAt` (lib/org/board-mine-cache.mjs
|
|
1779
|
+
* renderRefresh) — one bad tick must not blank the primer, session_status
|
|
1780
|
+
* and `maestro board mine` for five minutes. `listMineImpl` may return the
|
|
1781
|
+
* `{ok, items}` shape of listMineResult or a bare array (= ok).
|
|
1782
|
+
* @param {{now?:number, listMineImpl?:Function, force?:boolean}} [deps]
|
|
1783
|
+
* @returns {Promise<{refreshed:boolean, reason:string, count?:number}>}
|
|
1784
|
+
*/
|
|
1785
|
+
export async function refreshBoardMineCache(deps = {}) {
|
|
1786
|
+
const now = Number.isFinite(deps.now) ? deps.now : Date.now();
|
|
1787
|
+
const path = join(AGENT_REPO_DIR, BOARD_MINE_REL);
|
|
1788
|
+
let cur = null;
|
|
1789
|
+
try { cur = JSON.parse(readFileSync(path, "utf8")); } catch { cur = null; /* absent or corrupt → refresh */ }
|
|
1790
|
+
if (!deps.force && !boardMineShouldRefresh(boardMineCacheTs(cur), now)) return { refreshed: false, reason: "fresh" };
|
|
1791
|
+
const cfg = daemonOrgCfg();
|
|
1792
|
+
if (!orgEnabled(cfg)) return { refreshed: false, reason: "org-disabled" };
|
|
1793
|
+
const conn = configFromAgent(cfg) || {};
|
|
1794
|
+
const result = await (deps.listMineImpl || orgListMineResult)({ base: conn.base, token: conn.token, orgId: conn.orgId });
|
|
1795
|
+
const doc = renderBoardMineRefresh(result, cur, now);
|
|
1796
|
+
writeJsonAtomicFile(path, doc);
|
|
1797
|
+
return { refreshed: true, reason: doc.stale ? "kept-previous" : "written", count: doc.items.length };
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
/**
|
|
1801
|
+
* Re-sync the workspace brand foundation into DESIGN.md + PRODUCT.md (WP-M7
|
|
1802
|
+
* mechanic 4), in the same idiom as `refreshIntegrationTools` in
|
|
1803
|
+
* `lib/org/mesh.mjs`: a cheap gated poll on the beat, fail-open end to end,
|
|
1804
|
+
* keeping the last-good files when hq is unreachable.
|
|
1805
|
+
*
|
|
1806
|
+
* WHY THIS EXISTS AT ALL. Every design skill on the seat is told to read
|
|
1807
|
+
* `$AGENT_ROOT/DESIGN.md` before it picks a colour or a typeface. A designer
|
|
1808
|
+
* working from a palette a human replaced last week is worse than one with no
|
|
1809
|
+
* palette, because the stale file looks authoritative — so the seat follows the
|
|
1810
|
+
* foundation rather than waiting for someone to re-run the CLI.
|
|
1811
|
+
*
|
|
1812
|
+
* THE GATE IS ASKED TWICE (see `lib/design/refresh-gate.mjs`): once before the
|
|
1813
|
+
* read, with the version unknown, to decide whether to spend a call at all
|
|
1814
|
+
* (15-minute floor); once after, with the version hq returned, to say honestly
|
|
1815
|
+
* WHY this tick refreshed. hq exposes no cheap "has the foundation changed"
|
|
1816
|
+
* etag the way `integration.toolsetVersion` does, so the version comparison can
|
|
1817
|
+
* only happen after a read and the interval is what bounds the reads.
|
|
1818
|
+
*
|
|
1819
|
+
* FAIL-OPEN, AND SILENT ABOUT NOTHING. An un-enrolled seat writes nothing and
|
|
1820
|
+
* says so; a fetch failure keeps the files that are already on disk and logs
|
|
1821
|
+
* the reason. The write itself is byte-idempotent (`syncDesign`), so a steady
|
|
1822
|
+
* foundation never touches an mtime — which is what keeps a git tree clean and
|
|
1823
|
+
* an editor's file watcher quiet on a seat that polls every 15 minutes.
|
|
1824
|
+
*
|
|
1825
|
+
* @param {{now?:number, intervalMs?:number, force?:boolean, getFoundationImpl?:Function, fetchImpl?:Function}} [deps]
|
|
1826
|
+
* @returns {Promise<{refreshed:boolean, reason:string, version?:string|null, written?:string[]}>}
|
|
1827
|
+
*/
|
|
1828
|
+
export async function refreshDesignFoundation(deps = {}) {
|
|
1829
|
+
const now = Number.isFinite(deps.now) ? deps.now : Date.now();
|
|
1830
|
+
const intervalMs = Number.isFinite(deps.intervalMs) ? deps.intervalMs : DESIGN_REFRESH_INTERVAL_MS;
|
|
1831
|
+
const prior = readFoundationState(AGENT_REPO_DIR);
|
|
1832
|
+
const before = shouldRefreshDesign({
|
|
1833
|
+
lastVersion: prior ? prior.version : null,
|
|
1834
|
+
currentVersion: null,
|
|
1835
|
+
lastAt: prior ? prior.fetchedAt : null,
|
|
1836
|
+
now,
|
|
1837
|
+
intervalMs,
|
|
1838
|
+
});
|
|
1839
|
+
if (!deps.force && !before.refresh) return { refreshed: false, reason: "fresh" };
|
|
1840
|
+
|
|
1841
|
+
const cfg = daemonOrgCfg();
|
|
1842
|
+
if (!orgEnabled(cfg)) return { refreshed: false, reason: "org-disabled" };
|
|
1843
|
+
const conn = configFromAgent(cfg) || {};
|
|
1844
|
+
|
|
1845
|
+
let frame;
|
|
1846
|
+
try {
|
|
1847
|
+
frame = await (deps.getFoundationImpl || brandingGetFoundation)(
|
|
1848
|
+
{ historyLimit: 0 },
|
|
1849
|
+
{ base: conn.base, token: conn.token, orgId: conn.orgId, fetchImpl: deps.fetchImpl },
|
|
1850
|
+
);
|
|
1851
|
+
} catch (err) {
|
|
1852
|
+
console.warn(`[daemon] design sync: foundation read failed (${err && err.message ? err.message : err}) — keeping the DESIGN.md already on disk`);
|
|
1853
|
+
return { refreshed: false, reason: "fetch-failed" };
|
|
1854
|
+
}
|
|
1855
|
+
if (!frame || !frame.ok || !frame.result || typeof frame.result !== "object" || !frame.result.foundation) {
|
|
1856
|
+
const why = frame && frame.error && frame.error.code ? frame.error.code : "no-foundation";
|
|
1857
|
+
return { refreshed: false, reason: why === "no-foundation" ? "no-foundation" : `error:${why}` };
|
|
1858
|
+
}
|
|
1859
|
+
// A workspace that has never SAVED a foundation reads back hq's own product
|
|
1860
|
+
// defaults with `unversioned: true` — a complete palette that is nobody's
|
|
1861
|
+
// brand. Writing it would ground every design skill on the seat in the wrong
|
|
1862
|
+
// palette, with a generated file's authority behind it. Nothing is written
|
|
1863
|
+
// and nothing already on disk is disturbed; see
|
|
1864
|
+
// refresh-gate.mjs#isUnversionedFoundation.
|
|
1865
|
+
if (isUnversionedFoundation(frame.result)) return { refreshed: false, reason: "unversioned" };
|
|
1866
|
+
|
|
1867
|
+
// The second gate call: now that the version is known, say why this tick ran.
|
|
1868
|
+
const after = shouldRefreshDesign({
|
|
1869
|
+
lastVersion: prior ? prior.version : null,
|
|
1870
|
+
currentVersion: foundationVersion(frame.result),
|
|
1871
|
+
lastAt: prior ? prior.fetchedAt : null,
|
|
1872
|
+
now,
|
|
1873
|
+
intervalMs,
|
|
1874
|
+
});
|
|
1875
|
+
|
|
1876
|
+
let out;
|
|
1877
|
+
try {
|
|
1878
|
+
out = syncDesign({
|
|
1879
|
+
agentRoot: AGENT_REPO_DIR,
|
|
1880
|
+
result: frame.result,
|
|
1881
|
+
now,
|
|
1882
|
+
name: designWorkspaceName(cfg),
|
|
1883
|
+
source: "cohort · branding.getFoundation",
|
|
1884
|
+
});
|
|
1885
|
+
} catch (err) {
|
|
1886
|
+
console.warn(`[daemon] design sync: write failed (${err && err.message ? err.message : err})`);
|
|
1887
|
+
return { refreshed: false, reason: "write-failed" };
|
|
1888
|
+
}
|
|
1889
|
+
if (out.written.length) {
|
|
1890
|
+
console.log(`[daemon] design sync (${after.reason}) → wrote ${out.written.join(", ")}${out.version ? ` — foundation ${out.version}` : ""}`);
|
|
1891
|
+
}
|
|
1892
|
+
return { refreshed: out.written.length > 0, reason: after.reason, version: out.version, written: out.written };
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1715
1895
|
/** Longest completion note we will put on a board comment. */
|
|
1716
1896
|
const COMPLETION_NOTE_CHARS = 1200;
|
|
1717
1897
|
|
|
@@ -2179,6 +2359,38 @@ async function sweepBacklog() {
|
|
|
2179
2359
|
// Main
|
|
2180
2360
|
// ---------------------------------------------------------------------------
|
|
2181
2361
|
|
|
2362
|
+
/**
|
|
2363
|
+
* Daemon singleton ADVISORY — a pidfile check, deliberately not a lock.
|
|
2364
|
+
*
|
|
2365
|
+
* Two live daemons on one AGENT_DIR (the operator's manual run next to the
|
|
2366
|
+
* launchd one) both poll, both sweep, and both narrate; the per-obligation
|
|
2367
|
+
* ownership guard in assurance.sweepObligations bounds the damage, but the
|
|
2368
|
+
* condition itself deserves a LOUD line the moment it starts. This only logs
|
|
2369
|
+
* and counts — it never refuses to start and never exits — because launchd's
|
|
2370
|
+
* KeepAlive restart may race the old process's teardown, and a pidfile that
|
|
2371
|
+
* can veto startup under KeepAlive is a daemon that stays down. A recycled pid
|
|
2372
|
+
* can false-positive; that costs one loud log line, nothing more.
|
|
2373
|
+
*/
|
|
2374
|
+
function advisePidfileSingleton() {
|
|
2375
|
+
const pidPath = join(AGENT_REPO_DIR, "state", "daemon.pid");
|
|
2376
|
+
try {
|
|
2377
|
+
let prior = null;
|
|
2378
|
+
try { prior = parseInt(readFileSync(pidPath, "utf-8"), 10); } catch { /* no pidfile yet */ }
|
|
2379
|
+
if (Number.isFinite(prior) && prior > 0 && prior !== process.pid && isPidAlive(prior)) {
|
|
2380
|
+
console.error(
|
|
2381
|
+
`[daemon] ⚠ ANOTHER DAEMON MAY BE LIVE: pid ${prior} (from ${pidPath}) is running and also holds ${AGENT_REPO_DIR}. ` +
|
|
2382
|
+
`Two daemons on one AGENT_DIR double-poll and race the obligation ledger — stop one unless this is deliberate.`,
|
|
2383
|
+
);
|
|
2384
|
+
counters.bump("daemon.singleton_advisory", { prior_pid: prior });
|
|
2385
|
+
}
|
|
2386
|
+
mkdirSync(join(AGENT_REPO_DIR, "state"), { recursive: true });
|
|
2387
|
+
writeFileSync(pidPath, `${process.pid}\n`);
|
|
2388
|
+
} catch (err) {
|
|
2389
|
+
// Advisory only — an unwritable pidfile must never stop the daemon booting.
|
|
2390
|
+
console.warn(`[daemon] pidfile advisory unavailable: ${err.message}`);
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2182
2394
|
async function main() {
|
|
2183
2395
|
console.log("╔══════════════════════════════════════════════════════════╗");
|
|
2184
2396
|
console.log(`║ ${(loadAgent().firstName || "Agent").padEnd(8)} Daemon — Reactive Event Processor ║`);
|
|
@@ -2205,6 +2417,11 @@ async function main() {
|
|
|
2205
2417
|
// No emergency stop — continue
|
|
2206
2418
|
}
|
|
2207
2419
|
|
|
2420
|
+
// Two-daemon advisory: log loudly if another live daemon already holds this
|
|
2421
|
+
// AGENT_DIR (pidfile check; never blocks startup — see the function's note on
|
|
2422
|
+
// launchd KeepAlive semantics), then record this process as the holder.
|
|
2423
|
+
advisePidfileSingleton();
|
|
2424
|
+
|
|
2208
2425
|
// Clear orphaned state from prior daemon instances
|
|
2209
2426
|
const staleCleared = scanStaleLocks();
|
|
2210
2427
|
if (staleCleared > 0) {
|
|
@@ -2242,6 +2459,9 @@ async function main() {
|
|
|
2242
2459
|
|
|
2243
2460
|
// Initial poll immediately
|
|
2244
2461
|
await poll();
|
|
2462
|
+
// First board.mine cache write, so the primer has something before the
|
|
2463
|
+
// first health tick (fail-open; an un-enrolled seat writes nothing).
|
|
2464
|
+
refreshBoardMineCache().catch((err) => console.error("[daemon] board-mine cache error:", err.message));
|
|
2245
2465
|
|
|
2246
2466
|
// Poll loop
|
|
2247
2467
|
setInterval(async () => {
|
|
@@ -2254,6 +2474,16 @@ async function main() {
|
|
|
2254
2474
|
}
|
|
2255
2475
|
}, POLL_INTERVAL);
|
|
2256
2476
|
|
|
2477
|
+
// Brand foundation → DESIGN.md + PRODUCT.md, every 15 min (WP-M7). Fired once
|
|
2478
|
+
// at boot so a seat that has never synced has its design files before the
|
|
2479
|
+
// first session opens, then on its own interval. Fail-open and never awaited:
|
|
2480
|
+
// the design docs are guidance, and no failure to fetch them may hold up the
|
|
2481
|
+
// poll loop.
|
|
2482
|
+
refreshDesignFoundation().catch((err) => console.error("[daemon] design sync error:", err.message));
|
|
2483
|
+
setInterval(() => {
|
|
2484
|
+
refreshDesignFoundation().catch((err) => console.error("[daemon] design sync error:", err.message));
|
|
2485
|
+
}, DESIGN_REFRESH_INTERVAL_MS);
|
|
2486
|
+
|
|
2257
2487
|
// Fast reactive cohort scan — drains org-push-fetched DMs in ~2s instead of
|
|
2258
2488
|
// waiting up to 60s for the full poll. Local YAML only; no API/rate-limit cost.
|
|
2259
2489
|
setInterval(() => {
|
|
@@ -2285,6 +2515,12 @@ async function main() {
|
|
|
2285
2515
|
if (claimsSwept > 0) {
|
|
2286
2516
|
console.log(`[daemon] Swept ${claimsSwept} stale item claims`);
|
|
2287
2517
|
}
|
|
2518
|
+
// Front door: reopen session claims older than 20 min, due deferrals and
|
|
2519
|
+
// (while the session is live) the daemon's own orphaned .dispatched files.
|
|
2520
|
+
const reopened = sweepSessionInboxForDaemon(AGENT_REPO_DIR).reopened;
|
|
2521
|
+
if (reopened.length > 0) console.log(`[daemon] reopened ${reopened.length} session-claimed inbox item(s): ${reopened.map((r) => `${r.id} (${r.reason})`).join(", ")}`);
|
|
2522
|
+
// board.mine cache — the pure gate says "no" for four of every five ticks.
|
|
2523
|
+
refreshBoardMineCache().catch((err) => console.error("[daemon] board-mine cache error:", err.message));
|
|
2288
2524
|
} catch (err) {
|
|
2289
2525
|
console.error("[daemon] Health write error:", err.message);
|
|
2290
2526
|
}
|
|
@@ -2300,18 +2536,21 @@ async function main() {
|
|
|
2300
2536
|
}, HEALTH_INTERVAL);
|
|
2301
2537
|
|
|
2302
2538
|
// Graceful shutdown — clean up active.json so next startup doesn't see stale sessions
|
|
2539
|
+
// Exit honours a pre-set process.exitCode: the org-mesh "restart" directive
|
|
2540
|
+
// sets 1 before signalling so launchd's KeepAlive {SuccessfulExit:false}
|
|
2541
|
+
// relaunches the daemon; a plain SIGTERM still exits 0 (stays down).
|
|
2303
2542
|
process.on("SIGTERM", () => {
|
|
2304
2543
|
console.log("[daemon] SIGTERM received, shutting down gracefully");
|
|
2305
2544
|
resetActiveSessions();
|
|
2306
2545
|
writeHealthDashboard();
|
|
2307
|
-
process.exit(0);
|
|
2546
|
+
process.exit(process.exitCode || 0);
|
|
2308
2547
|
});
|
|
2309
2548
|
|
|
2310
2549
|
process.on("SIGINT", () => {
|
|
2311
2550
|
console.log("[daemon] SIGINT received, shutting down");
|
|
2312
2551
|
resetActiveSessions();
|
|
2313
2552
|
writeHealthDashboard();
|
|
2314
|
-
process.exit(0);
|
|
2553
|
+
process.exit(process.exitCode || 0);
|
|
2315
2554
|
});
|
|
2316
2555
|
|
|
2317
2556
|
console.log("[daemon] Running. Ctrl+C to stop.");
|
|
@@ -903,3 +903,76 @@ test("fastPathClassify: a short directed question skips the classifier; work-req
|
|
|
903
903
|
assert.ok(daemon.fastPathClassify({ content: "what OS?" }, true));
|
|
904
904
|
assert.equal(daemon.fastPathClassify({ content: "what OS?" }, true, { disableFastPath: true }), null);
|
|
905
905
|
});
|
|
906
|
+
|
|
907
|
+
// ===========================================================================
|
|
908
|
+
// SELF-ECHO — the agent's own outbound, re-ingested by a poll lane, must be
|
|
909
|
+
// dropped BEFORE it can be classified, elected on, acked or answered. Recurred
|
|
910
|
+
// live 2026-08-25 / 08-27 / 08-30: the old filter normalised the agent's full
|
|
911
|
+
// name to "isla-roselli" while real events carry "Isla Roselli", so the agent
|
|
912
|
+
// said "Looking now." to itself (and, in shared DMs, to another agent's echo).
|
|
913
|
+
// ===========================================================================
|
|
914
|
+
|
|
915
|
+
test("SELF-ECHO: an echoed own message is dropped at the processItem choke point — file retired, drop audited", async () => {
|
|
916
|
+
resetState();
|
|
917
|
+
const selfEcho = await import("./lib/self-echo.mjs");
|
|
918
|
+
const { writeFileSync: wf } = await import("node:fs");
|
|
919
|
+
mkdirSync(join(AGENT_DIR, "config"), { recursive: true });
|
|
920
|
+
wf(join(AGENT_DIR, "config", "agent.json"), JSON.stringify({
|
|
921
|
+
firstName: "Isla", fullName: "Isla Roselli", memberId: "cmqh0tcml004ihhl6zev6ocb5",
|
|
922
|
+
}));
|
|
923
|
+
selfEcho._resetSelfIdentity();
|
|
924
|
+
try {
|
|
925
|
+
// The REAL echo shape: display name verbatim, spaces and all.
|
|
926
|
+
const item = { id: "MSG-SELF", raw_ref: "slack:DSELF:1", channel: "dm/candace", channel_id: "DSELF0001", sender: "Isla Roselli", content: "Both answers — and the second one settles it." };
|
|
927
|
+
const name = writeLiveItem(item);
|
|
928
|
+
|
|
929
|
+
let pipelineTouched = null;
|
|
930
|
+
const res = await daemon.processItem(item, "slack", {
|
|
931
|
+
classify: async () => { pipelineTouched = "classify"; return { priority: "high", action: "respond", model: "opus", summary: "x", category: "action_required", directed_at_agent: true }; },
|
|
932
|
+
isQuickReply: () => { pipelineTouched = "isQuickReply"; return false; },
|
|
933
|
+
sendQuickResponse: async () => { pipelineTouched = "quick_reply"; return { sent: true }; },
|
|
934
|
+
sendHoldingMessage: async () => { pipelineTouched = "holding_ack"; return { sent: true, holdingText: "x" }; },
|
|
935
|
+
dispatch: () => { pipelineTouched = "dispatch"; },
|
|
936
|
+
claudeAvailable: () => true,
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
assert.equal(res && res.path, "filtered", "the echo is filtered, not answered");
|
|
940
|
+
assert.equal(res && res.reason, "self_echo");
|
|
941
|
+
assert.equal(pipelineTouched, null, "no classify, no ack, no reply, no session — the echo never enters the pipeline");
|
|
942
|
+
assert.deepEqual(inboxFiles(), [`${name}.processed`], "the echo's inbox file is RETIRED — an un-retired echo would be re-scanned and re-dropped every tick forever");
|
|
943
|
+
const auditPath = join(AGENT_DIR, "logs", "daemon", "self-echo-drops.jsonl");
|
|
944
|
+
assert.ok(existsSync(auditPath), "every drop leaves an audit row");
|
|
945
|
+
assert.match(readFileSync(auditPath, "utf-8"), /slack:DSELF:1/, "the audit row names the dropped event");
|
|
946
|
+
} finally {
|
|
947
|
+
rmSync(join(AGENT_DIR, "config", "agent.json"), { force: true });
|
|
948
|
+
selfEcho._resetSelfIdentity();
|
|
949
|
+
}
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
test("SELF-ECHO: a genuine colleague's message is untouched by the guard", async () => {
|
|
953
|
+
resetState();
|
|
954
|
+
const selfEcho = await import("./lib/self-echo.mjs");
|
|
955
|
+
const { writeFileSync: wf } = await import("node:fs");
|
|
956
|
+
mkdirSync(join(AGENT_DIR, "config"), { recursive: true });
|
|
957
|
+
wf(join(AGENT_DIR, "config", "agent.json"), JSON.stringify({
|
|
958
|
+
firstName: "Isla", fullName: "Isla Roselli", memberId: "cmqh0tcml004ihhl6zev6ocb5",
|
|
959
|
+
}));
|
|
960
|
+
selfEcho._resetSelfIdentity();
|
|
961
|
+
try {
|
|
962
|
+
const item = { id: "MSG-COLLEAGUE", raw_ref: "slack:DCOLL:1", channel: "dm/jordan", channel_id: "DCOLL0001", is_dm: true, sender: "Jordan Blake", content: "please draft the board memo" };
|
|
963
|
+
writeLiveItem(item);
|
|
964
|
+
let dispatched = false;
|
|
965
|
+
const res = await daemon.answerItem(item, "slack", "slack:DCOLL:1", "trace-coll", {
|
|
966
|
+
classify: async () => ({ priority: "critical", action: "respond", model: "opus", summary: "draft board memo", category: "action_required", directed_at_agent: true }),
|
|
967
|
+
isQuickReply: () => false,
|
|
968
|
+
sendHoldingMessage: async () => ({ sent: true, holdingText: "Starting on the board memo now." }),
|
|
969
|
+
dispatch: (_p, _it, _cr, _s, opts) => { dispatched = true; opts.onClose({ ok: true, code: 0 }); },
|
|
970
|
+
claudeAvailable: () => true,
|
|
971
|
+
});
|
|
972
|
+
assert.equal(dispatched, true, "a real colleague's ask still dispatches");
|
|
973
|
+
assert.equal(res.path, "session");
|
|
974
|
+
} finally {
|
|
975
|
+
rmSync(join(AGENT_DIR, "config", "agent.json"), { force: true });
|
|
976
|
+
selfEcho._resetSelfIdentity();
|
|
977
|
+
}
|
|
978
|
+
});
|