@cydm/happy-elves 0.1.0-beta.58 → 0.1.0-beta.59

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.
Files changed (56) hide show
  1. package/apps/cli/dist/commands/lib/orchestrator.d.ts +2 -1
  2. package/apps/cli/dist/commands/lib/orchestrator.js +3 -0
  3. package/apps/cli/dist/commands/lib/usage.js +28 -7
  4. package/apps/cli/dist/commands/machine.js +29 -5
  5. package/apps/cli/dist/commands/orchestrator.js +94 -42
  6. package/apps/cli/dist/commands/session.js +24 -0
  7. package/apps/cli/dist/commands/skill.js +1 -0
  8. package/apps/cli/dist/skills/registry.js +11 -6
  9. package/apps/daemon/dist/gateway/runtime.js +1 -0
  10. package/apps/daemon/dist/runtime/external-provider.js +3 -2
  11. package/apps/daemon/dist/runtime.js +1 -1
  12. package/apps/daemon/dist/session/lifecycle.d.ts +2 -1
  13. package/apps/daemon/dist/session/lifecycle.js +42 -7
  14. package/apps/daemon/dist/session/metadata.d.ts +2 -0
  15. package/apps/daemon/dist/session/metadata.js +49 -0
  16. package/apps/daemon/dist/session/primitives.js +23 -13
  17. package/apps/daemon/dist/session/prompt.js +11 -7
  18. package/apps/daemon/dist/types.d.ts +6 -0
  19. package/apps/daemon/package.json +1 -1
  20. package/apps/relay/dist/connections.d.ts +3 -0
  21. package/apps/relay/dist/controller-handlers.d.ts +2 -1
  22. package/apps/relay/dist/controller-handlers.js +119 -8
  23. package/apps/relay/dist/db.js +2 -0
  24. package/apps/relay/dist/http-routes.js +9 -3
  25. package/apps/relay/dist/http-schemas.d.ts +23 -0
  26. package/apps/relay/dist/http-schemas.js +2 -0
  27. package/apps/relay/dist/machine-command-result-handlers.js +29 -11
  28. package/apps/relay/dist/metadata-source.d.ts +3 -0
  29. package/apps/relay/dist/metadata-source.js +16 -0
  30. package/apps/relay/dist/projection-repository.d.ts +0 -7
  31. package/apps/relay/dist/projection-repository.js +0 -6
  32. package/apps/relay/dist/projections.js +2 -1
  33. package/apps/relay/dist/relay-context.d.ts +6 -20
  34. package/apps/relay/dist/relay-context.js +11 -1
  35. package/apps/relay/dist/repositories.d.ts +3 -1
  36. package/apps/relay/dist/repositories.js +23 -7
  37. package/apps/relay/dist/session-projection-reducer.js +8 -11
  38. package/apps/relay/dist/types.d.ts +2 -0
  39. package/npm-shrinkwrap.json +2 -2
  40. package/package.json +1 -1
  41. package/packages/client/dist/client.d.ts +7 -2
  42. package/packages/client/dist/client.js +39 -4
  43. package/packages/client/dist/live.js +9 -1
  44. package/packages/client/dist/parsers.js +2 -0
  45. package/packages/client/dist/types.d.ts +3 -0
  46. package/packages/pie-provider/dist/index.js +3 -3
  47. package/packages/runtime/dist/index.d.ts +1 -0
  48. package/packages/runtime-acpx/dist/index.d.ts +2 -2
  49. package/packages/runtime-acpx/dist/index.js +1 -1
  50. package/packages/runtime-cli/dist/codex-protocol.js +1 -1
  51. package/packages/runtime-cli/dist/permissions.js +3 -3
  52. package/packages/shared/dist/protocol-schemas.d.ts +1 -0
  53. package/packages/shared/dist/protocol-schemas.js +1 -0
  54. package/packages/shared/dist/protocol-types.d.ts +1 -0
  55. package/packages/shared/dist/protocol.d.ts +12 -0
  56. package/packages/shared/dist/protocol.js +14 -0
@@ -1,4 +1,4 @@
1
- import type { SessionSnapshot } from "../../../../../packages/shared/dist/index.js";
1
+ import type { PermissionMode, SessionSnapshot } from "../../../../../packages/shared/dist/index.js";
2
2
  import type { OrchestrationSummary } from "./types.js";
3
3
  import type { TurnSummary } from "./session-output.js";
4
4
  export type OrchestratorStatus = "idle" | "inProgress" | "completed" | "needsInput" | "blocked" | "failed" | "cancelled" | "offline" | "closed";
@@ -10,6 +10,7 @@ export type OrchestratorRequestRecord = {
10
10
  runtimeTurnId?: string;
11
11
  model?: string | null;
12
12
  thoughtLevel?: string | null;
13
+ permissionMode?: PermissionMode;
13
14
  createdAt: number;
14
15
  };
15
16
  export type OrchestratorMetadata = {
@@ -83,6 +83,9 @@ export function readOrchestratorMetadata(metadata) {
83
83
  ...(typeof candidate.runtimeTurnId === "string" ? { runtimeTurnId: candidate.runtimeTurnId } : {}),
84
84
  ...(candidate.model === null || typeof candidate.model === "string" ? { model: candidate.model } : {}),
85
85
  ...(candidate.thoughtLevel === null || typeof candidate.thoughtLevel === "string" ? { thoughtLevel: candidate.thoughtLevel } : {}),
86
+ ...(candidate.permissionMode === "approve-all" || candidate.permissionMode === "approve-reads" || candidate.permissionMode === "deny-all"
87
+ ? { permissionMode: candidate.permissionMode }
88
+ : {}),
86
89
  createdAt: candidate.createdAt,
87
90
  };
88
91
  }
