claude-tempo 0.28.0 → 0.29.1

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/CLAUDE.md CHANGED
@@ -91,6 +91,7 @@ src/
91
91
  │ ├── pause.ts / play.ts / shutdown.ts / restore.ts
92
92
  │ ├── hosts.ts / set-ensemble-description.ts
93
93
  │ ├── save-state.ts / fetch-state.ts / clear-state.ts
94
+ │ ├── coat-check-put.ts / coat-check-get.ts / coat-check-list.ts / coat-check-evict.ts
94
95
  │ └── helpers.ts # Zod/MCP tool registration wrapper
95
96
  ├── tui/
96
97
  │ ├── App.tsx / store.ts / commands.ts # TUI root, state, slash commands
@@ -170,6 +171,7 @@ daemon worker notes, `npx ts-node` dev runner).
170
171
  - **Claude Code headless adapter** (`agent: 'claude-code-headless'`, #520): Headless adapter that drives sessions via the official `claude` CLI as a per-turn `claude -p --output-format stream-json` subprocess. The whole point: turns bill against the host's existing Claude Code subscription extra-usage credits (Pro / Max plans) rather than a Console workspace API key — the only ToS-clean way for a third-party tool to tap that pool. Requires the `claude` binary on PATH AND a logged-in Claude Code session (`claude auth login`); recruit pre-flight rejects with an actionable error otherwise. Tool surface is the union of full Claude Code built-ins (Bash / Read / Write / Edit / Glob / Grep / WebSearch / WebFetch) and the claude-tempo MCP surface — registered via inline `--mcp-config` so `claude` spawns `dist/server.js` as its own MCP child (no in-process bridge). Recruit knobs: `permissionMode` (default `'acceptEdits'`) or `dangerouslySkipPermissions: true` (mutually exclusive). Sessions resume across restart via the existing `sessionId` metadata field — the same UUID is shared with the interactive `claude-code` adapter (per-cwd JSONL is per-cwd, not per-adapter). See `src/adapters/claude-code-headless/` and `examples/ensembles/tempo-headless-jam.yaml`.
171
172
  - **Mock adapter** (`agent: 'mock'`, dev mode only): Four modes: `echo` (echoes input), `scripted` (replays YAML scenario rules), `silent` (drains messages without replying — heartbeat-stale validation), `chaos` (probabilistic fail/crash injection via seeded PRNG). Only registered when `isDevMode()` is true; stripped from the npm tarball by `prepack`. See `src/adapters/mock/`.
172
173
  - **Saveable state** (#334, ADR 0011): Per-player curated state slots — the player itself decides what context survives a restart. Three MCP tools: `save_state` (owner-only write, max 4 slots × 32 KiB), `fetch_state` (read self or peer; audit identity recorded on each entry's `savedBy`), `clear_state` (owner-only). `restart` accepts `loadFromState: true | 'someKey'` to seed the new session from a saved-state slot instead of (or, with `transcript: 'replay'`, alongside) transcript replay. Saved-state delivery uses `from: 'self-restart'` as a stable system identity. Empty-slot fallback: graceful — falls through to transcript replay with a log line. See [docs/design/334-player-saveable-state.md](docs/design/334-player-saveable-state.md).
174
+ - **Coat-check** (#318, ADR 0008): Per-ensemble transient content store on Maestro state. Solves the 100 KB cue body cap — stash a large artifact with `coat_check_put` (returns a ticket id) and attach the ticket to a `cue` via `attachmentTicket`; the recipient calls `coat_check_get` to pull the full body. Four MCP tools: `coat_check_put` (any player; max 32 KiB per entry, 20 slots per ensemble, TTL 7d default), `coat_check_get` (any player; bumps fetch-audit counters), `coat_check_list` (read-only survey; headers only, content omitted), `coat_check_evict` (owner or conductor). Saturation rejects with `CoatCheckSlotsFull` (no LRU eviction). See `src/tools/coat-check-*.ts` and [docs/adr/0008-coat-check-pattern.md](docs/adr/0008-coat-check-pattern.md).
173
175
  - **Lineup examples**: Six pre-built ensemble YAML files in `examples/ensembles/` — `tempo-big-band`, `tempo-dev-team`, `tempo-review-squad`, `tempo-jam-session`, `tempo-mock-jam` (dev-mode all-mock ensemble), `tempo-headless-jam` (#520 — all-`claude-code-headless` subscription-billed ensemble). Load with `claude-tempo up --lineup <name>` or the `load_lineup` tool.
174
176
  - **GitHub App identity** (`claude-tempo[bot]`): When a player writes to GitHub — issue comments, PR creation/merge, commits, labels, check runs — **use `./scripts/ensemble-gh`** instead of `gh`. The wrapper mints a short-lived installation token so the action is attributed to `claude-tempo[bot]`, not to the human maintainer, making the AI authorship visible. Plain `gh` is still correct for read-only local dev (`gh pr view`, `gh repo clone`, `gh auth status`). Every bot-authored comment/PR body must include the AI attribution footer documented in [docs/github-app.md](docs/github-app.md).
175
177
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "claude-tempo-dashboard",
3
3
  "private": true,
4
- "version": "0.28.0",
4
+ "version": "0.29.1",
5
5
  "type": "module",
6
6
  "description": "Web dashboard for claude-tempo. Bundled into the npm package; served by the daemon at /dashboard/*.",
7
7
  "scripts": {
@@ -15,6 +15,14 @@ export interface DeliverCueInput {
15
15
  * later TUI grouping. `undefined` for non-broadcast direct cues.
16
16
  */
17
17
  broadcastId?: string;
18
+ /**
19
+ * #318: Coat-check ticket referencing content stashed via
20
+ * `coat_check_put`. Threaded through to the target's `receiveMessage`
21
+ * signal so the stored `Message` carries it and the recipient can
22
+ * pull the body via `coat_check_get`. `undefined` for cues without an
23
+ * attachment.
24
+ */
25
+ attachmentTicket?: string;
18
26
  }
19
27
  export interface DeliverReportInput {
20
28
  ensemble: string;
@@ -140,19 +140,20 @@ function classifyAndRethrow(err, contextPrefix) {
140
140
  function createOutboxActivities(client, config) {
141
141
  return {
142
142
  async deliverCue(input) {
143
- const { ensemble, fromPlayerId, targetPlayerId, message, broadcastId } = input;
143
+ const { ensemble, fromPlayerId, targetPlayerId, message, broadcastId, attachmentTicket } = input;
144
144
  try {
145
145
  const handle = await (0, resolve_1.resolveSession)(client, ensemble, targetPlayerId);
146
146
  if (!handle) {
147
147
  throw activity_1.ApplicationFailure.nonRetryable(`No active session found for "${targetPlayerId}"`);
148
148
  }
149
- // #357: thread broadcastId (when present) onto the receiver's
150
- // `receiveMessage` signal payload. Additive optional field —
151
- // direct cues omit it.
149
+ // #357 + #318: thread broadcastId / attachmentTicket onto the
150
+ // receiver's `receiveMessage` signal payload. Both fields are
151
+ // additive optionals — direct cues omit one or both.
152
152
  await handle.signal('receiveMessage', {
153
153
  from: fromPlayerId,
154
154
  text: message,
155
155
  ...(broadcastId !== undefined ? { broadcastId } : {}),
156
+ ...(attachmentTicket !== undefined ? { attachmentTicket } : {}),
156
157
  });
157
158
  return { success: true };
158
159
  }
@@ -40,6 +40,11 @@ const set_ensemble_description_1 = require("./tools/set-ensemble-description");
40
40
  const save_state_1 = require("./tools/save-state");
41
41
  const fetch_state_1 = require("./tools/fetch-state");
42
42
  const clear_state_1 = require("./tools/clear-state");
43
+ // #318 — ensemble-shared coat-check (put / get / list / evict).
44
+ const coat_check_put_1 = require("./tools/coat-check-put");
45
+ const coat_check_get_1 = require("./tools/coat-check-get");
46
+ const coat_check_list_1 = require("./tools/coat-check-list");
47
+ const coat_check_evict_1 = require("./tools/coat-check-evict");
43
48
  /**
44
49
  * Register every tempo MCP tool onto `server`. Single source of truth — the
45
50
  * stdio MCP server (`src/server.ts`) and the in-process MCP server in the
@@ -83,6 +88,13 @@ function registerAllTempoTools(server, opts) {
83
88
  (0, save_state_1.registerSaveStateTool)(server, handle, getPlayerId);
84
89
  (0, fetch_state_1.registerFetchStateTool)(server, client, config, handle, getPlayerId);
85
90
  (0, clear_state_1.registerClearStateTool)(server, handle);
91
+ // #318 — ensemble-shared coat-check (put/get/list/evict). Any player can put;
92
+ // any player can get/list; owner-or-conductor can evict. Audit identity is
93
+ // set at the tool layer via getPlayerId() — no playerId arg on any schema.
94
+ (0, coat_check_put_1.registerCoatCheckPutTool)(server, client, config, getPlayerId);
95
+ (0, coat_check_get_1.registerCoatCheckGetTool)(server, client, config, getPlayerId);
96
+ (0, coat_check_list_1.registerCoatCheckListTool)(server, client, config);
97
+ (0, coat_check_evict_1.registerCoatCheckEvictTool)(server, client, config, getPlayerId);
86
98
  if (isConductor) {
87
99
  (0, quality_gate_1.registerQualityGateTool)(server, handle, getPlayerId);
88
100
  (0, evaluate_gate_1.registerEvaluateGateTool)(server, handle, getPlayerId);
@@ -0,0 +1,4 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { Client } from '@temporalio/client';
3
+ import { Config } from '../config';
4
+ export declare function registerCoatCheckEvictTool(server: McpServer, client: Client, config: Config, getPlayerId: () => string): void;
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerCoatCheckEvictTool = registerCoatCheckEvictTool;
4
+ /**
5
+ * `coat_check_evict` — remove a coat-check entry (#318, ADR 0008) before
6
+ * its TTL expires. Owner-or-conductor only: the workflow validator rejects
7
+ * mismatched `evictedBy` with `CoatCheckEvictPermissionDenied`. Returns
8
+ * `evicted: false` when the ticket was already missing / expired before
9
+ * the call landed.
10
+ *
11
+ * Audit identity (`evictedBy`) is set by the tool layer from
12
+ * `getPlayerId()` — there is NO `evictedBy` arg on the MCP schema.
13
+ */
14
+ const zod_1 = require("zod");
15
+ const config_1 = require("../config");
16
+ const maestro_signals_1 = require("../workflows/maestro-signals");
17
+ const helpers_1 = require("./helpers");
18
+ const validation_1 = require("../utils/validation");
19
+ function registerCoatCheckEvictTool(server, client, config, getPlayerId) {
20
+ (0, helpers_1.defineTool)(server, 'coat_check_evict', `Evict a coat-check entry (#318) before its TTL expires. Owner-or-conductor only — non-owners (and non-conductors) get a permission error.
21
+
22
+ Use to free a slot when this ensemble is at the 20-entry cap and you want to make room. \`evicted: false\` means the ticket was already gone (TTL-expired or evicted by someone else).`, {
23
+ ticket: zod_1.z.string().regex(validation_1.COAT_CHECK_TICKET_REGEX).max(validation_1.COAT_CHECK_TICKET_MAX).describe(`The ticket id returned by an earlier \`coat_check_put\` (≤${validation_1.COAT_CHECK_TICKET_MAX} chars).`),
24
+ }, async (args) => {
25
+ const { ticket } = args;
26
+ const evictedBy = getPlayerId();
27
+ try {
28
+ const handle = client.workflow.getHandle((0, config_1.maestroWorkflowId)(config.ensemble));
29
+ const result = await handle.executeUpdate(maestro_signals_1.coatCheckEvictUpdate, {
30
+ args: [{ ticket, evictedBy }],
31
+ });
32
+ if (!result.evicted) {
33
+ return (0, helpers_1.ok)(`Ticket **${ticket}** was already gone (no-op).`);
34
+ }
35
+ return (0, helpers_1.ok)(`Evicted ticket **${ticket}**.`);
36
+ }
37
+ catch (err) {
38
+ // Surfaces `CoatCheckEvictPermissionDenied` ApplicationFailure with
39
+ // owner/conductor diagnostic from the workflow validator.
40
+ return (0, helpers_1.fail)(`Failed to evict ticket: ${(0, helpers_1.formatError)(err)}`);
41
+ }
42
+ });
43
+ }
@@ -0,0 +1,4 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { Client } from '@temporalio/client';
3
+ import { Config } from '../config';
4
+ export declare function registerCoatCheckGetTool(server: McpServer, client: Client, config: Config, getPlayerId: () => string): void;
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerCoatCheckGetTool = registerCoatCheckGetTool;
4
+ /**
5
+ * `coat_check_get` — redeem a coat-check ticket and pull the stashed content
6
+ * (#318, ADR 0008). Returns the entry's summary, content body, and audit
7
+ * fields, or `null` when the ticket is missing / expired / evicted (no
8
+ * error-class proliferation for the common "ticket already gone" case).
9
+ *
10
+ * Audit identity (`fetchedBy`) is set by the tool layer from `getPlayerId()`
11
+ * — there is NO `fetchedBy` arg on the MCP schema. The workflow handler
12
+ * bumps `lastFetchedAt` / `lastFetchedBy` / `fetchCount` on the entry so
13
+ * the putter can later see whether anyone has redeemed.
14
+ *
15
+ * Implemented as a workflow Update (not Query) because the fetch-audit
16
+ * counters mutate state — Queries cannot mutate.
17
+ */
18
+ const zod_1 = require("zod");
19
+ const config_1 = require("../config");
20
+ const maestro_signals_1 = require("../workflows/maestro-signals");
21
+ const helpers_1 = require("./helpers");
22
+ const validation_1 = require("../utils/validation");
23
+ function registerCoatCheckGetTool(server, client, config, getPlayerId) {
24
+ (0, helpers_1.defineTool)(server, 'coat_check_get', `Redeem a coat-check ticket (#318) and pull the stashed content. Returns the entry's summary, content body, and audit info — or "not found" when the ticket is missing / expired / evicted (no error, just empty).
25
+
26
+ Successful redemptions bump the entry's fetch-audit counters (\`lastFetchedAt\` / \`lastFetchedBy\` / \`fetchCount\`) so the putter can later see whether anyone has redeemed. \`coat_check_list\` won't bump these — only an actual redemption counts.`, {
27
+ ticket: zod_1.z.string().regex(validation_1.COAT_CHECK_TICKET_REGEX).max(validation_1.COAT_CHECK_TICKET_MAX).describe(`The ticket id returned by an earlier \`coat_check_put\` (≤${validation_1.COAT_CHECK_TICKET_MAX} chars).`),
28
+ }, async (args) => {
29
+ const { ticket } = args;
30
+ const fetchedBy = getPlayerId();
31
+ try {
32
+ const handle = client.workflow.getHandle((0, config_1.maestroWorkflowId)(config.ensemble));
33
+ const entry = await handle.executeUpdate(maestro_signals_1.coatCheckGetUpdate, {
34
+ args: [{ ticket, fetchedBy }],
35
+ });
36
+ if (!entry) {
37
+ return (0, helpers_1.ok)(`Ticket **${ticket}** is not found (missing, expired, or evicted).`);
38
+ }
39
+ const lines = [
40
+ `**Ticket ${ticket}** — stashed by ${entry.putBy} at ${entry.putAt}, expires ${entry.expiresAt}`,
41
+ `Summary: ${entry.summary}`,
42
+ ];
43
+ if (entry.contentType)
44
+ lines.push(`Content-Type: ${entry.contentType}`);
45
+ lines.push(`Size: ${entry.size} bytes`);
46
+ lines.push(`Fetches: ${entry.fetchCount}${entry.lastFetchedBy ? ` (last by ${entry.lastFetchedBy} at ${entry.lastFetchedAt})` : ''}`);
47
+ lines.push('');
48
+ lines.push('---');
49
+ lines.push(entry.content);
50
+ return (0, helpers_1.ok)(lines.join('\n'));
51
+ }
52
+ catch (err) {
53
+ return (0, helpers_1.fail)(`Failed to redeem ticket: ${(0, helpers_1.formatError)(err)}`);
54
+ }
55
+ });
56
+ }
@@ -0,0 +1,4 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { Client } from '@temporalio/client';
3
+ import { Config } from '../config';
4
+ export declare function registerCoatCheckListTool(server: McpServer, client: Client, config: Config): void;
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerCoatCheckListTool = registerCoatCheckListTool;
4
+ /**
5
+ * `coat_check_list` — list coat-check entry headers (#318, ADR 0008) for
6
+ * the calling player's ensemble. Read-only — does NOT bump fetch-audit
7
+ * counters (only an actual `coat_check_get` redemption counts).
8
+ *
9
+ * Returns headers (no `content` body) sorted newest-first. Optional
10
+ * `putBy` / `prefix` / `unfetchedOnly` filters narrow the result.
11
+ */
12
+ const zod_1 = require("zod");
13
+ const config_1 = require("../config");
14
+ const maestro_signals_1 = require("../workflows/maestro-signals");
15
+ const helpers_1 = require("./helpers");
16
+ const validation_1 = require("../utils/validation");
17
+ function registerCoatCheckListTool(server, client, config) {
18
+ (0, helpers_1.defineTool)(server, 'coat_check_list', `List coat-check entries (#318) for this ensemble. Read-only — does NOT bump fetch-audit counters; only \`coat_check_get\` does. Sorted newest-first. Pass \`unfetchedOnly: true\` to surface entries nobody has redeemed yet — useful for an owner cleaning up stale stashes.`, {
19
+ putBy: zod_1.z.string().max(validation_1.PLAYER_NAME_MAX).optional().describe('Optional filter — only entries stashed by this player.'),
20
+ prefix: zod_1.z.string().max(validation_1.COAT_CHECK_SUMMARY_MAX).optional().describe('Optional summary-prefix filter — narrows the listing to entries whose summary starts with this string.'),
21
+ unfetchedOnly: zod_1.z.boolean().optional().describe('When true, only return entries with fetchCount=0 (never redeemed). Default false.'),
22
+ }, async (args) => {
23
+ const { putBy, prefix, unfetchedOnly } = args;
24
+ try {
25
+ const handle = client.workflow.getHandle((0, config_1.maestroWorkflowId)(config.ensemble));
26
+ const filter = {
27
+ ...(putBy !== undefined ? { putBy } : {}),
28
+ ...(prefix !== undefined ? { prefix } : {}),
29
+ ...(unfetchedOnly !== undefined ? { unfetchedOnly } : {}),
30
+ };
31
+ const headers = await handle.query(maestro_signals_1.coatCheckListQuery, filter);
32
+ if (headers.length === 0) {
33
+ const filters = [];
34
+ if (putBy)
35
+ filters.push(`putBy="${putBy}"`);
36
+ if (prefix)
37
+ filters.push(`prefix="${prefix}"`);
38
+ if (unfetchedOnly)
39
+ filters.push('unfetchedOnly=true');
40
+ const suffix = filters.length > 0 ? ` (filter: ${filters.join(', ')})` : '';
41
+ return (0, helpers_1.ok)(`No coat-check entries in this ensemble${suffix}.`);
42
+ }
43
+ const lines = [];
44
+ lines.push(`${headers.length}/${validation_1.COAT_CHECK_SLOTS_MAX} coat-check ${headers.length === 1 ? 'entry' : 'entries'}:`);
45
+ lines.push('');
46
+ for (const h of headers) {
47
+ const fetchTag = h.fetchCount === 0
48
+ ? ' (unfetched)'
49
+ : ` (fetched ${h.fetchCount}× by ${h.lastFetchedBy ?? '(unknown)'} at ${h.lastFetchedAt ?? '(unknown)'})`;
50
+ const typeTag = h.contentType ? ` [${h.contentType}]` : '';
51
+ lines.push(`- **${h.ticket}** ${typeTag}— ${h.summary}`);
52
+ lines.push(` putBy=${h.putBy} · putAt=${h.putAt} · expires=${h.expiresAt} · ${h.size} bytes${fetchTag}`);
53
+ }
54
+ return (0, helpers_1.ok)(lines.join('\n'));
55
+ }
56
+ catch (err) {
57
+ return (0, helpers_1.fail)(`Failed to list coat-check entries: ${(0, helpers_1.formatError)(err)}`);
58
+ }
59
+ });
60
+ }
@@ -0,0 +1,4 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { Client } from '@temporalio/client';
3
+ import { Config } from '../config';
4
+ export declare function registerCoatCheckPutTool(server: McpServer, client: Client, config: Config, getPlayerId: () => string): void;
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerCoatCheckPutTool = registerCoatCheckPutTool;
4
+ /**
5
+ * `coat_check_put` — stash a large content body on per-ensemble Maestro state
6
+ * (#318, ADR 0008). Returns a ticket id that any player in the ensemble can
7
+ * later redeem via `coat_check_get` (or pass on a `cue`'s `attachmentTicket`
8
+ * field so the recipient knows what to fetch).
9
+ *
10
+ * Audit identity (`putBy`) is set by the tool layer from `getPlayerId()` —
11
+ * the MCP schema has NO `playerId` arg, so callers cannot spoof. Same
12
+ * structural-permission pattern as `save_state` (#334).
13
+ */
14
+ const zod_1 = require("zod");
15
+ const config_1 = require("../config");
16
+ const maestro_signals_1 = require("../workflows/maestro-signals");
17
+ const helpers_1 = require("./helpers");
18
+ const validation_1 = require("../utils/validation");
19
+ function registerCoatCheckPutTool(server, client, config, getPlayerId) {
20
+ (0, helpers_1.defineTool)(server, 'coat_check_put', `Stash a large content body on this ensemble's coat-check (#318). Returns a ticket id any player can redeem later via \`coat_check_get\`. Pass the ticket on a \`cue\`'s \`attachmentTicket\` field so the recipient knows what to fetch.
21
+
22
+ Use this when your message body would otherwise exceed the cue's 100 KB cap — researcher reports, review-item dumps, etc. The cue body should carry a short summary; the coat-check entry holds the full artifact.
23
+
24
+ Limits: ${validation_1.COAT_CHECK_CONTENT_MAX} bytes (UTF-8) per entry, max ${validation_1.COAT_CHECK_SLOTS_MAX} live entries per ensemble. Saturation rejects with \`CoatCheckSlotsFull\` — wait for TTL or \`coat_check_evict\` an entry you own. TTL defaults to 7 days (configurable per put within [1h, 30d]).`, {
25
+ summary: zod_1.z.string().min(1).max(validation_1.COAT_CHECK_SUMMARY_MAX).describe(`Short preamble surfaced in \`coat_check_list\` and on dashboards (≤${validation_1.COAT_CHECK_SUMMARY_MAX} chars). 1-2 sentences describing what the recipient gets if they redeem.`),
26
+ content: zod_1.z.string().min(1).max(validation_1.COAT_CHECK_CONTENT_MAX).describe(`The full content body — markdown encouraged, opaque to the system. Max ${validation_1.COAT_CHECK_CONTENT_MAX} bytes (UTF-8).`),
27
+ contentType: zod_1.z.string().max(validation_1.COAT_CHECK_CONTENT_TYPE_MAX).optional().describe(`Optional MIME-shaped hint (e.g. "text/markdown"). Free-form; ≤${validation_1.COAT_CHECK_CONTENT_TYPE_MAX} chars.`),
28
+ ttlMs: zod_1.z.number().int().min(validation_1.COAT_CHECK_TTL_MIN_MS).max(validation_1.COAT_CHECK_TTL_MAX_MS).optional().describe(`Time-to-live in milliseconds. Default ${validation_1.COAT_CHECK_TTL_DEFAULT_MS} (7 days). Range [${validation_1.COAT_CHECK_TTL_MIN_MS}, ${validation_1.COAT_CHECK_TTL_MAX_MS}] (1h to 30d).`),
29
+ }, async (args) => {
30
+ const { summary, content, contentType, ttlMs } = args;
31
+ const putBy = getPlayerId();
32
+ try {
33
+ const handle = client.workflow.getHandle((0, config_1.maestroWorkflowId)(config.ensemble));
34
+ const result = await handle.executeUpdate(maestro_signals_1.coatCheckPutUpdate, {
35
+ args: [{
36
+ summary,
37
+ content,
38
+ ...(contentType !== undefined ? { contentType } : {}),
39
+ ...(ttlMs !== undefined ? { ttlMs } : {}),
40
+ putBy,
41
+ }],
42
+ });
43
+ return (0, helpers_1.ok)(`Stashed as ticket **${result.ticket}** (expires ${result.expiresAt}). Slots: ${result.slotsUsed}/${result.slotsTotal}.`);
44
+ }
45
+ catch (err) {
46
+ // The workflow validator surfaces structured ApplicationFailure
47
+ // errors (`CoatCheckSlotsFull`, `CoatCheckEntryTooLarge`, …). The
48
+ // `formatError` message preserves the workflow text so the LLM
49
+ // sees the oldest-3 ticket list and can pick which to evict.
50
+ return (0, helpers_1.fail)(`Failed to stash content: ${(0, helpers_1.formatError)(err)}`);
51
+ }
52
+ });
53
+ }
package/dist/tools/cue.js CHANGED
@@ -56,11 +56,12 @@ function formatDetachedDeliveryError(playerId, phase) {
56
56
  `(3) the workflow inbox queues the signal and auto-delivers on re-attach.`);
57
57
  }
58
58
  function registerCueTool(server, client, config, getPlayerId, handle) {
59
- (0, helpers_1.defineTool)(server, 'cue', 'Send a message to another Claude Code session by player name. Delivered instantly via Temporal signal.', {
59
+ (0, helpers_1.defineTool)(server, 'cue', 'Send a message to another Claude Code session by player name. Delivered instantly via Temporal signal. For content larger than ~100 KB, use `coat_check_put` to stash the body and pass the returned ticket via `attachmentTicket` — the cue body itself should carry a short summary the recipient can act on without fetching.', {
60
60
  playerId: zod_1.z.string().max(validation_1.PLAYER_NAME_MAX).describe('The player name of the target session'),
61
61
  message: zod_1.z.string().max(validation_1.MESSAGE_MAX).describe('The message to send'),
62
+ attachmentTicket: zod_1.z.string().regex(validation_1.COAT_CHECK_TICKET_REGEX).max(validation_1.COAT_CHECK_TICKET_MAX).optional().describe('Optional coat-check ticket (#318). Reference content stashed via `coat_check_put`; the receiver sees the ticket on their `recall` message and can pull the body via `coat_check_get`. Backward-compatible — omit for normal cues.'),
62
63
  }, async (args) => {
63
- const { playerId, message } = args;
64
+ const { playerId, message, attachmentTicket } = args;
64
65
  const nameError = (0, validation_1.validatePlayerName)(playerId);
65
66
  if (nameError)
66
67
  return (0, helpers_1.fail)(nameError);
@@ -99,6 +100,7 @@ function registerCueTool(server, client, config, getPlayerId, handle) {
99
100
  type: 'cue',
100
101
  targetPlayerId: playerId,
101
102
  message,
103
+ ...(attachmentTicket !== undefined ? { attachmentTicket } : {}),
102
104
  };
103
105
  const entryId = await handle.executeUpdate(signals_1.submitOutboxUpdate, { args: [entry] });
104
106
  return (0, helpers_1.ok)(`Message sent to ${playerId}. (outbox: ${entryId})`);
@@ -41,7 +41,7 @@ const helpers_1 = require("./helpers");
41
41
  const worktree_1 = require("../utils/worktree");
42
42
  const validation_1 = require("../utils/validation");
43
43
  function registerWorktreeTool(server, client, config, handle, getPlayerId) {
44
- (0, helpers_1.defineTool)(server, 'worktree', 'Manage git worktrees for player isolation. Conductor only. Actions: create (provision worktree for a player), remove (clean up), list (show active worktrees). Use when multiple players commit to different branches of the same repo simultaneously; skip for read-only work, sequential work, or tasks under ~5 min. See docs/orchestration.md#when-to-use-worktrees.', {
44
+ (0, helpers_1.defineTool)(server, 'worktree', 'Manage git worktrees for player isolation. Conductor only. Actions: create (provision worktree for a player), remove (clean up), list (show active worktrees). Use when multiple players commit to different branches of the same repo simultaneously; skip for read-only work, sequential work, or tasks under ~5 min. IMPORTANT: before `remove`, have the player stop any long-running processes inside the worktree (dev servers, file watchers) — on Windows a memory-mapped native module will block directory removal and `remove` will fail. See docs/orchestration.md#when-to-use-worktrees.', {
45
45
  action: zod_1.z.enum(['create', 'remove', 'list']).describe('Action to perform'),
46
46
  player: zod_1.z.string().max(validation_1.PLAYER_NAME_MAX).optional().describe('Player name (required for create/remove)'),
47
47
  branch: zod_1.z.string().optional().describe('Git branch for the worktree (defaults to {ensemble}/{player-name})'),
@@ -132,9 +132,22 @@ function registerWorktreeTool(server, client, config, handle, getPlayerId) {
132
132
  if (!entry) {
133
133
  return (0, helpers_1.fail)(`No worktree found for player "${player}".`);
134
134
  }
135
- // Remove from disk
136
- (0, worktree_1.removeWorktree)(entry.path);
137
- // Remove from conductor state
135
+ // Remove from disk. #594: removeWorktree throws if the directory
136
+ // survives the removal (Windows file-lock half-removal). We must
137
+ // NOT signal `removeWorktree` state or cue the player until disk
138
+ // removal is confirmed — otherwise Temporal state records "no
139
+ // worktree" while a locked orphan directory remains on disk, and
140
+ // the next `create` fails with a confusing git fatal.
141
+ try {
142
+ (0, worktree_1.removeWorktree)(entry.path, entry.gitRoot);
143
+ }
144
+ catch (err) {
145
+ return (0, helpers_1.fail)(`Worktree for **${player}** could not be removed: ${(0, helpers_1.formatError)(err)}\n\n` +
146
+ `Conductor state is unchanged — the worktree is still tracked. ` +
147
+ `Have the player stop any long-running processes inside the worktree ` +
148
+ `(dev servers, file watchers), then retry \`worktree remove\`.`);
149
+ }
150
+ // Remove from conductor state (only reached on confirmed disk removal)
138
151
  await handle.signal('removeWorktree', player);
139
152
  // Auto-cue the player
140
153
  try {
package/dist/types.d.ts CHANGED
@@ -437,6 +437,13 @@ export interface Message {
437
437
  * for non-broadcast direct cues.
438
438
  */
439
439
  broadcastId?: string;
440
+ /**
441
+ * #318: Coat-check ticket id when the sender stashed content via
442
+ * `coat_check_put` and called `cue` with `attachmentTicket`. Receivers
443
+ * call `coat_check_get` with this value to pull the full content body.
444
+ * `undefined` for cues without an attachment.
445
+ */
446
+ attachmentTicket?: string;
440
447
  }
441
448
  export interface SentMessage {
442
449
  id: string;
@@ -485,6 +492,14 @@ export interface CueOutboxEntry extends OutboxEntryBase {
485
492
  * into a single chat row. `undefined` for non-broadcast cues.
486
493
  */
487
494
  broadcastId?: string;
495
+ /**
496
+ * #318: Optional coat-check ticket id referencing content stashed on
497
+ * per-ensemble Maestro state via `coat_check_put`. Receivers see this
498
+ * field on their delivered `Message` and can call `coat_check_get` to
499
+ * pull the full content body. Backward-compatible — pre-#318 cues just
500
+ * don't have it.
501
+ */
502
+ attachmentTicket?: string;
488
503
  }
489
504
  export interface RecruitOutboxEntry extends OutboxEntryBase {
490
505
  type: 'recruit';
@@ -941,5 +956,65 @@ export interface MaestroInput {
941
956
  startMs: number;
942
957
  count: number;
943
958
  };
959
+ /**
960
+ * #318 coat-check pattern — ticket-keyed stash for large content. Carried
961
+ * across CAN only when the map is non-empty (mirrors the `playerState`
962
+ * carry idiom in `src/workflows/session.ts:1617-1638`).
963
+ */
964
+ coatCheck?: Record<string, CoatCheckEntry>;
965
+ }
966
+ /**
967
+ * A single coat-check entry as stored on per-ensemble Maestro workflow
968
+ * state. `content` is opaque to the system — author-supplied markdown,
969
+ * JSON, or arbitrary text up to `COAT_CHECK_CONTENT_MAX`. All timestamps
970
+ * are ISO 8601 from `workflowNow()` for replay determinism.
971
+ *
972
+ * The `lastFetched*` / `fetchCount` triple is the fetch-audit surface
973
+ * vinceblank added on top of the architect's verdict — owners need to
974
+ * know if a recipient consumed their ticket. Default semantics: undefined
975
+ * `lastFetchedAt` + `fetchCount === 0` means "never fetched."
976
+ */
977
+ export interface CoatCheckEntry {
978
+ /** Short human-readable preamble (≤ `COAT_CHECK_SUMMARY_MAX`). */
979
+ summary: string;
980
+ /** Opaque body (≤ `COAT_CHECK_CONTENT_MAX` UTF-8 bytes). */
981
+ content: string;
982
+ /** Optional MIME-shaped hint (e.g. `'text/markdown'`); free-form. */
983
+ contentType?: string;
984
+ /** Audit identity — player who called `coat_check_put`. */
985
+ putBy: string;
986
+ /** When the entry was admitted, `workflowNow().toISOString()`. */
987
+ putAt: string;
988
+ /** When the entry expires under TTL inline-sweep, `workflowNow().toISOString()`. */
989
+ expiresAt: string;
990
+ /** UTF-8 byte length of `content` — pre-computed at put time for cheap listing. */
991
+ size: number;
992
+ /** Last successful `coat_check_get` timestamp, or undefined if never fetched. */
993
+ lastFetchedAt?: string;
994
+ /** Last `coat_check_get` caller's playerId, or undefined if never fetched. */
995
+ lastFetchedBy?: string;
996
+ /** Successful `coat_check_get` count. 0 until first fetch. */
997
+ fetchCount: number;
998
+ }
999
+ /**
1000
+ * Listing projection — `coat_check_list` and the put/evict update return
1001
+ * shapes that omit `content` so callers can survey without pulling the
1002
+ * whole body for every entry. `size` is preserved so dashboards can
1003
+ * present an at-a-glance "how big" without an extra fetch.
1004
+ */
1005
+ export interface CoatCheckEntryHeader {
1006
+ ticket: string;
1007
+ summary: string;
1008
+ contentType?: string;
1009
+ putBy: string;
1010
+ putAt: string;
1011
+ expiresAt: string;
1012
+ size: number;
1013
+ /** Mirrors `CoatCheckEntry.lastFetchedAt`. */
1014
+ lastFetchedAt?: string;
1015
+ /** Mirrors `CoatCheckEntry.lastFetchedBy`. */
1016
+ lastFetchedBy?: string;
1017
+ /** Mirrors `CoatCheckEntry.fetchCount`. */
1018
+ fetchCount: number;
944
1019
  }
945
1020
  export {};
@@ -28,6 +28,24 @@ export declare const PLAYER_STATE_KEY_MAX = 32;
28
28
  export declare const PLAYER_STATE_CONTENT_MAX: number;
29
29
  /** Maximum number of populated slots per player. Saturation rejects with `PlayerStateSlotsFull`. */
30
30
  export declare const PLAYER_STATE_SLOTS_MAX = 4;
31
+ /** Coat-check ticket id pattern. Alphanumeric + underscore + hyphen, generated server-side via `uuid4()`. */
32
+ export declare const COAT_CHECK_TICKET_REGEX: RegExp;
33
+ /** Maximum coat-check ticket id length. Generous to accommodate uuid4 + future versioning prefixes. */
34
+ export declare const COAT_CHECK_TICKET_MAX = 64;
35
+ /** Maximum content size per coat-check entry — 32 KiB. Mirrors `PLAYER_STATE_CONTENT_MAX`. */
36
+ export declare const COAT_CHECK_CONTENT_MAX: number;
37
+ /** Maximum summary length per coat-check entry — short, listing-friendly preamble. */
38
+ export declare const COAT_CHECK_SUMMARY_MAX = 500;
39
+ /** Maximum free-form contentType string (e.g. `'text/markdown'`). */
40
+ export declare const COAT_CHECK_CONTENT_TYPE_MAX = 64;
41
+ /** Maximum populated coat-check slots per ensemble. Saturation rejects with `CoatCheckSlotsFull`. */
42
+ export declare const COAT_CHECK_SLOTS_MAX = 20;
43
+ /** Minimum allowed `ttlMs` argument on `coat_check_put` — 1 hour. */
44
+ export declare const COAT_CHECK_TTL_MIN_MS: number;
45
+ /** Maximum allowed `ttlMs` argument on `coat_check_put` — 30 days. */
46
+ export declare const COAT_CHECK_TTL_MAX_MS: number;
47
+ /** Default `ttlMs` when caller omits the argument — 7 days. */
48
+ export declare const COAT_CHECK_TTL_DEFAULT_MS: number;
31
49
  /**
32
50
  * Maximum length of the ensemble description set via
33
51
  * `set_ensemble_description` (#399 W1, Q5.1). Soft cap — the MCP tool
@@ -4,7 +4,7 @@
4
4
  * Used by MCP tool Zod schemas and config validation.
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.DEFAULT_RESTART_LEASE_MS = exports.DEFAULT_RESTART_DETACH_DEADLINE_MS = exports.MAX_DETACH_DEADLINE_MS = exports.RESTART_CONTEXT_MESSAGES_MAX = exports.PREVIEW_MAX_LENGTH = exports.WORKTREE_INSTALL_TIMEOUT = exports.GATE_NOTES_MAX = exports.GATE_CRITERION_TEXT_MAX = exports.GATE_CRITERIA_MAX = exports.GATE_TASK_MAX = exports.CRON_EXPRESSION_MAX = exports.SCHEDULE_MESSAGE_MAX = exports.SCHEDULE_NAME_MAX = exports.PATH_MAX = exports.ENSEMBLE_DESCRIPTION_MAX = exports.PLAYER_STATE_SLOTS_MAX = exports.PLAYER_STATE_CONTENT_MAX = exports.PLAYER_STATE_KEY_MAX = exports.PLAYER_STATE_KEY_REGEX = exports.PLAYER_STATE_DEFAULT_KEY = exports.PART_MAX = exports.MESSAGE_MAX = exports.STAGE_PLAYERS_MAX = exports.STAGE_NAME_MAX = exports.ENSEMBLE_NAME_REGEX = exports.PLAYER_NAME_MAX = exports.PLAYER_NAME_REGEX = void 0;
7
+ exports.DEFAULT_RESTART_LEASE_MS = exports.DEFAULT_RESTART_DETACH_DEADLINE_MS = exports.MAX_DETACH_DEADLINE_MS = exports.RESTART_CONTEXT_MESSAGES_MAX = exports.PREVIEW_MAX_LENGTH = exports.WORKTREE_INSTALL_TIMEOUT = exports.GATE_NOTES_MAX = exports.GATE_CRITERION_TEXT_MAX = exports.GATE_CRITERIA_MAX = exports.GATE_TASK_MAX = exports.CRON_EXPRESSION_MAX = exports.SCHEDULE_MESSAGE_MAX = exports.SCHEDULE_NAME_MAX = exports.PATH_MAX = exports.ENSEMBLE_DESCRIPTION_MAX = exports.COAT_CHECK_TTL_DEFAULT_MS = exports.COAT_CHECK_TTL_MAX_MS = exports.COAT_CHECK_TTL_MIN_MS = exports.COAT_CHECK_SLOTS_MAX = exports.COAT_CHECK_CONTENT_TYPE_MAX = exports.COAT_CHECK_SUMMARY_MAX = exports.COAT_CHECK_CONTENT_MAX = exports.COAT_CHECK_TICKET_MAX = exports.COAT_CHECK_TICKET_REGEX = exports.PLAYER_STATE_SLOTS_MAX = exports.PLAYER_STATE_CONTENT_MAX = exports.PLAYER_STATE_KEY_MAX = exports.PLAYER_STATE_KEY_REGEX = exports.PLAYER_STATE_DEFAULT_KEY = exports.PART_MAX = exports.MESSAGE_MAX = exports.STAGE_PLAYERS_MAX = exports.STAGE_NAME_MAX = exports.ENSEMBLE_NAME_REGEX = exports.PLAYER_NAME_MAX = exports.PLAYER_NAME_REGEX = void 0;
8
8
  exports.shouldIncludeInBroadcast = shouldIncludeInBroadcast;
9
9
  exports.validatePlayerName = validatePlayerName;
10
10
  exports.validateEnsembleName = validateEnsembleName;
@@ -43,6 +43,37 @@ exports.PLAYER_STATE_KEY_MAX = 32;
43
43
  exports.PLAYER_STATE_CONTENT_MAX = 32 * 1024;
44
44
  /** Maximum number of populated slots per player. Saturation rejects with `PlayerStateSlotsFull`. */
45
45
  exports.PLAYER_STATE_SLOTS_MAX = 4;
46
+ // ── Coat-check (#318, ADR 0008) ────────────────────────────────────────────
47
+ //
48
+ // Ensemble-shared, ticket-keyed stash for content too large to inline in a
49
+ // `cue` message body. Lives on per-ensemble Maestro workflow state.
50
+ //
51
+ // Sizing per the #318 architect verdict (which adjusts ADR 0008's 50-slot
52
+ // proposal):
53
+ // per-entry 32 KiB × max 20 slots → 640 KiB structural max per ensemble.
54
+ // That lands at ~16 % of Temporal's 4 MiB CAN-payload ceiling — comfortable
55
+ // headroom on top of existing maestro state (~316 KiB). Saturation refuses
56
+ // with a `CoatCheckSlotsFull` ApplicationFailure rather than evicting LRU
57
+ // — cross-host scope makes silent peer eviction a sharper footgun than
58
+ // the rejection's diagnostic UX.
59
+ /** Coat-check ticket id pattern. Alphanumeric + underscore + hyphen, generated server-side via `uuid4()`. */
60
+ exports.COAT_CHECK_TICKET_REGEX = /^[a-zA-Z0-9_-]+$/;
61
+ /** Maximum coat-check ticket id length. Generous to accommodate uuid4 + future versioning prefixes. */
62
+ exports.COAT_CHECK_TICKET_MAX = 64;
63
+ /** Maximum content size per coat-check entry — 32 KiB. Mirrors `PLAYER_STATE_CONTENT_MAX`. */
64
+ exports.COAT_CHECK_CONTENT_MAX = 32 * 1024;
65
+ /** Maximum summary length per coat-check entry — short, listing-friendly preamble. */
66
+ exports.COAT_CHECK_SUMMARY_MAX = 500;
67
+ /** Maximum free-form contentType string (e.g. `'text/markdown'`). */
68
+ exports.COAT_CHECK_CONTENT_TYPE_MAX = 64;
69
+ /** Maximum populated coat-check slots per ensemble. Saturation rejects with `CoatCheckSlotsFull`. */
70
+ exports.COAT_CHECK_SLOTS_MAX = 20;
71
+ /** Minimum allowed `ttlMs` argument on `coat_check_put` — 1 hour. */
72
+ exports.COAT_CHECK_TTL_MIN_MS = 60 * 60 * 1000;
73
+ /** Maximum allowed `ttlMs` argument on `coat_check_put` — 30 days. */
74
+ exports.COAT_CHECK_TTL_MAX_MS = 30 * 24 * 60 * 60 * 1000;
75
+ /** Default `ttlMs` when caller omits the argument — 7 days. */
76
+ exports.COAT_CHECK_TTL_DEFAULT_MS = 7 * 24 * 60 * 60 * 1000;
46
77
  /**
47
78
  * Maximum length of the ensemble description set via
48
79
  * `set_ensemble_description` (#399 W1, Q5.1). Soft cap — the MCP tool
@@ -82,5 +82,22 @@ export declare function createWorktree(opts: CreateWorktreeOpts): CreateWorktree
82
82
  export declare function installDependencies(worktreePath: string, timeoutMs?: number): void;
83
83
  /**
84
84
  * Remove a git worktree.
85
+ *
86
+ * Throws if the worktree directory survives the removal (#594). On Windows,
87
+ * `git worktree remove --force` deletes `.git/worktrees/<name>` metadata
88
+ * *first*, then `rmdir`s the directory — so when a native `.node` module
89
+ * inside the worktree is memory-mapped by a live process, the directory
90
+ * deletion fails while the metadata is already gone. The pre-#594 code
91
+ * swallowed that failure (log-only, no throw), so the caller reported
92
+ * success and Temporal state diverged from disk. Now the post-removal
93
+ * `existsSync` check turns a half-removal into a hard error the caller can
94
+ * surface.
95
+ *
96
+ * `gitRoot` scopes the `git worktree remove` invocation to the owning
97
+ * repository. Pre-#594 the command ran with no `cwd`, so it only worked when
98
+ * `process.cwd()` happened to be the right repo — fragile, and wrong outright
99
+ * when the conductor's cwd is a different checkout than the worktree's repo.
100
+ * Callers should pass `entry.gitRoot` from the `WorktreeEntry`; it defaults to
101
+ * `process.cwd()` for backward compatibility.
85
102
  */
86
- export declare function removeWorktree(worktreePath: string): void;
103
+ export declare function removeWorktree(worktreePath: string, gitRoot?: string): void;