@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/org/mesh.mjs
CHANGED
|
@@ -22,6 +22,13 @@
|
|
|
22
22
|
* (lib/org/policy.fetchAndApply against the PINNED root).
|
|
23
23
|
* - "resync" → re-fetch the directory (client.fetchDirectory) into the
|
|
24
24
|
* local peers cache (state/org/peers/<id>.json).
|
|
25
|
+
* - "restart" → (audit F10) the daemon flushes and exits NON-ZERO so
|
|
26
|
+
* launchd's KeepAlive {SuccessfulExit:false} relaunches
|
|
27
|
+
* it. Never writes .emergency-stop — a restart is not a
|
|
28
|
+
* halt. Injected as requestRestart.
|
|
29
|
+
* - "session.restart" → touch state/session/restart-requested; the main
|
|
30
|
+
* session's feed sees it and the session restarts itself
|
|
31
|
+
* at an idle moment (design §3.10). The daemon stays up.
|
|
25
32
|
* 3. stop() — clears the interval so the daemon can shut the mesh down cleanly.
|
|
26
33
|
*
|
|
27
34
|
* FAIL-OPEN by design (mirrors lib/org/client): if the org is disabled
|
|
@@ -42,6 +49,7 @@
|
|
|
42
49
|
|
|
43
50
|
import { join, resolve } from "node:path";
|
|
44
51
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
52
|
+
import { writeJsonAtomic } from "../fs-atomic.mjs";
|
|
45
53
|
|
|
46
54
|
import * as defaultClient from "./client.mjs";
|
|
47
55
|
import * as policy from "./policy.mjs";
|
|
@@ -70,8 +78,13 @@ const KNOWN_DIRECTIVES = new Set([
|
|
|
70
78
|
"policy_stale",
|
|
71
79
|
"resync",
|
|
72
80
|
"toolset_changed",
|
|
81
|
+
"restart",
|
|
82
|
+
"session.restart",
|
|
73
83
|
]);
|
|
74
84
|
|
|
85
|
+
/** Where the session runtime looks for a restart request (relative to the agent root). */
|
|
86
|
+
export const SESSION_RESTART_FLAG_RELATIVE = "state/session/restart-requested";
|
|
87
|
+
|
|
75
88
|
function logInfo(msg) { try { console.log(`[org-mesh] ${msg}`); } catch { /* never throw from logging */ } }
|
|
76
89
|
function logWarn(msg) { try { console.warn(`[org-mesh] ${msg}`); } catch { /* never throw from logging */ } }
|
|
77
90
|
|
|
@@ -84,6 +97,38 @@ function defaultRequestStop() {
|
|
|
84
97
|
try { process.kill(process.pid, "SIGTERM"); } catch { /* fail-open */ }
|
|
85
98
|
}
|
|
86
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Default restart signal (the "restart" directive, audit F10). The daemon
|
|
102
|
+
* plist's KeepAlive is {SuccessfulExit:false}: launchd relaunches ONLY after a
|
|
103
|
+
* non-zero exit, and a clean exit 0 leaves the seat down. So: set exitCode 1
|
|
104
|
+
* first, then go through the daemon's own SIGTERM handlers (they flush
|
|
105
|
+
* active-session state + the health dashboard and exit honouring exitCode).
|
|
106
|
+
* If we cannot signal ourselves, exit 1 directly — a restart must not turn
|
|
107
|
+
* into a hang.
|
|
108
|
+
*/
|
|
109
|
+
function defaultRequestRestart() {
|
|
110
|
+
process.exitCode = 1;
|
|
111
|
+
try { process.kill(process.pid, "SIGTERM"); }
|
|
112
|
+
catch { try { process.exit(1); } catch { /* fail-open */ } }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Ask the main session to restart itself ("session.restart"): write the
|
|
117
|
+
* restart-requested flag the session's feed watches. Idempotent overwrite
|
|
118
|
+
* (the newest request time wins) and fail-open. Never throws.
|
|
119
|
+
*/
|
|
120
|
+
function requestSessionRestart(agentRoot, now) {
|
|
121
|
+
const at = new Date((typeof now === "function" ? now() : Date.now())).toISOString();
|
|
122
|
+
try {
|
|
123
|
+
const path = join(agentRoot, SESSION_RESTART_FLAG_RELATIVE);
|
|
124
|
+
mkdirSync(join(agentRoot, "state", "session"), { recursive: true });
|
|
125
|
+
writeFileSync(path, `${at}\n`);
|
|
126
|
+
logInfo(`session.restart directive — flagged ${SESSION_RESTART_FLAG_RELATIVE} (${at})`);
|
|
127
|
+
} catch (err) {
|
|
128
|
+
logWarn(`session.restart directive — failed to write the restart flag (${err && err.message})`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
87
132
|
/**
|
|
88
133
|
* Trip the local kill switch in response to a "halt" directive.
|
|
89
134
|
* 1. Write `.emergency-stop` (the same flag emergency-stop.sh drops) so a
|
|
@@ -187,7 +232,7 @@ async function resyncDirectory(o) {
|
|
|
187
232
|
const id = String(p.id).toLowerCase().replace(/[^a-z0-9._-]/gi, "_");
|
|
188
233
|
if (!id) continue;
|
|
189
234
|
try {
|
|
190
|
-
|
|
235
|
+
writeJsonAtomic(join(peersDir, `${id}.json`), { ...p, source: "synced" });
|
|
191
236
|
written += 1;
|
|
192
237
|
} catch { /* skip this peer, keep going */ }
|
|
193
238
|
}
|
|
@@ -279,6 +324,16 @@ async function handleDirective(directive, ctx) {
|
|
|
279
324
|
// Force an immediate refresh + disk write, ahead of the 30s poll.
|
|
280
325
|
await refreshIntegrationTools(ctx);
|
|
281
326
|
return;
|
|
327
|
+
case "restart":
|
|
328
|
+
// Stop beating first: this process is going down to come back, and a
|
|
329
|
+
// beat in flight during teardown would just be noise.
|
|
330
|
+
logWarn("restart directive received — stopping the beat loop and restarting the daemon (exit 1 → launchd relaunch)");
|
|
331
|
+
ctx.stop();
|
|
332
|
+
try { ctx.requestRestart(); } catch { /* fail-open */ }
|
|
333
|
+
return;
|
|
334
|
+
case "session.restart":
|
|
335
|
+
requestSessionRestart(ctx.agentRoot, ctx.now);
|
|
336
|
+
return;
|
|
282
337
|
default:
|
|
283
338
|
return; // forward-compatible: ignore unknown directives
|
|
284
339
|
}
|
|
@@ -389,6 +444,7 @@ async function beatOnce(ctx) {
|
|
|
389
444
|
* @param {Function} [o.clearInterval] injected interval clearer
|
|
390
445
|
* @param {object} [o.client] injected org client (defaults to lib/org/client)
|
|
391
446
|
* @param {Function}[o.requestStop] injected stop signal (defaults to SIGTERM self)
|
|
447
|
+
* @param {Function}[o.requestRestart] injected restart signal (defaults to exitCode 1 + SIGTERM self)
|
|
392
448
|
* @param {number} [o.beatMs] beat interval (defaults to DEFAULT_BEAT_MS)
|
|
393
449
|
* @param {Function}[o.collectStatus] injected status collector (defaults to lib/telemetry/collect)
|
|
394
450
|
* @returns {Promise<{stop:Function, isEnabled:boolean}>}
|
|
@@ -409,6 +465,7 @@ export async function connectOrgMesh(o = {}) {
|
|
|
409
465
|
const clearIntervalFn = typeof o.clearInterval === "function" ? o.clearInterval : clearInterval;
|
|
410
466
|
const beatMs = Number.isFinite(o.beatMs) && o.beatMs > 0 ? o.beatMs : DEFAULT_BEAT_MS;
|
|
411
467
|
const requestStop = typeof o.requestStop === "function" ? o.requestStop : defaultRequestStop;
|
|
468
|
+
const requestRestart = typeof o.requestRestart === "function" ? o.requestRestart : defaultRequestRestart;
|
|
412
469
|
|
|
413
470
|
let conn = {};
|
|
414
471
|
try { conn = client.configFromAgent(cfg) || {}; } catch { conn = {}; }
|
|
@@ -420,6 +477,7 @@ export async function connectOrgMesh(o = {}) {
|
|
|
420
477
|
fetchImpl: o.fetchImpl,
|
|
421
478
|
now,
|
|
422
479
|
requestStop,
|
|
480
|
+
requestRestart,
|
|
423
481
|
base: conn.base,
|
|
424
482
|
token: conn.token,
|
|
425
483
|
orgId: conn.orgId,
|
|
@@ -491,6 +549,8 @@ export const _internals = {
|
|
|
491
549
|
handleDirective,
|
|
492
550
|
tripKillSwitch,
|
|
493
551
|
flagRepairNeeded,
|
|
552
|
+
defaultRequestRestart,
|
|
553
|
+
requestSessionRestart,
|
|
494
554
|
resyncDirectory,
|
|
495
555
|
refreshPolicy,
|
|
496
556
|
refreshIntegrationTools,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
8bce96163171fc5b9f23ea03889cd9220113af09e14f87d704c635f33f9aa76e
|
package/lib/org/protocol.mjs
CHANGED
|
@@ -1045,6 +1045,49 @@ export const METHODS = Object.freeze({
|
|
|
1045
1045
|
"architecture.status": { family: "architecture", scope: "org.read", sideEffecting: false },
|
|
1046
1046
|
"architecture.accept": { family: "architecture", scope: "org.write", sideEffecting: true, idempotent: true },
|
|
1047
1047
|
"architecture.approve": { family: "architecture", scope: "org.write", sideEffecting: true, idempotent: true },
|
|
1048
|
+
// --- architecture, the LIVE half (2026-08): the COMMITTED company's board at
|
|
1049
|
+
// hq's /org/architecture, as opposed to the three above, which review a
|
|
1050
|
+
// Genesis DRAFT. Same family because it is the same surface outliving the
|
|
1051
|
+
// run — the reason the family was split from `genesis` in the first place.
|
|
1052
|
+
//
|
|
1053
|
+
// THREE METHODS, AND THE SHAPE OF THE THREE IS THE AUTHORITY MODEL.
|
|
1054
|
+
// hq's own rule (src/server/architecture/proposals.ts) is that a change is
|
|
1055
|
+
// gated on a reviewed record when the CHANGE has a cascade, not when the
|
|
1056
|
+
// ACTOR is a machine: axis acts and capability removal need a proposal
|
|
1057
|
+
// somebody with the floor approved; the mechanical 90% goes direct. What
|
|
1058
|
+
// an agent gets on this wire is therefore the ASKING (`propose`) and
|
|
1059
|
+
// enough of the board to ask accurately (`board`), plus the queue that
|
|
1060
|
+
// tells it what became of the ask (`proposals`). Approval is NOT here: it
|
|
1061
|
+
// is a session act in hq, and the module that owns it says why — "an agent
|
|
1062
|
+
// cannot reach an approval at all, not because a check says no, but
|
|
1063
|
+
// because there is no door. A door that does not exist has to be BUILT,
|
|
1064
|
+
// and building it is a review a person will see."
|
|
1065
|
+
//
|
|
1066
|
+
// `architecture.board` EXISTS BECAUSE THE IDS ON SCREEN ADDRESS NOTHING.
|
|
1067
|
+
// hq mints a board's entity ids BY POSITION (`P1`/`S3`/`C12`, `D1`/`T1`)
|
|
1068
|
+
// because its id schema bounds an id at 2-4 chars while a cuid is 25, and
|
|
1069
|
+
// every write resolves through a derived-id -> row-id map the browser
|
|
1070
|
+
// holds. An agent has no browser. Without this read the only ids it could
|
|
1071
|
+
// put in a proposal are guessed ones, so the read is not a convenience —
|
|
1072
|
+
// it is the addressing layer `propose` is unusable without.
|
|
1073
|
+
//
|
|
1074
|
+
// SCOPES REUSE `org.read` / `org.write` — the same argument the resource
|
|
1075
|
+
// family makes below, and here it is also the reason the family is
|
|
1076
|
+
// reachable AT ALL on the day it lands: both are in DEFAULT_AGENT_SCOPES,
|
|
1077
|
+
// `org.read` is in hq's VIEWER tier, so a paired key can read the board
|
|
1078
|
+
// today and an EDITOR key can propose. NOTE the asymmetry that follows,
|
|
1079
|
+
// deliberately: `org.write` is NOT in hq's HUMAN_DEFAULT_SCOPES, so
|
|
1080
|
+
// `architecture.propose` is an AGENT-plane verb and the human proposes
|
|
1081
|
+
// from the web app's own server action. That is the same split the
|
|
1082
|
+
// resource family notes, and it is the right way round here — the human
|
|
1083
|
+
// already has the surface; the agent is the one that had no door.
|
|
1084
|
+
//
|
|
1085
|
+
// `propose` is NOT idempotent, and the omission is the declaration: a
|
|
1086
|
+
// second call with the same params is a second, honestly distinct ask, and
|
|
1087
|
+
// caching one under a replay key would silently swallow it.
|
|
1088
|
+
"architecture.board": { family: "architecture", scope: "org.read", sideEffecting: false },
|
|
1089
|
+
"architecture.propose": { family: "architecture", scope: "org.write", sideEffecting: true },
|
|
1090
|
+
"architecture.proposals": { family: "architecture", scope: "org.read", sideEffecting: false },
|
|
1048
1091
|
// --- resource (2026-08): THE VENTURE-DELIVERABLE CATALOGUE. `OrgResource` is
|
|
1049
1092
|
// the org-scoped index of a venture's own artifacts — one row per
|
|
1050
1093
|
// deliverable, keyed `(orgId, slug)`, provisioned by the portal
|
|
@@ -1163,6 +1206,11 @@ export const READS = Object.freeze({
|
|
|
1163
1206
|
"hierarchy": "org.read",
|
|
1164
1207
|
"board.ready": "board.read",
|
|
1165
1208
|
"board.context": "board.read",
|
|
1209
|
+
// the seat's OWN work across every board (2026-09, front-door session §3.5):
|
|
1210
|
+
// every item where the caller is assignee or reviewer. `board.ready` is the
|
|
1211
|
+
// unassigned frontier and cannot answer "what is mine?"; the daemon caches
|
|
1212
|
+
// this for the main session's primer and the session works it proactively.
|
|
1213
|
+
"board.mine": "board.read",
|
|
1166
1214
|
"ops": "org.read",
|
|
1167
1215
|
// phase 3 reads
|
|
1168
1216
|
"approval.wait": "approval.request", // bounded long-poll for an approval outcome
|
|
@@ -1213,6 +1261,10 @@ export const DIRECTIVES = Object.freeze([
|
|
|
1213
1261
|
"pause_schedules", // stop INITIATING (Loop B + Loop C); keep answering when addressed
|
|
1214
1262
|
"plan_stale", // the seat's mandate moved — refetch `mandate.get` and recompile
|
|
1215
1263
|
"revoke_scope", // a scope was withdrawn; drop it locally and re-probe capabilities
|
|
1264
|
+
// --- front-door session (2026-09) ---
|
|
1265
|
+
"restart", // a human pressed "Restart" on the fleet surface: exit the main
|
|
1266
|
+
// session cleanly so the supervisor relaunches it on current code.
|
|
1267
|
+
// The beat delivers it once; alerts close only on a later clean beat.
|
|
1216
1268
|
]);
|
|
1217
1269
|
|
|
1218
1270
|
// ---------------------------------------------------------------------------
|
|
@@ -205,7 +205,18 @@ test("protocol: frozen family + method counts (additive evolution guard)", () =>
|
|
|
205
205
|
// to `files.*`: a card carries a fileId POINTER and the drive keeps its own
|
|
206
206
|
// seat-scoped read surface → 483. Then +1: messaging.electResponder (the
|
|
207
207
|
// fleet's server-side respond-election) → 484.
|
|
208
|
-
|
|
208
|
+
// The 2026-08 architecture live-board pass adds 3, closing the half of the
|
|
209
|
+
// `architecture` family that was never on the wire: the three methods above
|
|
210
|
+
// review a Genesis DRAFT, while hq's /org/architecture is a CRUD surface over
|
|
211
|
+
// the COMMITTED company and no agent could see it, let alone ask it to change.
|
|
212
|
+
// architecture.board (the read that hands back LIVE ROW IDS beside the
|
|
213
|
+
// positional `P1`/`S3` labels the board renders — an agent has no browser to
|
|
214
|
+
// hold the derived-id -> row-id map, so without it every id it could propose
|
|
215
|
+
// on would be a guess), architecture.propose (the agent's end of hq's
|
|
216
|
+
// "agents propose, a human with the floor approves" rule) and
|
|
217
|
+
// architecture.proposals (the queue that says what became of the ask). NOT
|
|
218
|
+
// approve/reject: those are session acts in hq by design → 487.
|
|
219
|
+
assert.equal(Object.keys(METHODS).length, 487, "method count");
|
|
209
220
|
});
|
|
210
221
|
|
|
211
222
|
test("protocol SP3: messaging + calling families/methods/scopes", async () => {
|
package/lib/org/registry.mjs
CHANGED
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
|
|
17
17
|
"use strict";
|
|
18
18
|
|
|
19
|
-
import { existsSync, readFileSync,
|
|
19
|
+
import { existsSync, readFileSync, mkdirSync, readdirSync } from "node:fs";
|
|
20
|
+
import { writeJsonAtomic } from "../fs-atomic.mjs";
|
|
20
21
|
import { join } from "node:path";
|
|
21
22
|
|
|
22
23
|
const ORG_REL = "state/org";
|
|
@@ -151,7 +152,7 @@ function orgDir(agentRoot) { return join(agentRoot, ORG_REL); }
|
|
|
151
152
|
export function writeSelfEntry(agentRoot, entry) {
|
|
152
153
|
try {
|
|
153
154
|
mkdirSync(orgDir(agentRoot), { recursive: true });
|
|
154
|
-
|
|
155
|
+
writeJsonAtomic(join(orgDir(agentRoot), "self.json"), entry);
|
|
155
156
|
return true;
|
|
156
157
|
} catch { return false; }
|
|
157
158
|
}
|
package/lib/org/tool-surface.mjs
CHANGED
|
@@ -96,7 +96,15 @@ import {
|
|
|
96
96
|
read,
|
|
97
97
|
approvalWait,
|
|
98
98
|
fetchSelfProfile,
|
|
99
|
+
listMine,
|
|
99
100
|
} from "./client.mjs";
|
|
101
|
+
// The work ladder hq's board.track walks (accepted → working → blocked/review
|
|
102
|
+
// → done/failed) — one vocabulary, owned by the ledger client.
|
|
103
|
+
import { WORK_STAGES } from "./work-ledger.mjs";
|
|
104
|
+
// Front-door session status (2026-09): the main session's heartbeat, its peer
|
|
105
|
+
// sessions, open cadence handoffs and the cached board-mine count, read from
|
|
106
|
+
// local state so the tool answers with no token and no network.
|
|
107
|
+
import { readSessionStatus, statusLine } from "../session/status-summary.mjs";
|
|
100
108
|
// The hq vocabularies the model must pick from (an out-of-vocabulary enum value
|
|
101
109
|
// is a BAD_REQUEST). Sourced from the wire contract so the tool table and the
|
|
102
110
|
// normaliser can never disagree about what hq accepts.
|
|
@@ -809,6 +817,86 @@ export const ORG_TOOLS = Object.freeze([
|
|
|
809
817
|
access: "write",
|
|
810
818
|
binding: { kind: "rpc", method: "board.track" },
|
|
811
819
|
},
|
|
820
|
+
// ── front-door session (2026-09): my work across boards, the inbound→board
|
|
821
|
+
// seam, and the main session's liveness ─────────────────────────────────
|
|
822
|
+
{
|
|
823
|
+
name: "board_mine",
|
|
824
|
+
title: "My work across all boards",
|
|
825
|
+
description:
|
|
826
|
+
"Every board item assigned to you or awaiting your review, across every board and " +
|
|
827
|
+
"workstream (GET /v1/board.mine) — the list `board_ready` cannot show, because that feed " +
|
|
828
|
+
"is unassigned work only. Each item: {itemId, title, boardId, boardName, channelId, " +
|
|
829
|
+
"workstreamId, col, stage, priority, dueAt, updatedAt, url}. Use it when idle to pick the " +
|
|
830
|
+
"highest-priority item you can move, then `board_claim` it; use it to answer \"what is on " +
|
|
831
|
+
"your plate?\". An org server that does not serve this read yet returns an empty list, " +
|
|
832
|
+
"not an error.",
|
|
833
|
+
input_schema: S({}),
|
|
834
|
+
access: "read",
|
|
835
|
+
binding: { kind: "local" },
|
|
836
|
+
},
|
|
837
|
+
{
|
|
838
|
+
name: "board_track",
|
|
839
|
+
title: "File or advance an ask on its board",
|
|
840
|
+
description:
|
|
841
|
+
"Record where an ask stands on the board that owns it (board.track) — the server derives " +
|
|
842
|
+
"the board from the message identity (a DM's conversation board or the space's board), " +
|
|
843
|
+
"dedupes on (channelId, messageId) and ladders the column, so a repeated call is free. " +
|
|
844
|
+
"This is the sanctioned way to put an inbound ask onto a board: when a request needs more " +
|
|
845
|
+
"than one reply's worth of work, acknowledge it in-channel in the SAME turn and call this " +
|
|
846
|
+
"with stage 'accepted' (title = what you took on, why = the one-line reason it is more " +
|
|
847
|
+
"than a reply). Advance with 'working', 'blocked' (say what you need; `notify` whoever " +
|
|
848
|
+
"can unstick it), 'review' (finished, wants a human's eyes) and close with 'done' or " +
|
|
849
|
+
"'failed'. `work_track` is the mid-work subset of this verb.",
|
|
850
|
+
input_schema: S(
|
|
851
|
+
{
|
|
852
|
+
channelId: str("The channel the ask arrived on."),
|
|
853
|
+
messageId: str("The id of the message that asked. Identifies the board row."),
|
|
854
|
+
stage: {
|
|
855
|
+
type: "string",
|
|
856
|
+
enum: WORK_STAGES,
|
|
857
|
+
description:
|
|
858
|
+
"accepted = taken on (opens the row); working = started; blocked = stuck and needs " +
|
|
859
|
+
"someone; review = finished, needs a human's eyes; done / failed = closed.",
|
|
860
|
+
},
|
|
861
|
+
title: str("Row title when the row is opened (optional; the server defaults it from the ask)."),
|
|
862
|
+
why: str("One line on why this became board work rather than a reply (optional)."),
|
|
863
|
+
note: str("What changed, in a sentence or two, for the person waiting (optional)."),
|
|
864
|
+
priority: {
|
|
865
|
+
type: "string",
|
|
866
|
+
enum: TASK_PRIORITIES,
|
|
867
|
+
description: "Priority P0 (highest) .. P4, read only when the row is created (optional).",
|
|
868
|
+
},
|
|
869
|
+
notify: {
|
|
870
|
+
type: "array",
|
|
871
|
+
items: { type: "string" },
|
|
872
|
+
description: "Member ids to @-tag on this step — only people who must act or know.",
|
|
873
|
+
},
|
|
874
|
+
attachments: {
|
|
875
|
+
type: "array",
|
|
876
|
+
description: "Files this step produced: [{name, mimeType?, sizeBytes?, dataUrl}].",
|
|
877
|
+
items: { type: "object" },
|
|
878
|
+
},
|
|
879
|
+
},
|
|
880
|
+
["channelId", "messageId", "stage"],
|
|
881
|
+
),
|
|
882
|
+
access: "write",
|
|
883
|
+
binding: { kind: "rpc", method: "board.track" },
|
|
884
|
+
},
|
|
885
|
+
{
|
|
886
|
+
name: "session_status",
|
|
887
|
+
title: "Main session status",
|
|
888
|
+
description:
|
|
889
|
+
"Whether this agent's main (front-door) session is live on this machine, its name, the " +
|
|
890
|
+
"peer sessions it has spawned, how many cadence handoffs are waiting for it, and how many " +
|
|
891
|
+
"board items the daemon last cached for it — read from local state, offline, no token " +
|
|
892
|
+
"needed. `live:false` with state 'stale' means the session exists but stopped beating; " +
|
|
893
|
+
"'absent' means it is not running (the daemon is then answering inbound itself). Use it " +
|
|
894
|
+
"before addressing the main session with SendMessage, or to answer \"is the front door " +
|
|
895
|
+
"up?\". Never quote session names to a human — they are internal.",
|
|
896
|
+
input_schema: S({}),
|
|
897
|
+
access: "read",
|
|
898
|
+
binding: { kind: "local" },
|
|
899
|
+
},
|
|
812
900
|
// ── decisions / approvals ────────────────────────────────────────────────
|
|
813
901
|
{
|
|
814
902
|
name: "decision_list",
|
|
@@ -2977,6 +3065,9 @@ export function orgToolDef(name) {
|
|
|
2977
3065
|
* the first-class tools.
|
|
2978
3066
|
* @type {Set<string>}
|
|
2979
3067
|
*/
|
|
3068
|
+
/** board_track input fields forwarded to hq (the schema's optional keys). */
|
|
3069
|
+
export const BOARD_TRACK_PASSTHROUGH = Object.freeze(["title", "why", "note", "priority", "notify", "attachments"]);
|
|
3070
|
+
|
|
2980
3071
|
export const OUTBOUND_METHODS = Object.freeze(
|
|
2981
3072
|
new Set(
|
|
2982
3073
|
ORG_TOOLS.filter((t) => t.outbound && t.binding && t.binding.kind === "rpc").map(
|
|
@@ -3263,6 +3354,10 @@ async function executeOrgToolInner(name, input = {}, o = {}) {
|
|
|
3263
3354
|
|
|
3264
3355
|
// Offline-capable local tools first.
|
|
3265
3356
|
if (name === "org_describe") return describeProtocol(input);
|
|
3357
|
+
if (name === "session_status") {
|
|
3358
|
+
const status = readSessionStatus(cfg.agentRoot, { now: Date.now() });
|
|
3359
|
+
return okFrame({ ...status, agentRoot: cfg.agentRoot, summary: statusLine(status) });
|
|
3360
|
+
}
|
|
3266
3361
|
|
|
3267
3362
|
// Every network tool needs a token; per §1.4 the server stays up and each
|
|
3268
3363
|
// call returns a clear error frame instead.
|
|
@@ -3421,6 +3516,31 @@ async function executeOrgToolInner(name, input = {}, o = {}) {
|
|
|
3421
3516
|
return readToFrame(await read(withQuery("events", q), callOpts));
|
|
3422
3517
|
}
|
|
3423
3518
|
|
|
3519
|
+
case "board_mine": {
|
|
3520
|
+
// listMine is fail-open by contract: an hq that does not serve the read
|
|
3521
|
+
// yet (404), a transport error or a malformed body all come back as [].
|
|
3522
|
+
const items = await listMine(callOpts);
|
|
3523
|
+
return okFrame({ items, count: items.length });
|
|
3524
|
+
}
|
|
3525
|
+
|
|
3526
|
+
case "board_track": {
|
|
3527
|
+
const i = input || {};
|
|
3528
|
+
const channelId = i.channelId ? String(i.channelId) : "";
|
|
3529
|
+
const messageId = i.messageId ? String(i.messageId) : "";
|
|
3530
|
+
const stage = i.stage ? String(i.stage) : "";
|
|
3531
|
+
if (!channelId || !messageId) return errFrame("BAD_REQUEST", "board_track: channelId and messageId are required");
|
|
3532
|
+
if (!WORK_STAGES.includes(stage)) {
|
|
3533
|
+
return errFrame("BAD_REQUEST", `board_track: stage must be one of ${WORK_STAGES.join("|")} (got ${stage || "(none)"})`);
|
|
3534
|
+
}
|
|
3535
|
+
// `service` is pinned (this seam is Cohort-only) and only the schema's
|
|
3536
|
+
// passthrough fields travel: model input never reaches hq verbatim.
|
|
3537
|
+
const params = { service: "cohort", channelId, messageId, stage };
|
|
3538
|
+
for (const k of BOARD_TRACK_PASSTHROUGH) if (i[k] !== undefined) params[k] = i[k];
|
|
3539
|
+
// Same ask + same stage = the same call (mirrors lib/org/work-ledger):
|
|
3540
|
+
// a retry after a timeout must not double-post the tag.
|
|
3541
|
+
return call("board.track", params, { ...callOpts, idempotencyKey: `board.track:${channelId}:${messageId}:${stage}` });
|
|
3542
|
+
}
|
|
3543
|
+
|
|
3424
3544
|
case "decision_list": {
|
|
3425
3545
|
const q = input && input.status ? { status: input.status } : null;
|
|
3426
3546
|
return readToFrame(await read(withQuery("decision.list", q), callOpts));
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
"use strict";
|
|
10
10
|
|
|
11
11
|
import { test, before, after } from "node:test";
|
|
12
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
12
15
|
import assert from "node:assert/strict";
|
|
13
16
|
|
|
14
17
|
import {
|
|
@@ -117,7 +120,14 @@ test("table: curated 54 + 5 email + 5 artifact + 77 desk tools, snake_case names
|
|
|
117
120
|
// declaration (lib/org/resource-tools.mjs) rather than transcribed from hq's
|
|
118
121
|
// desk, so it cannot acquire the 239-vs-69 drift the hand-copied desks
|
|
119
122
|
// already carry; resource-tools.test.mjs is the parity that holds it there.
|
|
120
|
-
|
|
123
|
+
// The FRONT-DOOR delta (+3 curated → 57, table 141 → 144; design spec
|
|
124
|
+
// 2026-09-08 §3.5/§3.7): board_mine (the agent's own items across every
|
|
125
|
+
// board — board.ready is unassigned-only, so "my tasks" was unreadable from
|
|
126
|
+
// any plane), board_track (the sanctioned inbound→board seam with the
|
|
127
|
+
// accepted/done ends of the ladder and title/why passthrough) and
|
|
128
|
+
// session_status (the main session's liveness + peers from local state,
|
|
129
|
+
// offline-capable).
|
|
130
|
+
assert.equal(ORG_TOOLS.length, 144, "57 curated + 5 email + 5 artifact + 77 desk");
|
|
121
131
|
assert.equal(ORG_TOOLS.filter((t) => t.desk).length, 77, "exactly seventy-seven desk tools");
|
|
122
132
|
assert.equal(ORG_TOOLS.filter((t) => t.email).length, 5, "exactly five email tools");
|
|
123
133
|
assert.equal(ORG_TOOLS.filter((t) => t.artifact).length, 5, "exactly five artifact tools");
|
|
@@ -168,19 +178,19 @@ test("OUTBOUND_METHODS is DERIVED from outbound:true tools (messaging/email/mail
|
|
|
168
178
|
test("email gating: family present in the vendored protocol → tools active; override excludes", () => {
|
|
169
179
|
// The email family landed in the vendored protocol (sync-protocol phase 1).
|
|
170
180
|
assert.equal(emailFamilyAvailable(), true, "vendored protocol carries email.send");
|
|
171
|
-
assert.equal(getOrgTools().length,
|
|
181
|
+
assert.equal(getOrgTools().length, 144);
|
|
172
182
|
const without = getOrgTools({ emailAvailable: false });
|
|
173
|
-
assert.equal(without.length,
|
|
183
|
+
assert.equal(without.length, 139);
|
|
174
184
|
assert.ok(!without.some((t) => t.email), "email tools excluded when family absent");
|
|
175
185
|
});
|
|
176
186
|
|
|
177
187
|
test("artifact gating: family present → tools active; override excludes (email precedent)", () => {
|
|
178
188
|
assert.equal(artifactFamilyAvailable(), true, "vendored protocol carries artifact.act");
|
|
179
189
|
const without = getOrgTools({ artifactAvailable: false });
|
|
180
|
-
assert.equal(without.length,
|
|
190
|
+
assert.equal(without.length, 139);
|
|
181
191
|
assert.ok(!without.some((t) => t.artifact), "artifact tools excluded when family absent");
|
|
182
192
|
const neither = getOrgTools({ emailAvailable: false, artifactAvailable: false, desksAvailable: false });
|
|
183
|
-
assert.equal(neither.length,
|
|
193
|
+
assert.equal(neither.length, 57, "all additive families off → the 57 always-on tools");
|
|
184
194
|
});
|
|
185
195
|
|
|
186
196
|
test("isOrgTool / orgToolDef cover the full table; unknown names rejected", () => {
|
|
@@ -1153,3 +1163,106 @@ test("transport failure on a new tool fails open into an INTERNAL frame, never a
|
|
|
1153
1163
|
assert.equal(frame.ok, false);
|
|
1154
1164
|
assert.equal(frame.error.code, "INTERNAL");
|
|
1155
1165
|
});
|
|
1166
|
+
|
|
1167
|
+
// ── WP-M4: front-door tools (board_mine / board_track / session_status) ────
|
|
1168
|
+
|
|
1169
|
+
test("board_mine: read access, GET /v1/board.mine, items normalised, [] when hq lacks the read (fail-open)", async () => {
|
|
1170
|
+
const def = orgToolDef("board_mine");
|
|
1171
|
+
assert.ok(def, "board_mine curated");
|
|
1172
|
+
assert.equal(def.access, "read");
|
|
1173
|
+
assert.equal(def.binding.kind, "local", "local so an hq without board.mine yet degrades to [] instead of a bare 404");
|
|
1174
|
+
const fetchImpl = fakeFetch({ items: [{ itemId: "i1", title: "Deck", col: "doing" }] });
|
|
1175
|
+
const frame = await executeOrgTool("board_mine", {}, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl });
|
|
1176
|
+
assert.equal(frame.ok, true);
|
|
1177
|
+
assert.deepEqual(frame.result.items, [{ itemId: "i1", title: "Deck", col: "doing" }]);
|
|
1178
|
+
assert.equal(frame.result.count, 1);
|
|
1179
|
+
assert.match(fetchImpl.calls[0].url, /\/v1\/board\.mine$/);
|
|
1180
|
+
assert.equal(fetchImpl.calls[0].init.method, "GET");
|
|
1181
|
+
const gone = fakeFetch({ error: { code: "NOT_FOUND", message: "no such read" } }, { status: 404, ok: false });
|
|
1182
|
+
const f2 = await executeOrgTool("board_mine", {}, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl: gone });
|
|
1183
|
+
assert.equal(f2.ok, true, "fail-open: an older hq is an empty list, not an error the model must interpret");
|
|
1184
|
+
assert.deepEqual(f2.result.items, []);
|
|
1185
|
+
// No token → UNAUTHORIZED like every other network tool.
|
|
1186
|
+
const noTok = await executeOrgTool("board_mine", {}, { orgConfig: { org: { cohort: { enabled: true, base: "https://org.example" } } }, agentRoot: "/tmp/none", fetchImpl });
|
|
1187
|
+
assert.equal(noTok.ok, false);
|
|
1188
|
+
assert.equal(noTok.error.code, "UNAUTHORIZED");
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
test("board_track: write access on board.track; title/why/stage pass through; whole ladder incl. accepted|done; bad stage rejected offline", async () => {
|
|
1192
|
+
const def = orgToolDef("board_track");
|
|
1193
|
+
assert.ok(def, "board_track curated");
|
|
1194
|
+
assert.equal(def.access, "write");
|
|
1195
|
+
assert.equal(def.binding.method, "board.track");
|
|
1196
|
+
assert.deepEqual(def.input_schema.properties.stage.enum, ["accepted", "working", "blocked", "review", "done", "failed"]);
|
|
1197
|
+
assert.ok(def.input_schema.properties.title, "title passthrough");
|
|
1198
|
+
assert.ok(def.input_schema.properties.why, "why passthrough");
|
|
1199
|
+
const fetchImpl = fakeFetch({ ok: true, result: { tracked: true, taskId: "t1", col: "todo", created: true } });
|
|
1200
|
+
const frame = await executeOrgTool(
|
|
1201
|
+
"board_track",
|
|
1202
|
+
{ channelId: "c1", messageId: "m1", stage: "accepted", title: "Build the deck", why: "asked in DM", note: "on it" },
|
|
1203
|
+
{ orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl },
|
|
1204
|
+
);
|
|
1205
|
+
assert.equal(frame.ok, true);
|
|
1206
|
+
assert.equal(frame.result.taskId, "t1");
|
|
1207
|
+
assert.match(fetchImpl.calls[0].url, /\/v1\/board\.track$/);
|
|
1208
|
+
const body = JSON.parse(fetchImpl.calls[0].init.body);
|
|
1209
|
+
assert.equal(body.stage, "accepted");
|
|
1210
|
+
assert.equal(body.title, "Build the deck");
|
|
1211
|
+
assert.equal(body.why, "asked in DM");
|
|
1212
|
+
assert.equal(body.service, "cohort", "service defaults to cohort");
|
|
1213
|
+
assert.equal(body.channelId, "c1");
|
|
1214
|
+
assert.equal(body.messageId, "m1");
|
|
1215
|
+
assert.equal(fetchImpl.calls[0].init.headers["x-idempotency-key"], "board.track:c1:m1:accepted", "stable per (ask, stage)");
|
|
1216
|
+
const bad = fakeFetch();
|
|
1217
|
+
const rej = await executeOrgTool("board_track", { channelId: "c1", messageId: "m1", stage: "later" }, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl: bad });
|
|
1218
|
+
assert.equal(rej.ok, false);
|
|
1219
|
+
assert.equal(rej.error.code, "BAD_REQUEST");
|
|
1220
|
+
assert.equal(bad.calls.length, 0, "validated before any network");
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1223
|
+
test("session_status: offline local read of state/session + state/org/board-mine; works with no token", async () => {
|
|
1224
|
+
const def = orgToolDef("session_status");
|
|
1225
|
+
assert.ok(def, "session_status curated");
|
|
1226
|
+
assert.equal(def.access, "read");
|
|
1227
|
+
assert.equal(def.binding.kind, "local");
|
|
1228
|
+
const root = mkdtempSync(join(tmpdir(), "ts-session-"));
|
|
1229
|
+
try {
|
|
1230
|
+
mkdirSync(join(root, "state", "session", "handoffs"), { recursive: true });
|
|
1231
|
+
mkdirSync(join(root, "state", "org"), { recursive: true });
|
|
1232
|
+
writeFileSync(join(root, "state", "session", "heartbeat.json"), JSON.stringify({ pid: 1, name: "alex-main", sessionId: "s1", ts: Date.now() }));
|
|
1233
|
+
writeFileSync(join(root, "state", "session", "peers.json"), JSON.stringify([{ name: "alex-deck", purpose: "board deck" }]));
|
|
1234
|
+
writeFileSync(join(root, "state", "session", "handoffs", "t1.json"), "{}");
|
|
1235
|
+
writeFileSync(join(root, "state", "org", "board-mine.json"), JSON.stringify({ items: [{ itemId: "i1" }, { itemId: "i2" }], fetchedAt: new Date().toISOString() }));
|
|
1236
|
+
const fetchImpl = fakeFetch();
|
|
1237
|
+
const frame = await executeOrgTool("session_status", {}, { orgConfig: { org: { cohort: { enabled: true, base: "https://org.example" } } }, agentRoot: root, fetchImpl });
|
|
1238
|
+
assert.equal(frame.ok, true, "no token is fine — this never touches the network");
|
|
1239
|
+
assert.equal(fetchImpl.calls.length, 0);
|
|
1240
|
+
assert.equal(frame.result.live, true);
|
|
1241
|
+
assert.equal(frame.result.name, "alex-main");
|
|
1242
|
+
assert.equal(frame.result.peers[0].name, "alex-deck");
|
|
1243
|
+
assert.equal(frame.result.handoffsOpen, 1);
|
|
1244
|
+
assert.equal(frame.result.boardMineCount, 2);
|
|
1245
|
+
assert.equal(typeof frame.result.summary, "string");
|
|
1246
|
+
// An empty root is "absent", not an error.
|
|
1247
|
+
const empty = await executeOrgTool("session_status", {}, { orgConfig: CFG, agentRoot: join(root, "nope"), fetchImpl });
|
|
1248
|
+
assert.equal(empty.ok, true);
|
|
1249
|
+
assert.equal(empty.result.state, "absent");
|
|
1250
|
+
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
test("board_track: the model cannot override service or smuggle extra keys — only the schema's passthrough fields reach hq", async () => {
|
|
1254
|
+
const fetchImpl = fakeFetch({ ok: true, result: { tracked: true, taskId: "t2", col: "doing" } });
|
|
1255
|
+
const frame = await executeOrgTool(
|
|
1256
|
+
"board_track",
|
|
1257
|
+
{ channelId: "c1", messageId: "m1", stage: "working", service: "slack", junk: "x", note: "halfway", priority: "P1", notify: ["u1"] },
|
|
1258
|
+
{ orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl },
|
|
1259
|
+
);
|
|
1260
|
+
assert.equal(frame.ok, true);
|
|
1261
|
+
const body = JSON.parse(fetchImpl.calls[0].init.body);
|
|
1262
|
+
assert.equal(body.service, "cohort", "service is pinned, not model-supplied");
|
|
1263
|
+
assert.equal(body.junk, undefined, "unknown keys are dropped");
|
|
1264
|
+
assert.equal(body.note, "halfway");
|
|
1265
|
+
assert.equal(body.priority, "P1");
|
|
1266
|
+
assert.deepEqual(body.notify, ["u1"]);
|
|
1267
|
+
assert.deepEqual(Object.keys(body).sort(), ["channelId", "messageId", "note", "notify", "priority", "service", "stage"]);
|
|
1268
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/prompts/parallelism.mjs — the fleet-wide parallelism directive (WP-M7).
|
|
3
|
+
*
|
|
4
|
+
* THE GAP this closes (owner directive 2026-09-09): a session handed a plan with
|
|
5
|
+
* six independent tasks ran them one at a time, because nothing in its prompt
|
|
6
|
+
* ever said it was allowed not to. The directive below is the standing
|
|
7
|
+
* permission — plus the ONE safety rule that makes parallel dispatch safe on a
|
|
8
|
+
* shared checkout (no two agents editing the same file).
|
|
9
|
+
*
|
|
10
|
+
* It is a CONSTANT, not a template: the same bytes reach `maestro session spawn`
|
|
11
|
+
* (peer sessions) and `scripts/daemon/prompt-builder.mjs` (escalate/guarded
|
|
12
|
+
* sub-sessions), so the wording can be audited in one place and a test can pin
|
|
13
|
+
* it byte-for-byte.
|
|
14
|
+
*
|
|
15
|
+
* {@link withParallelism} is IDEMPOTENT. Two prompt seams compose (a daemon
|
|
16
|
+
* prompt handed to `session spawn`), and a directive repeated twice reads as
|
|
17
|
+
* emphasis to a model, which is exactly how "maximum safe parallelism" turns
|
|
18
|
+
* into "ignore the file-scope rule" — so a prompt that already carries the text
|
|
19
|
+
* is returned unchanged.
|
|
20
|
+
*
|
|
21
|
+
* Pure: no clock, no env, no I/O. Node builtins only. ESM.
|
|
22
|
+
*
|
|
23
|
+
* @module lib/prompts/parallelism
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
"use strict";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The directive, verbatim (design spec 2026-09-08 §WP-M7 mechanic 5). Do not
|
|
30
|
+
* reword without changing the spec — `parallelism.test.mjs` pins these bytes.
|
|
31
|
+
* @type {string}
|
|
32
|
+
*/
|
|
33
|
+
export const PARALLELISM_DIRECTIVE = `You are allowed to run multiple sub-agents concurrently.
|
|
34
|
+
|
|
35
|
+
When the plan contains independent tasks, dispatch them together in a single parallel batch rather than waiting for each task to finish before starting the next.
|
|
36
|
+
|
|
37
|
+
Only run tasks sequentially when one task genuinely depends on the output of another.
|
|
38
|
+
|
|
39
|
+
Before dispatching any batch, check the planned file scope for each task and ensure that no two agents are assigned to edit the same file. If tasks would overlap on a file, sequence those tasks or redefine their scope to eliminate the conflict.
|
|
40
|
+
|
|
41
|
+
Default to maximum safe parallelism.`;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Prepend {@link PARALLELISM_DIRECTIVE} to a prompt, exactly once.
|
|
45
|
+
*
|
|
46
|
+
* A prompt that already contains the directive anywhere in its body is returned
|
|
47
|
+
* unchanged (idempotent — see the module docstring). A non-string or empty
|
|
48
|
+
* prompt yields the directive alone, so a caller that lost its prompt still
|
|
49
|
+
* sends something coherent rather than `undefined`.
|
|
50
|
+
*
|
|
51
|
+
* @param {string} prompt the prompt to lead
|
|
52
|
+
* @returns {string}
|
|
53
|
+
*/
|
|
54
|
+
export function withParallelism(prompt) {
|
|
55
|
+
const body = typeof prompt === "string" ? prompt : "";
|
|
56
|
+
if (body.includes(PARALLELISM_DIRECTIVE)) return body;
|
|
57
|
+
if (!body.trim()) return PARALLELISM_DIRECTIVE;
|
|
58
|
+
return `${PARALLELISM_DIRECTIVE}\n\n${body}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* How many times the directive appears in a string. The seam a test (or a
|
|
63
|
+
* caller composing two prompts) uses to assert "exactly once".
|
|
64
|
+
* @param {string} text
|
|
65
|
+
* @returns {number}
|
|
66
|
+
*/
|
|
67
|
+
export function countParallelism(text) {
|
|
68
|
+
const body = typeof text === "string" ? text : "";
|
|
69
|
+
if (!body) return 0;
|
|
70
|
+
let n = 0;
|
|
71
|
+
let i = body.indexOf(PARALLELISM_DIRECTIVE);
|
|
72
|
+
while (i !== -1) {
|
|
73
|
+
n += 1;
|
|
74
|
+
i = body.indexOf(PARALLELISM_DIRECTIVE, i + PARALLELISM_DIRECTIVE.length);
|
|
75
|
+
}
|
|
76
|
+
return n;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export default { PARALLELISM_DIRECTIVE, withParallelism, countParallelism };
|