@@ -31,8 +31,10 @@ Check local controller config, relay reachability, and daemon state.
31
31
  happy-elves remote revoke <deviceId> --json
32
32
  `,
33
33
  machine: `Usage:
34
- happy-elves machine list [--verbose] [--json]
35
- happy-elves machine status <machineId> [--verbose] [--json]
34
+ happy-elves machine list [--include-archived] [--verbose] [--json]
35
+ happy-elves machine status <machineId> [--include-archived] [--verbose] [--json]
36
+ happy-elves machine archive <machineId> [--json]
37
+ happy-elves machine unarchive <machineId> [--json]
36
38
  happy-elves machine diagnose <machineId> [--json]
37
39
  happy-elves machine directory <machineId> [--path <path>] [--json]
38
40
  happy-elves machine exec <machineId> --shell <command> [--cwd <path>] [--timeout-ms <ms>] [--wait-timeout-ms <ms>] [--json]
@@ -58,6 +60,9 @@ Codex-like orchestration:
58
60
  returns a deterministic turnId for the supplied request id. wait-thread waits
59
61
  for that exact turn id instead of following the latest turn. handoff injects
60
62
  bounded source turn context directly into the target prompt.
63
+ Permission defaults to trusted-local Yolo (approve-all). Use --permission-mode
64
+ default/ask/accept-edits/approve-reads/deny-all/read-only only to tighten one
65
+ turn.
61
66
  `,
62
67
  session: `Usage:
63
68
  happy-elves session create --machine <machineId> [--agent codex] [--cwd <path>] [--name <name>] [--permission-mode <mode>] [--model <key>] [--thought-level <key>|--thinking <key>] --json
@@ -88,7 +93,7 @@ Session management:
88
93
  happy-elves turn list <sessionId> [--limit 10] [--verbose] [--json]
89
94
  happy-elves turn status <sessionId> <turnId> [--verbose] [--json]
90
95
  happy-elves turn result <sessionId> [<turnId>] [--verbose] [--json]
91
- happy-elves turn run <sessionId> (--text <prompt> | --text-file <path>) [--detach] [--timeout 1000s] [--runtime-timeout 1000s] [--permission-mode approve-reads|approve-all|deny-all] [--model <key>] [--thought-level <key>|--thinking <key>] [--json]
96
+ happy-elves turn run <sessionId> (--text <prompt> | --text-file <path>) [--detach] [--timeout 1000s] [--runtime-timeout 1000s] [--permission-mode <mode>] [--model <key>] [--thought-level <key>|--thinking <key>] [--json]
92
97
  happy-elves turn wait <sessionId> <turnId> [--timeout 1000s] --json
93
98
  happy-elves turn cancel <sessionId> <turnId> [--reason <reason>] --json
94
99
  happy-elves turn rewind <sessionId> <turn-or-checkpoint-id> --json
@@ -96,7 +101,9 @@ Session management:
96
101
 
97
102
  Turn management:
98
103
  run starts one agent execution. It waits by default; use --detach for
99
- background execution.
104
+ background execution. Permission defaults to trusted-local Yolo (approve-all);
105
+ pass --permission-mode default/ask/accept-edits/approve-reads/deny-all/read-only
106
+ to tighten.
100
107
  wait/result inspect a specific turn.
101
108
  rewind/fork operate on a turn or checkpoint id supplied by runtime history.
102
109
  `,
@@ -164,7 +171,7 @@ export function commandUsage(command, exitCode = 1) {
164
171
  export function usageText() {
165
172
  return `happy-elves
166
173
 
167
- Happy Elves is a remote controller and orchestration layer for coding agents across machines. The local agent remains the authority for runtime state. Agents should run happy-elves --skill before using this CLI.
174
+ Happy Elves is a remote controller and orchestration layer for coding agents across machines. The local agent remains the authority for runtime state. Agents should run happy-elves skill doctor --name happy-elves-orchestrator --json before orchestrating; happy-elves --skill remains legacy guidance.
168
175
 
169
176
  Core commands:
170
177
  machine list [--json]
@@ -248,6 +255,19 @@ Recommended agent workflow:
248
255
  5. orchestrator read-turn <threadId> <turnId> --json
249
256
  6. orchestrator handoff --from <sourceThreadId> --turn <sourceTurnId> --to <targetThreadId> --text <instruction> --request-id <stable-id> --json
250
257
 
258
+ Permission defaults:
259
+ Happy Elves is trusted-local by default. If orchestrator send-message omits
260
+ --permission-mode, the effective permission is approve-all. Pass
261
+ --permission-mode default/ask/accept-edits/approve-reads/deny-all/read-only
262
+ only when you intentionally want to tighten a turn. Inspect
263
+ effectivePermissionMode and permissionSource before diagnosing sandbox, git,
264
+ desktop, or backend permission failures.
265
+
266
+ Orchestrator idempotency:
267
+ Reusing the same request id with the same prompt/model/thinking/permission
268
+ returns the same turn. Reusing it with different prompt/model/thinking/
269
+ permission returns REQUEST_ID_CONFLICT.
270
+
251
271
  Concepts:
252
272
  Loaded session: a Happy Elves control-plane session that can be operated by
253
273
  this CLI.
@@ -282,8 +302,9 @@ Output modes:
282
302
  thread's history is unavailable, the item reports historyStatus/historyError
283
303
  instead of pretending to be healthy inProgress.
284
304
  send-message requires --request-id and is detached by default. Reusing the
285
- same request id with the same prompt/model/thinking returns the same turn;
286
- reusing it with different prompt/model/thinking returns REQUEST_ID_CONFLICT.
305
+ same request id with the same prompt/model/thinking/permission returns the
306
+ same turn; reusing it with different prompt/model/thinking/permission returns
307
+ REQUEST_ID_CONFLICT.
287
308
  After send-message, keep the orchestrator turn open and wait on the exact
288
309
  returned turn id until terminal. Do not stop after dispatching the prompt.
289
310
  For direct handoff, prefer handoff or send-message --context-turn. The CLI
@@ -6,6 +6,8 @@ function structuredMachineSummary(machine) {
6
6
  machineId: machine.id,
7
7
  name: machine.metadata.name ?? machine.name,
8
8
  online: machine.online,
9
+ archived: Boolean(machine.archivedAt),
10
+ archivedAt: machine.archivedAt,
9
11
  sessionCount: summary.sessionCount,
10
12
  runningCount: summary.runningSessionCount,
11
13
  blockedCount: summary.blockedSessionCount,
@@ -15,6 +17,8 @@ function structuredMachineVerbose(machine) {
15
17
  return {
16
18
  machineId: machine.id,
17
19
  online: machine.online,
20
+ archived: Boolean(machine.archivedAt),
21
+ archivedAt: machine.archivedAt,
18
22
  lastSeen: machine.lastSeen,
19
23
  name: machine.metadata.name ?? machine.name,
20
24
  defaultCwd: machine.defaultCwd,
@@ -28,14 +32,17 @@ function structuredMachineVerbose(machine) {
28
32
  function printMachineLine(machine) {
29
33
  const name = machine.metadata.name ?? machine.name;
30
34
  const summary = machine.summary;
31
- console.log(`${machine.id} ${machine.online ? "online" : "offline"} ${compactText(name, 32)} sessions:${summary.sessionCount} running:${summary.runningSessionCount} blocked:${summary.blockedSessionCount}`);
35
+ const state = machine.archivedAt ? "archived" : machine.online ? "online" : "offline";
36
+ console.log(`${machine.id} ${state} ${compactText(name, 32)} sessions:${summary.sessionCount} running:${summary.runningSessionCount} blocked:${summary.blockedSessionCount}`);
32
37
  }
33
38
  function printMachineDetails(machine) {
34
39
  const name = machine.metadata.name ?? machine.name;
35
40
  const agents = machine.capabilities.agents.map((agent) => agent.id).join(", ") || "-";
36
41
  const summary = machine.summary;
37
42
  console.log(`${name} (${machine.id})`);
38
- console.log(` status: ${machine.online ? "online" : "offline"} agents: ${agents}`);
43
+ console.log(` status: ${machine.archivedAt ? "archived" : machine.online ? "online" : "offline"} agents: ${agents}`);
44
+ if (machine.archivedAt)
45
+ console.log(` archived: ${new Date(machine.archivedAt).toISOString()}`);
39
46
  console.log(` cwd: ${machine.defaultCwd ?? "-"}`);
40
47
  console.log(` sessions: ${summary.sessionCount} total, ${summary.runningSessionCount} running, ${summary.readySessionCount} ready, ${summary.needsUserSessionCount} needs user`);
41
48
  console.log(` sessions: happy-elves session list --machine ${machine.id} --limit 20`);
@@ -171,7 +178,7 @@ export async function handleMachine({ domain, action, positional, flags }) {
171
178
  return true;
172
179
  }
173
180
  if (action === "list") {
174
- const snapshot = await client.snapshot();
181
+ const snapshot = await client.snapshot({ includeArchived: flags["include-archived"] === true });
175
182
  const machines = await Promise.all(snapshot.machines.map(async (machine) => ({
176
183
  ...(await showMachine(client, machine)),
177
184
  ...(await machineSessionOverview(client, snapshot.sessions, machine.id)),
@@ -192,9 +199,26 @@ export async function handleMachine({ domain, action, positional, flags }) {
192
199
  });
193
200
  return true;
194
201
  }
202
+ if (action === "archive" || action === "unarchive" || action === "restore") {
203
+ const machineId = requirePositional(positional[0], "machineId");
204
+ const machine = action === "archive"
205
+ ? await client.archiveMachine(machineId)
206
+ : await client.unarchiveMachine(machineId);
207
+ const projectedMachine = await showMachine(client, machine);
208
+ const overviewSnapshot = await client.snapshot({ includeArchived: true });
209
+ const overview = await machineSessionOverview(client, overviewSnapshot.sessions, machine.id);
210
+ if (!wantsJson(flags)) {
211
+ printMachineLine({ ...projectedMachine, ...overview });
212
+ return true;
213
+ }
214
+ ok(action === "archive" ? "machine.archive" : "machine.unarchive", {
215
+ machine: structuredMachineVerbose({ ...projectedMachine, ...overview }),
216
+ }, { machineId: machine.id });
217
+ return true;
218
+ }
195
219
  if (action === "status") {
196
220
  const machineId = requirePositional(positional[0], "machineId");
197
- const snapshot = await client.snapshot();
221
+ const snapshot = await client.snapshot({ includeArchived: flags["include-archived"] === true });
198
222
  const machine = snapshot.machines.find((item) => item.id === machineId);
199
223
  if (!machine)
200
224
  throw new CliError("Machine not found", "MACHINE_NOT_FOUND");
@@ -212,7 +236,7 @@ export async function handleMachine({ domain, action, positional, flags }) {
212
236
  machineId: machine.id,
213
237
  name: projectedMachine.metadata.name ?? machine.name,
214
238
  online: machine.online,
215
- state: machine.online ? "available" : "offline",
239
+ state: machine.archivedAt ? "archived" : machine.online ? "available" : "offline",
216
240
  summary: overview.summary,
217
241
  }),
218
242
  }, { machineId: machine.id });
@@ -105,7 +105,34 @@ async function readThreadContext(client, threadId) {
105
105
  }
106
106
  return { machines: snapshot.machines, session };
107
107
  }
108
- function requestRecordForExistingTurn(turn, hash, selection, inputSha256, sourceOutputSha256, createdAt) {
108
+ function effectivePermissionMode(requestedPermissionMode) {
109
+ return requestedPermissionMode ?? "approve-all";
110
+ }
111
+ function permissionEvidence(requestedPermissionMode, record) {
112
+ if (record?.permissionMode) {
113
+ return {
114
+ effectivePermissionMode: record.permissionMode,
115
+ permissionSource: "stored-request",
116
+ ...(requestedPermissionMode !== undefined ? { requestedPermissionMode } : {}),
117
+ };
118
+ }
119
+ if (record) {
120
+ return {
121
+ permissionSource: "legacy-record",
122
+ ...(requestedPermissionMode !== undefined ? { requestedPermissionMode } : {}),
123
+ permissionWarning: "existing turn predates stored permission evidence",
124
+ };
125
+ }
126
+ return {
127
+ effectivePermissionMode: effectivePermissionMode(requestedPermissionMode),
128
+ permissionSource: requestedPermissionMode === undefined ? "product-default" : "request",
129
+ ...(requestedPermissionMode !== undefined ? { requestedPermissionMode } : {}),
130
+ };
131
+ }
132
+ function samePermissionShape(existing, permissionMode) {
133
+ return existing.permissionMode === undefined || existing.permissionMode === permissionMode;
134
+ }
135
+ function requestRecordForExistingTurn(turn, hash, selection, permissionMode, inputSha256, sourceOutputSha256, createdAt) {
109
136
  if (!turn)
110
137
  return undefined;
111
138
  return {
@@ -115,6 +142,7 @@ function requestRecordForExistingTurn(turn, hash, selection, inputSha256, source
115
142
  ...(sourceOutputSha256 ? { sourceOutputSha256 } : {}),
116
143
  ...(turn.runtimeTurnId ? { runtimeTurnId: turn.runtimeTurnId } : {}),
117
144
  ...selectionRecordFields(selection),
145
+ ...(permissionMode !== undefined ? { permissionMode } : {}),
118
146
  createdAt: turn.startedAt ?? turn.updatedAt ?? createdAt,
119
147
  };
120
148
  }
@@ -130,13 +158,15 @@ function sameRequestRecord(existing, expected) {
130
158
  existing.inputSha256 === expected.inputSha256 &&
131
159
  existing.sourceOutputSha256 === expected.sourceOutputSha256 &&
132
160
  existing.model === expected.model &&
133
- existing.thoughtLevel === expected.thoughtLevel;
161
+ existing.thoughtLevel === expected.thoughtLevel &&
162
+ samePermissionShape(existing, expected.permissionMode ?? "approve-all");
134
163
  }
135
164
  function sameBaseRequestShape(existing, input) {
136
165
  return existing.turnId === input.turnId &&
137
166
  existing.inputSha256 === input.inputSha256 &&
138
167
  existing.model === input.model &&
139
- existing.thoughtLevel === input.thoughtLevel;
168
+ existing.thoughtLevel === input.thoughtLevel &&
169
+ samePermissionShape(existing, input.permissionMode);
140
170
  }
141
171
  function sameEffectiveRequestShape(existing, input) {
142
172
  return existing.promptSha256 === input.promptSha256 &&
@@ -383,25 +413,26 @@ async function safeEventsForThreadSummary(client, session) {
383
413
  return { events: [], historyFailure: threadHistoryFailure(error) };
384
414
  }
385
415
  }
386
- async function persistRequestRecordBestEffort(client, threadId, requestId, record) {
416
+ async function persistRequestRecord(client, threadId, requestId, record) {
387
417
  for (let attempt = 0; attempt < 3; attempt += 1) {
388
418
  const { session } = await readThreadContext(client, threadId);
389
419
  const metadata = await client.decodeSessionMetadata(session);
390
420
  const existing = readOrchestratorMetadata(metadata).requests?.[requestId];
391
421
  if (existing) {
392
422
  if (sameRequestRecord(existing, record))
393
- return;
394
- return;
423
+ return { metadata, session };
424
+ throw requestConflict(threadId, requestId, record.turnId);
395
425
  }
396
426
  try {
397
- await client.updateSessionMetadata(session.id, withOrchestratorRequest(metadata, requestId, record), { expectedUpdatedAt: session.updatedAt });
398
- return;
427
+ const updated = await client.updateSessionMetadata(session.id, withOrchestratorRequest(metadata, requestId, record), { expectedEncryptedMetadata: session.encryptedMetadata ?? null, expectedUpdatedAt: session.updatedAt });
428
+ return { metadata: await client.decodeSessionMetadata(updated), session: updated };
399
429
  }
400
430
  catch (error) {
401
431
  if (errorCode(error) !== "SESSION_METADATA_CONFLICT")
402
- return;
432
+ throw error;
403
433
  }
404
434
  }
435
+ throw new OrchestratorError("Session metadata changed while recording request id; retry with the latest session.", "SESSION_METADATA_CONFLICT", { retryable: true, sessionId: threadId, turnId: record.turnId });
405
436
  }
406
437
  function terminalStatusForTurn(turn) {
407
438
  if (turn.status === "completed")
@@ -413,7 +444,7 @@ function terminalStatusForTurn(turn) {
413
444
  return "inProgress";
414
445
  }
415
446
  function requestConflict(threadId, requestId, turnId) {
416
- return new OrchestratorError(`Request id already exists with different prompt, model, or thoughtLevel: ${requestId}`, "REQUEST_ID_CONFLICT", { retryable: false, sessionId: threadId, turnId });
447
+ return new OrchestratorError(`Request id already exists with different prompt, model, thoughtLevel, or permissionMode: ${requestId}`, "REQUEST_ID_CONFLICT", { retryable: false, sessionId: threadId, turnId });
417
448
  }
418
449
  function threadBusy(threadId, active, latestCompleted) {
419
450
  return new OrchestratorError(`Thread already has a running turn: ${active.turnId}`, "THREAD_BUSY", {
@@ -477,6 +508,8 @@ async function sendOrchestratorMessage(client, flags, input) {
477
508
  const requestId = requireString(flags, "request-id");
478
509
  const turnId = deterministicTurnId(input.threadId, requestId);
479
510
  const relayRequestId = deterministicRelayRequestId(input.threadId, requestId);
511
+ const requestedPermissionMode = parsePermissionMode(flags["permission-mode"] ?? flags.permission);
512
+ const activePermissionMode = effectivePermissionMode(requestedPermissionMode);
480
513
  const model = parseRuntimeSelection(flags.model, "model");
481
514
  const thoughtLevel = parseThoughtLevel(flags);
482
515
  const selection = selectionRecordFields({ model, thoughtLevel });
@@ -495,6 +528,8 @@ async function sendOrchestratorMessage(client, flags, input) {
495
528
  const { session } = await readThreadContext(client, input.threadId);
496
529
  const metadata = await client.decodeSessionMetadata(session);
497
530
  const existingRequest = readOrchestratorMetadata(metadata).requests?.[requestId];
531
+ let existingRequestContext;
532
+ let existingRequestText;
498
533
  const earlyContextData = input.contextTurn ? {
499
534
  context: {
500
535
  sourceThreadId: input.contextTurn.sourceThreadId,
@@ -503,7 +538,7 @@ async function sendOrchestratorMessage(client, flags, input) {
503
538
  },
504
539
  } : {};
505
540
  if (existingRequest) {
506
- if (!sameBaseRequestShape(existingRequest, { turnId, inputSha256, ...selection }))
541
+ if (!sameBaseRequestShape(existingRequest, { turnId, inputSha256, ...selection, permissionMode: activePermissionMode }))
507
542
  throw requestConflict(input.threadId, requestId, turnId);
508
543
  const existingTurn = await readExactTurn(client, input.threadId, turnId, metadata);
509
544
  let context;
@@ -517,8 +552,22 @@ async function sendOrchestratorMessage(client, flags, input) {
517
552
  resolvedEffectivePrompt = false;
518
553
  }
519
554
  }
555
+ if (!resolvedEffectivePrompt) {
556
+ ok(input.responseType, {
557
+ threadId: input.threadId,
558
+ sessionId: input.threadId,
559
+ turnId,
560
+ requestId,
561
+ status: "inProgress",
562
+ rawStatus: existingTurn?.status ?? "pending",
563
+ ...permissionEvidence(requestedPermissionMode, existingRequest),
564
+ ...earlyContextData,
565
+ }, { requestId, sessionId: input.threadId, turnId });
566
+ return;
567
+ }
520
568
  if (resolvedEffectivePrompt) {
521
569
  const effectivePrompt = context?.prompt ?? input.instruction;
570
+ existingRequestText = effectivePrompt;
522
571
  if (!sameEffectiveRequestShape(existingRequest, {
523
572
  promptSha256: promptSha256(effectivePrompt),
524
573
  ...(context?.sourceOutputSha256 ? { sourceOutputSha256: context.sourceOutputSha256 } : {}),
@@ -541,6 +590,7 @@ async function sendOrchestratorMessage(client, flags, input) {
541
590
  latest: output.latestPath,
542
591
  metadata: output.metadataPath,
543
592
  },
593
+ ...permissionEvidence(requestedPermissionMode, existingRequest),
544
594
  ...(context ? {
545
595
  context: {
546
596
  sourceThreadId: context.source.threadId,
@@ -554,21 +604,25 @@ async function sendOrchestratorMessage(client, flags, input) {
554
604
  }, { requestId, sessionId: input.threadId, turnId });
555
605
  return;
556
606
  }
557
- ok(input.responseType, {
558
- threadId: input.threadId,
559
- sessionId: input.threadId,
560
- turnId,
561
- requestId,
562
- status: "inProgress",
563
- rawStatus: existingTurn?.status ?? "pending",
564
- ...earlyContextData,
565
- }, { requestId, sessionId: input.threadId, turnId });
566
- return;
607
+ if (existingTurn?.status === "running") {
608
+ ok(input.responseType, {
609
+ threadId: input.threadId,
610
+ sessionId: input.threadId,
611
+ turnId,
612
+ requestId,
613
+ status: "inProgress",
614
+ rawStatus: existingTurn.status,
615
+ ...permissionEvidence(requestedPermissionMode, existingRequest),
616
+ ...earlyContextData,
617
+ }, { requestId, sessionId: input.threadId, turnId });
618
+ return;
619
+ }
620
+ existingRequestContext = context;
567
621
  }
568
- const context = input.contextTurn
622
+ const context = existingRequestContext ?? (input.contextTurn
569
623
  ? await buildContextPrompt(client, input.contextTurn, input.instruction, includeContextOutput)
570
- : undefined;
571
- const text = context?.prompt ?? input.instruction;
624
+ : undefined);
625
+ const text = existingRequestText ?? context?.prompt ?? input.instruction;
572
626
  const hash = promptSha256(text);
573
627
  const record = {
574
628
  turnId,
@@ -576,6 +630,7 @@ async function sendOrchestratorMessage(client, flags, input) {
576
630
  inputSha256,
577
631
  ...(context?.sourceOutputSha256 ? { sourceOutputSha256: context.sourceOutputSha256 } : {}),
578
632
  ...selection,
633
+ permissionMode: activePermissionMode,
579
634
  createdAt: Date.now(),
580
635
  };
581
636
  const contextData = context ? {
@@ -593,32 +648,21 @@ async function sendOrchestratorMessage(client, flags, input) {
593
648
  const existingTurn = await readExactTurn(client, input.threadId, turnId, metadata);
594
649
  if (existingTurn?.prompt && promptSha256(existingTurn.prompt) !== hash)
595
650
  throw requestConflict(input.threadId, requestId, turnId);
596
- if (existingRequest && !existingTurn) {
597
- ok(input.responseType, {
598
- threadId: input.threadId,
599
- sessionId: input.threadId,
600
- turnId,
601
- requestId,
602
- status: "inProgress",
603
- rawStatus: "pending",
604
- ...contextData,
605
- }, { requestId, sessionId: input.threadId, turnId });
606
- return;
607
- }
608
651
  const active = turns[0]?.status === "running" ? turns[0] : undefined;
609
652
  const latestCompleted = turns.find((turn) => turn.status === "completed");
610
653
  if (active && active.turnId !== turnId)
611
654
  throw threadBusy(input.threadId, active, latestCompleted);
612
655
  if (existingTurn && existingTurn.status !== "running") {
613
656
  const output = await rebuildOutputForTurn(client, input.threadId, existingTurn);
614
- await persistRequestRecordBestEffort(client, input.threadId, requestId, requestRecordForExistingTurn(existingTurn, hash, selection, inputSha256, context?.sourceOutputSha256, Date.now()) ?? {
657
+ const existingTurnRecord = requestRecordForExistingTurn(existingTurn, hash, selection, undefined, inputSha256, context?.sourceOutputSha256, Date.now()) ?? {
615
658
  turnId,
616
659
  promptSha256: hash,
617
660
  inputSha256,
618
661
  ...(context?.sourceOutputSha256 ? { sourceOutputSha256: context.sourceOutputSha256 } : {}),
619
662
  ...selection,
620
663
  createdAt: Date.now(),
621
- });
664
+ };
665
+ await persistRequestRecord(client, input.threadId, requestId, existingTurnRecord);
622
666
  ok(input.responseType, {
623
667
  threadId: input.threadId,
624
668
  sessionId: input.threadId,
@@ -632,11 +676,13 @@ async function sendOrchestratorMessage(client, flags, input) {
632
676
  latest: output.latestPath,
633
677
  metadata: output.metadataPath,
634
678
  },
679
+ ...permissionEvidence(requestedPermissionMode, existingTurnRecord),
635
680
  ...contextData,
636
681
  }, { requestId, sessionId: input.threadId, turnId });
637
682
  return;
638
683
  }
639
684
  if (existingTurn?.status === "running") {
685
+ const runningTurnRecord = requestRecordForExistingTurn(existingTurn, hash, selection, undefined, inputSha256, context?.sourceOutputSha256, Date.now());
640
686
  ok(input.responseType, {
641
687
  threadId: input.threadId,
642
688
  sessionId: input.threadId,
@@ -644,16 +690,19 @@ async function sendOrchestratorMessage(client, flags, input) {
644
690
  requestId,
645
691
  status: "inProgress",
646
692
  rawStatus: existingTurn?.status ?? "running",
693
+ ...permissionEvidence(requestedPermissionMode, runningTurnRecord),
647
694
  ...contextData,
648
695
  }, { requestId, sessionId: input.threadId, turnId });
649
696
  return;
650
697
  }
651
- const permissionMode = parsePermissionMode(flags["permission-mode"] ?? flags.permission);
652
698
  const runtimeTimeoutMs = parseDurationMs(flags["runtime-timeout"], DEFAULT_COMMAND_TIMEOUT_MS);
699
+ const storedRecord = existingRequest ?? record;
700
+ if (!existingRequest)
701
+ await persistRequestRecord(client, input.threadId, requestId, record);
653
702
  try {
654
703
  await client.run(input.threadId, text, {
655
704
  wait: false,
656
- permissionMode,
705
+ permissionMode: requestedPermissionMode,
657
706
  requestId: relayRequestId,
658
707
  turnId,
659
708
  promptHash: hash,
@@ -679,7 +728,6 @@ async function sendOrchestratorMessage(client, flags, input) {
679
728
  }
680
729
  throw error;
681
730
  }
682
- await persistRequestRecordBestEffort(client, input.threadId, requestId, record);
683
731
  ok(input.responseType, {
684
732
  threadId: input.threadId,
685
733
  sessionId: input.threadId,
@@ -687,6 +735,7 @@ async function sendOrchestratorMessage(client, flags, input) {
687
735
  requestId,
688
736
  status: "inProgress",
689
737
  rawStatus: "running",
738
+ ...permissionEvidence(requestedPermissionMode, storedRecord),
690
739
  ...contextData,
691
740
  }, { requestId, sessionId: input.threadId, turnId });
692
741
  }
@@ -835,7 +884,10 @@ export async function handleOrchestrator({ domain, action, positional, flags })
835
884
  const metadata = await client.decodeSessionMetadata(session);
836
885
  let updated;
837
886
  try {
838
- updated = await client.updateSessionMetadata(threadId, withOrchestratorTags(metadata, tags), { expectedUpdatedAt: session.updatedAt });
887
+ updated = await client.updateSessionMetadata(threadId, withOrchestratorTags(metadata, tags), {
888
+ expectedEncryptedMetadata: session.encryptedMetadata ?? null,
889
+ expectedUpdatedAt: session.updatedAt,
890
+ });
839
891
  }
840
892
  catch (error) {
841
893
  if (errorCode(error) !== "SESSION_METADATA_CONFLICT")
@@ -188,12 +188,27 @@ function numberMetadata(metadata, key) {
188
188
  function isRecord(value) {
189
189
  return typeof value === "object" && value !== null && !Array.isArray(value);
190
190
  }
191
+ function diagnosticPermission(metadata) {
192
+ const metadataPermissionMode = stringMetadata(metadata, "permissionMode");
193
+ if (metadataPermissionMode === "approve-all" || metadataPermissionMode === "approve-reads" || metadataPermissionMode === "deny-all") {
194
+ return {
195
+ effectivePermissionMode: metadataPermissionMode,
196
+ permissionSource: "session-metadata",
197
+ metadataPermissionMode,
198
+ };
199
+ }
200
+ return {
201
+ effectivePermissionMode: "approve-all",
202
+ permissionSource: "product-default",
203
+ };
204
+ }
191
205
  function diagnosticSessionMetadata(metadata) {
192
206
  const runtime = isRecord(metadata.runtime) ? metadata.runtime : {};
193
207
  return {
194
208
  agent: stringMetadata(metadata, "agent"),
195
209
  cwd: stringMetadata(metadata, "cwd"),
196
210
  name: stringMetadata(metadata, "name"),
211
+ permissionMode: stringMetadata(metadata, "permissionMode"),
197
212
  providerName: stringMetadata(metadata, "providerName"),
198
213
  importedFromRuntimeSessionId: stringMetadata(metadata, "importedFromRuntimeSessionId"),
199
214
  runtime: {
@@ -510,6 +525,7 @@ export async function handleSession({ domain, action, positional, flags }) {
510
525
  : undefined;
511
526
  if (repair)
512
527
  session = repair.session;
528
+ const permission = diagnosticPermission(metadata);
513
529
  const data = {
514
530
  generatedAt: new Date().toISOString(),
515
531
  session: {
@@ -557,6 +573,9 @@ export async function handleSession({ domain, action, positional, flags }) {
557
573
  },
558
574
  orchestration,
559
575
  diagnosis: {
576
+ effectivePermissionMode: permission.effectivePermissionMode,
577
+ permissionSource: permission.permissionSource,
578
+ metadataPermissionMode: permission.metadataPermissionMode,
560
579
  importedRuntimeSessionId: stringMetadata(metadata, "importedFromRuntimeSessionId"),
561
580
  latestPageEmpty: latestPage.events.length === 0,
562
581
  latestPageHasVisibleTranscript: latestPage.events.some(isVisibleTranscriptPayloadEvent),
@@ -598,6 +617,7 @@ export async function handleSession({ domain, action, positional, flags }) {
598
617
  console.log(`Machine: ${session.machineId}${machine ? ` ${machine.online ? "online" : "offline"}` : ""}`);
599
618
  const runtime = isRecord(metadata.runtime) ? metadata.runtime : {};
600
619
  console.log(`Runtime: ${stringMetadata(metadata, "importedFromRuntimeSessionId") ?? stringMetadata(runtime, "backendSessionId") ?? stringMetadata(runtime, "agentSessionId") ?? "-"}`);
620
+ console.log(`Permission: ${data.diagnosis.effectivePermissionMode} source=${data.diagnosis.permissionSource}`);
601
621
  console.log(`Backfill: ${data.diagnosis.backfillStatus ?? "-"} available=${data.diagnosis.backfillAvailableEvents ?? "-"} emitted=${data.diagnosis.backfillEmittedEvents ?? "-"}`);
602
622
  console.log(`Head: ${session.currentHead ?? "-"} lastTurn=${session.lastTurnId ?? "-"} stale=${data.diagnosis.staleHeadSuspected ? "yes" : "no"}`);
603
623
  console.log(`Latest page: ${latestPage.events.length} events, visible=${latestPage.events.filter(isVisibleTranscriptPayloadEvent).length}, hasMore=${latestPage.hasMoreBefore}`);
@@ -618,6 +638,7 @@ export async function handleSession({ domain, action, positional, flags }) {
618
638
  throw new CliError("Cannot repair binding for a running session", "SESSION_BINDING_REPAIR_RUNNING");
619
639
  const metadata = await client.decodeSessionMetadata(session);
620
640
  const { candidate, candidates } = await resolveRuntimeBindingRepairTarget(client, session, metadata, flags);
641
+ const permissionMode = parsePermissionMode(flags["permission-mode"] ?? flags.permission);
621
642
  const name = typeof flags.name === "string" && flags.name.trim()
622
643
  ? flags.name.trim()
623
644
  : stringMetadata(metadata, "name") ?? session.name ?? candidate.name ?? candidate.summary ?? candidate.runtimeSessionId;
@@ -627,6 +648,7 @@ export async function handleSession({ domain, action, positional, flags }) {
627
648
  cwd: candidate.cwd,
628
649
  runtimeSessionId: candidate.runtimeSessionId,
629
650
  name,
651
+ permissionMode,
630
652
  summary: candidate.summary,
631
653
  runtimeState: candidate.runtimeState,
632
654
  ...(candidate.name ? { providerName: candidate.name } : {}),
@@ -729,12 +751,14 @@ export async function handleSession({ domain, action, positional, flags }) {
729
751
  const name = typeof flags.name === "string" && flags.name.trim()
730
752
  ? flags.name.trim()
731
753
  : historical.name?.trim() || historical.summary?.trim() || historical.runtimeSessionName?.trim() || runtimeSessionId;
754
+ const permissionMode = parsePermissionMode(flags["permission-mode"] ?? flags.permission);
732
755
  const continued = await client.importHistoricalSession({
733
756
  machineId,
734
757
  agent: historical.agent,
735
758
  cwd: historical.cwd,
736
759
  runtimeSessionId,
737
760
  name,
761
+ permissionMode,
738
762
  summary: historical.summary,
739
763
  runtimeState: historical.runtimeState,
740
764
  ...(historical.name ? { providerName: historical.name } : {}),
@@ -53,6 +53,7 @@ async function cliVersion() {
53
53
  async function addCliEnvironmentChecks(checks, warnings) {
54
54
  const version = await cliVersion();
55
55
  checks.push({ name: "cli-version", status: "ok", detail: version });
56
+ checks.push({ name: "default-permission-mode", status: "ok", detail: "approve-all (trusted-local Yolo)" });
56
57
  const executable = process.argv[1] ? path.resolve(process.argv[1]) : process.execPath;
57
58
  const workspaceCli = path.resolve(process.cwd(), "apps", "cli", "dist", "index.js");
58
59
  try {
@@ -1,7 +1,7 @@
1
1
  export const ORCHESTRATOR_SKILL_NAME = "happy-elves-orchestrator";
2
2
  const COMPUTER_USE_SKILL_NAME = "happy-elves-computer-use";
3
- const ORCHESTRATOR_SKILL_CONTRACT_VERSION = 2;
4
- const COMPUTER_USE_SKILL_CONTRACT_VERSION = 2;
3
+ const ORCHESTRATOR_SKILL_CONTRACT_VERSION = 3;
4
+ const COMPUTER_USE_SKILL_CONTRACT_VERSION = 3;
5
5
  function renderOrchestratorSkill(input) {
6
6
  return `---
7
7
  name: ${input.skillName}
@@ -32,8 +32,10 @@ Important rules:
32
32
  - First run \`happy-elves skill doctor --name happy-elves-orchestrator --json\` and verify the \`cli-executable\`, \`cli-version\`, \`workspace-cli\`, and \`git-commit-capability\` checks. If git commit capability is warning or a commit fails, report the blocker; do not claim staged-only changes are complete.
33
33
  - \`threadId\` is the same value as Happy Elves \`sessionId\`.
34
34
  - Use stable request ids such as \`orch_round_3_worker_fix\`.
35
- - Reusing the same request id with the same prompt/model/thinking returns the same turn.
36
- - Reusing the same request id with different prompt text, model, or thinking returns \`REQUEST_ID_CONFLICT\`.
35
+ - Reusing the same request id with the same prompt/model/thinking/permission returns the same turn.
36
+ - Reusing the same request id with different prompt text, model, thinking, or permission returns \`REQUEST_ID_CONFLICT\`.
37
+ - Happy Elves defaults to trusted-local YOLO execution: if \`send-message\` omits \`--permission-mode\`, the effective permission is \`approve-all\`. Pass \`--permission-mode default\`, \`ask\`, \`accept-edits\`, \`approve-reads\`, \`deny-all\`, or \`read-only\` only when you intentionally want to tighten one turn.
38
+ - Read \`effectivePermissionMode\` and \`permissionSource\` in \`send-message\` / \`handoff\` JSON before diagnosing sandbox, git, desktop, or backend permission failures.
37
39
  - To override runtime selection, pass \`--model <key>\` and \`--thought-level <key>\`; \`--thinking <key>\` is an alias. Use \`default\` to return to provider defaults.
38
40
  - Runtime failures such as \`CODEX_EMPTY_COMPLETION\` are terminal failed turns; inspect \`wait-thread --turn\` or \`turn result\` for the full error text.
39
41
  - If \`send-message\` returns \`THREAD_BUSY\`, wait/read the active turn instead of sending another prompt.
@@ -158,7 +160,7 @@ open-computer-use call --calls '[
158
160
 
159
161
  Always call \`get_app_state\` before using an \`element_index\`. Prefer semantic element actions such as \`click\`, \`set_value\`, and \`type_text\` over raw coordinates.
160
162
 
161
- When a Happy Elves orchestrator asks a Codex-backed session to use this skill, the turn must be sent with \`--permission-mode approve-all\`. Creating the session with elevated permissions is not enough; later turns can otherwise fall back to a stricter permission mode and decline backend app-agent probes.
163
+ Happy Elves orchestration defaults to \`approve-all\` for trusted local sessions. Do not add \`--permission-mode approve-all\` just for this skill; only pass a permission mode when intentionally tightening or debugging one turn. If desktop probes fail, check the orchestrator response \`effectivePermissionMode\` before blaming macOS, Open Computer Use, or app permissions.
162
164
 
163
165
  ## Background-Only Mode
164
166
  Use background-only mode when the user wants to keep using the computer while desktop dogfood runs. This mode is macOS-first; Windows and Linux support is best-effort and may require foreground desktop access.
@@ -252,6 +254,9 @@ const builtinSkills = [
252
254
  "keep this orchestrator turn open",
253
255
  "Strict handoff dogfood",
254
256
  "git-commit-capability",
257
+ "effectivePermissionMode",
258
+ "trusted-local YOLO",
259
+ "prompt/model/thinking/permission",
255
260
  "pnpm dogfood:orchestrator-real",
256
261
  "do not ask the target agent to read an intermediate markdown file",
257
262
  ],
@@ -268,7 +273,7 @@ const builtinSkills = [
268
273
  "OPEN_COMPUTER_USE_ALLOW_GLOBAL_POINTER_FALLBACKS",
269
274
  "pnpm dogfood:computer-use-background",
270
275
  "HAPPY_ELVES_CU_GATEWAY_CONFIRM=1",
271
- "--permission-mode approve-all",
276
+ "effectivePermissionMode",
272
277
  "Live Feishu Gateway Dogfood",
273
278
  "FOREGROUND_STOLEN",
274
279
  "POINTER_MOVED",
@@ -271,6 +271,7 @@ async function loadGatewaySession(ws, config, gateway, binding, input) {
271
271
  agent,
272
272
  mode: "persistent",
273
273
  cwd,
274
+ permissionMode: "approve-all",
274
275
  });
275
276
  const session = {
276
277
  sessionId: binding.sessionId,