@ravefox/space 0.2.1 → 0.3.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.
Files changed (163) hide show
  1. package/LICENSE +21 -205
  2. package/README.md +80 -31
  3. package/bin/space.js +106 -0
  4. package/lib/agents.js +119 -0
  5. package/lib/credentials.js +95 -0
  6. package/lib/daemon.js +294 -0
  7. package/lib/harness.js +166 -0
  8. package/lib/http.js +101 -0
  9. package/lib/login.js +68 -0
  10. package/lib/mcp.js +34 -0
  11. package/lib/runtime.js +241 -0
  12. package/lib/store.js +96 -0
  13. package/lib/tools.js +93 -0
  14. package/package.json +27 -46
  15. package/NOTICE +0 -55
  16. package/assets/webhook-sdk/python/bot.py +0 -63
  17. package/assets/webhook-sdk/python/commonly.py +0 -232
  18. package/node_modules/@commonlyai/seats/README.md +0 -25
  19. package/node_modules/@commonlyai/seats/package.json +0 -22
  20. package/node_modules/@commonlyai/seats/src/adapters/hermes.js +0 -264
  21. package/node_modules/@commonlyai/seats/src/adapters/stub.js +0 -24
  22. package/node_modules/@commonlyai/seats/src/contract.js +0 -48
  23. package/node_modules/@commonlyai/seats/src/failures.js +0 -44
  24. package/node_modules/@commonlyai/seats/src/harness-plan.js +0 -329
  25. package/node_modules/@commonlyai/seats/src/index.js +0 -22
  26. package/node_modules/@commonlyai/seats/src/process-group.js +0 -269
  27. package/node_modules/@commonlyai/seats/src/record/format.js +0 -737
  28. package/node_modules/@commonlyai/seats/src/record/otel.js +0 -238
  29. package/node_modules/@commonlyai/seats/src/runtime-observation.js +0 -29
  30. package/node_modules/@commonlyai/seats/src/seat-model.js +0 -17
  31. package/node_modules/@commonlyai/seats/src/supervisor.js +0 -308
  32. package/node_modules/@commonlyai/seats/src/trace/events.js +0 -46
  33. package/node_modules/@commonlyai/seats/src/trace/store.js +0 -276
  34. package/node_modules/@commonlyai/seats/src/turn-processes.js +0 -226
  35. package/node_modules/@commonlyai/seats/src/upstream-refusal.js +0 -418
  36. package/node_modules/@commonlyai/seats/src/valet/env.js +0 -201
  37. package/node_modules/@commonlyai/seats/src/valet/grant.js +0 -123
  38. package/node_modules/@commonlyai/seats/src/valet/permissions.js +0 -213
  39. package/node_modules/@commonlyai/seats/src/wire/provider-proxy.js +0 -529
  40. package/skills/commonly/SKILL.md +0 -233
  41. package/src/commands/agent.js +0 -6334
  42. package/src/commands/daemon.js +0 -933
  43. package/src/commands/dev.js +0 -153
  44. package/src/commands/doctor.js +0 -141
  45. package/src/commands/inbox.js +0 -330
  46. package/src/commands/login.js +0 -194
  47. package/src/commands/mcp.js +0 -56
  48. package/src/commands/pod.js +0 -120
  49. package/src/commands/task.js +0 -279
  50. package/src/commands/verify.js +0 -48
  51. package/src/index.js +0 -99
  52. package/src/lib/adapter-login.js +0 -76
  53. package/src/lib/adapters/broker-bridge-entry.js +0 -76
  54. package/src/lib/adapters/claude.js +0 -2207
  55. package/src/lib/adapters/codex-broker-bridge.mjs +0 -298
  56. package/src/lib/adapters/codex.js +0 -1898
  57. package/src/lib/adapters/dsh.js +0 -513
  58. package/src/lib/adapters/goose.js +0 -510
  59. package/src/lib/adapters/hermes.js +0 -18
  60. package/src/lib/adapters/index.js +0 -68
  61. package/src/lib/adapters/opencode.js +0 -1681
  62. package/src/lib/adapters/pi-commonly-mcp.mjs +0 -71
  63. package/src/lib/adapters/pi-mcp-client.mjs +0 -617
  64. package/src/lib/adapters/pi-shell-env.mjs +0 -41
  65. package/src/lib/adapters/pi.js +0 -1025
  66. package/src/lib/adapters/qwen.js +0 -523
  67. package/src/lib/adapters/seat-model.js +0 -8
  68. package/src/lib/adapters/stub.js +0 -1
  69. package/src/lib/adapters/tested-version.js +0 -106
  70. package/src/lib/api.js +0 -205
  71. package/src/lib/claim-outcome.js +0 -128
  72. package/src/lib/claimed-work-gate.js +0 -73
  73. package/src/lib/cli-hint.js +0 -33
  74. package/src/lib/commit-attribution.js +0 -391
  75. package/src/lib/config.js +0 -155
  76. package/src/lib/content-refusal.js +0 -94
  77. package/src/lib/credential-file.js +0 -208
  78. package/src/lib/daemon-agent-states.js +0 -95
  79. package/src/lib/daemon-hint.js +0 -18
  80. package/src/lib/daemon-logs.js +0 -43
  81. package/src/lib/daemon-service-windows.js +0 -263
  82. package/src/lib/daemon-service.js +0 -486
  83. package/src/lib/daemon-state.js +0 -84
  84. package/src/lib/daemon-store.js +0 -119
  85. package/src/lib/daemon-supervisor.js +0 -629
  86. package/src/lib/daemon-watch.js +0 -115
  87. package/src/lib/declared-mcp-guard.js +0 -284
  88. package/src/lib/default-environment.js +0 -207
  89. package/src/lib/device-login.js +0 -126
  90. package/src/lib/digest-frame.js +0 -151
  91. package/src/lib/doctor.js +0 -291
  92. package/src/lib/egress-proxy.js +0 -160
  93. package/src/lib/enforcement.js +0 -946
  94. package/src/lib/environment.js +0 -681
  95. package/src/lib/first-output-watchdog.js +0 -191
  96. package/src/lib/git-readonly-server.js +0 -138
  97. package/src/lib/git-readonly.js +0 -463
  98. package/src/lib/git-remote-commonly.mjs +0 -100
  99. package/src/lib/grant-broker-guard.js +0 -345
  100. package/src/lib/hooks-config.js +0 -238
  101. package/src/lib/host-gh-broker.js +0 -152
  102. package/src/lib/host-gh-shim.mjs +0 -98
  103. package/src/lib/inbox.js +0 -483
  104. package/src/lib/instance-proxy.js +0 -76
  105. package/src/lib/machine-host.js +0 -29
  106. package/src/lib/mcp-credential-delivery.js +0 -121
  107. package/src/lib/mcp-first-run.js +0 -50
  108. package/src/lib/mcp-home.js +0 -644
  109. package/src/lib/mcp-ready.js +0 -68
  110. package/src/lib/mcp-server-version.js +0 -123
  111. package/src/lib/mcp-token.js +0 -36
  112. package/src/lib/mcp-warm-child.mjs +0 -412
  113. package/src/lib/memory-bridge.js +0 -191
  114. package/src/lib/memory-import.js +0 -156
  115. package/src/lib/missing-harness.js +0 -31
  116. package/src/lib/model-unavailable.js +0 -154
  117. package/src/lib/native-runtime-inventory.js +0 -40
  118. package/src/lib/outsider-grant-mcp.js +0 -82
  119. package/src/lib/own-key-launch.js +0 -42
  120. package/src/lib/pod-catch-up.js +0 -55
  121. package/src/lib/pod-focus.js +0 -224
  122. package/src/lib/pod-tail.js +0 -68
  123. package/src/lib/poll-liveness.js +0 -103
  124. package/src/lib/poll-retry.js +0 -81
  125. package/src/lib/poller.js +0 -200
  126. package/src/lib/provider-proxy.js +0 -42
  127. package/src/lib/public-seat-mcp-flag.js +0 -36
  128. package/src/lib/public-tool-error.js +0 -60
  129. package/src/lib/public-withheld-tools.js +0 -48
  130. package/src/lib/reply-receipt.js +0 -55
  131. package/src/lib/resolve-on-path.js +0 -22
  132. package/src/lib/runtime-inventory.js +0 -56
  133. package/src/lib/runtime-observation.js +0 -37
  134. package/src/lib/sandbox/bwrap.js +0 -275
  135. package/src/lib/sandbox/codex-perimeter.js +0 -16
  136. package/src/lib/sandbox/host-check.js +0 -268
  137. package/src/lib/sandbox/mode.js +0 -49
  138. package/src/lib/sandbox/opencode-loopback.mjs +0 -68
  139. package/src/lib/sandbox/seatbelt.js +0 -684
  140. package/src/lib/sandbox/turn-dir.js +0 -56
  141. package/src/lib/sandbox/windows-unsandboxed.js +0 -183
  142. package/src/lib/sandbox/workspace-git-config.js +0 -243
  143. package/src/lib/sandbox/workspace-pin.js +0 -158
  144. package/src/lib/seat-runtime-controls.js +0 -61
  145. package/src/lib/seat-stop.js +0 -97
  146. package/src/lib/server-skills.js +0 -32
  147. package/src/lib/session-recycle.js +0 -219
  148. package/src/lib/session-store.js +0 -215
  149. package/src/lib/skill-prompt.js +0 -74
  150. package/src/lib/skills-import.js +0 -113
  151. package/src/lib/spawn-credential.js +0 -402
  152. package/src/lib/spawn-retry.js +0 -311
  153. package/src/lib/stable-install.js +0 -176
  154. package/src/lib/supervised-turn.js +0 -352
  155. package/src/lib/task.js +0 -235
  156. package/src/lib/turn-failure-notice.js +0 -177
  157. package/src/lib/unconfined-seat-guard.js +0 -94
  158. package/src/lib/update-check.js +0 -192
  159. package/src/lib/upstream-refusal.js +0 -1
  160. package/src/lib/webhook-server.js +0 -109
  161. package/src/lib/windows-command.js +0 -393
  162. package/src/lib/workspace-git.js +0 -87
  163. package/src/lib/workspace-trust.js +0 -162
@@ -1,352 +0,0 @@
1
- /**
2
- * One model turn of `commonly agent run`, on one of two paths. The supervised
3
- * path also writes one metadata line per turn (the turn log), once the
4
- * turn's outcome has reached the pod or failed to.
5
- *
6
- * codex by default, or COMMONLY_SUPERVISE_RUNS=1 (superviseRunsByDefault in
7
- * commands/agent.js; COMMONLY_SUPERVISE_RUNS=0 opts codex out)
8
- * the turn runs under `superviseRun` from @commonlyai/seats: a stall
9
- * window, a wall-clock backstop, and a guarantee the turn settles with a
10
- * named failure. No provider relay: the CLI talks to its provider directly,
11
- * exactly as on the plain path.
12
- * anything else
13
- * the plain path: `adapter.spawn(prompt, ctx)` with the caller's ctx object
14
- * untouched, and nothing written: the flag-off run loop is main's.
15
- *
16
- * A turn is ok only when its outcome reached the pod: the reply was delivered
17
- * or the agent chose silence. The run loop names the outcome after delivery
18
- * settles (turn.finish); a spawn that threw is `failed` at once.
19
- *
20
- * NO RECORD WRITER. The supervisor needs a trace store. The one here
21
- * (createMetadataStore) is in memory, lives for one turn, and keeps event type
22
- * names, times and the recovery action word. It drops every event's data, so
23
- * no agent text, prompt, reply, failure detail or credential is kept, and
24
- * nothing is written to disk (a redaction review on #2701).
25
- *
26
- * Metrics: ~/.commonly/state/turns/<seat>.jsonl (COMMONLY_TURN_LOG_DIR
27
- * overrides the directory). Whitelisted fields only; see metricLine.
28
- */
29
- import { appendFileSync, mkdirSync } from 'fs';
30
- import { homedir } from 'os';
31
- import { dirname, join } from 'path';
32
- import { superviseRun as defaultSuperviseRun } from '@commonlyai/seats/supervisor';
33
- import { isEventType } from '@commonlyai/seats/trace/events';
34
- import { failureKindOf } from '@commonlyai/seats/failures';
35
- import { classifySpawnFailure } from './spawn-retry.js';
36
-
37
- const MAX_TIMER_MS = 2_147_483_647;
38
-
39
- /**
40
- * Stall windows per adapter, in ms; 0 is off. Unlisted adapters: off.
41
- *
42
- * codex: `codex exec --json` writes a JSONL event per item, so its stdout is a
43
- * real progress signal, and the quiet stretch is the longest single item (a
44
- * reasoning step or one shell command). Measured 2026-10-25 from per-turn
45
- * timestamp gaps in October codex rollouts on one host (~/.codex/sessions):
46
- * 9,703 turns, max quiet gap 162 s, p99 53 s; 9 turns would have tripped the
47
- * package default of 120 s. 15 min is 5.5x the worst gap seen and still
48
- * 30 min inside the seats' 45 min turn budget.
49
- * claude: the adapter runs `claude -p --output-format text`, which writes
50
- * nothing to stdout until the reply is done. A healthy 14-minute claude turn
51
- * is 14 minutes of silence to the supervisor, so any window
52
- * under the turn budget would be a turn-length cap, not a liveness check.
53
- * Off; claude is guarded by the wall-clock backstop and the cancel ladder.
54
- */
55
- export const STALL_DEFAULTS_MS = Object.freeze({ codex: 15 * 60 * 1000, claude: 0 });
56
- // How long after the supervisor's abort before it SIGKILLs the child's tree
57
- // (process-group.js). The claude and codex adapters do not act on the abort,
58
- // so on a stall the child is alive at the kill and its whole tree goes with it:
59
- // a SIGTERM to the CLI alone would let it exit first and orphan the MCP
60
- // servers it started in groups of their own, which the tree kill then skips.
61
- // An adapter that honours ctx.signal gets this long to settle.
62
- export const CANCEL_GRACE_MS = 15_000;
63
- // The wall clock sits past the adapter's own timeout, so the adapter's timeout
64
- // (the same SIGTERM and error as on the plain path) fires first; the
65
- // supervisor's only settles a child that outlived it.
66
- export const WALL_BACKSTOP_MS = 60_000;
67
- // After a failed turn, how long to wait for the child's exit status before
68
- // writing the metric line anyway (the kill lands just before settle).
69
- const EXIT_WAIT_MS = 3_000;
70
-
71
- const envName = (prefix, adapterName) => `${prefix}_${String(adapterName || '').toUpperCase().replace(/[^A-Z0-9]/g, '_')}`;
72
-
73
- const envMs = (env, name, warnings) => {
74
- const raw = env?.[name];
75
- if (raw === undefined || raw === '') return undefined;
76
- const value = Number(raw);
77
- if (!Number.isInteger(value) || value < 0) {
78
- warnings.push(`${name} ignored (want a non-negative integer of ms)`);
79
- return undefined;
80
- }
81
- return Math.min(value, MAX_TIMER_MS);
82
- };
83
-
84
- /**
85
- * The supervisor policy for one adapter's turn. Env overrides, most specific
86
- * first: COMMONLY_SUPERVISE_STALL_MS_<ADAPTER>, COMMONLY_SUPERVISE_STALL_MS,
87
- * COMMONLY_SUPERVISE_CANCEL_GRACE_MS, COMMONLY_SUPERVISE_WALL_BACKSTOP_MS.
88
- * 0 turns a window off. No retries: the run loop keeps those.
89
- */
90
- export const supervisedRunPolicy = (turnTimeoutMs, { adapterName = null, env = process.env, warnings = [] } = {}) => {
91
- const stallMs = envMs(env, envName('COMMONLY_SUPERVISE_STALL_MS', adapterName), warnings)
92
- ?? envMs(env, 'COMMONLY_SUPERVISE_STALL_MS', warnings)
93
- ?? STALL_DEFAULTS_MS[adapterName] ?? 0;
94
- const cancelGraceMs = envMs(env, 'COMMONLY_SUPERVISE_CANCEL_GRACE_MS', warnings) ?? CANCEL_GRACE_MS;
95
- const backstopMs = envMs(env, 'COMMONLY_SUPERVISE_WALL_BACKSTOP_MS', warnings) ?? WALL_BACKSTOP_MS;
96
- return {
97
- stallMs,
98
- cancelGraceMs,
99
- timeoutMs: Math.min(Number(turnTimeoutMs) + backstopMs, MAX_TIMER_MS),
100
- };
101
- };
102
-
103
- /**
104
- * A trace store that keeps no agent text and writes nothing. superviseRun
105
- * calls startRun once per turn; each event keeps { type, at, action? }.
106
- */
107
- export const createMetadataStore = ({ now = Date.now } = {}) => {
108
- let current = null;
109
- let seq = 0;
110
- return {
111
- startRun({ runId } = {}) {
112
- seq += 1;
113
- const events = [];
114
- let settled = false;
115
- current = {
116
- runId: runId || `turn-${now().toString(36)}-${seq}`,
117
- emit(type, data) {
118
- if (!isEventType(type)) throw new Error(`unknown trace event type: ${type}`);
119
- if (settled) return null;
120
- const entry = { type, at: now() };
121
- if (type === 'liveness.recovery' && typeof data?.action === 'string') entry.action = data.action;
122
- events.push(entry);
123
- if (type === 'run.settled') settled = true;
124
- return entry;
125
- },
126
- get settled() { return settled; },
127
- events: () => events.map((entry) => ({ ...entry })),
128
- };
129
- return current;
130
- },
131
- lastRun: () => current,
132
- };
133
- };
134
-
135
- export const metricsPathFor = (seat, env = process.env) => {
136
- const dir = env?.COMMONLY_TURN_LOG_DIR || join(homedir(), '.commonly', 'state', 'turns');
137
- const safe = String(seat || 'unknown').replace(/[^A-Za-z0-9_.-]/g, '_').slice(0, 64) || 'unknown';
138
- return join(dir, `${safe}.jsonl`);
139
- };
140
-
141
- /** A failure's name, never its text. RunFailure kinds are the supervisor's words. */
142
- export const failureNameOf = (error) => {
143
- const kind = failureKindOf(error) || (typeof error?.kind === 'string' && /^[a-z_]{1,40}$/.test(error.kind) ? error.kind : null);
144
- if (kind) return { name: kind, source: 'supervisor' };
145
- const message = String(error?.message ?? error ?? '');
146
- if (/\btimed out after \d+ms\b/.test(message)) return { name: 'adapter_timeout', source: 'adapter' };
147
- if (/\bexited with code\b/.test(message)) return { name: 'adapter_exit', source: 'adapter' };
148
- return { name: 'adapter_error', source: 'adapter' };
149
- };
150
-
151
- const failureClassOf = (error) => {
152
- try { return classifySpawnFailure(error); } catch { return null; }
153
- };
154
-
155
- // What happened to a turn, named by the run loop once delivery settles.
156
- export const TURN_OUTCOMES = Object.freeze([
157
- 'delivered', // the reply reached the pod (wrapper post, the agent's own post, or an ask response)
158
- 'silent', // NO_REPLY, a heartbeat control word, or empty output: nothing to post
159
- 'reply_lost', // the model answered and every delivery rung threw
160
- 'claim_lost', // the lease lapsed mid-turn and a peer owns the message; reply suppressed
161
- 'server_skipped', // the server answered and created nothing (a duplicate, a kept heartbeat)
162
- 'server_refused', // the server refused the post (a run cap)
163
- 'upstream_refused', // the model route refused; the CLI exited with nothing to say
164
- ]);
165
- export const OK_OUTCOMES = Object.freeze(['delivered', 'silent']);
166
-
167
- const EVENT_TYPE_RE = /^[A-Za-z0-9._-]{1,40}$/;
168
- const iso = (ms) => new Date(ms).toISOString();
169
- const int = (value) => (Number.isInteger(value) ? value : null);
170
-
171
- /**
172
- * The metric line. Every field is listed here, each a number, boolean, null or
173
- * a short fixed word; nothing from the prompt, the reply, a failure's detail or
174
- * the environment can reach it.
175
- */
176
- export const metricLine = (f) => {
177
- const failed = f.outcome === 'failed';
178
- const outcome = failed || TURN_OUTCOMES.includes(f.outcome) ? f.outcome : 'unknown';
179
- return {
180
- v: 1,
181
- ts: iso(f.settledMs ?? f.endMs),
182
- seat: String(f.seat || 'unknown').replace(/[^A-Za-z0-9_.-]/g, '_').slice(0, 64),
183
- adapter: String(f.adapter || 'unknown').replace(/[^A-Za-z0-9_.-]/g, '_').slice(0, 32),
184
- path: f.path === 'supervised' ? 'supervised' : 'legacy',
185
- eventType: EVENT_TYPE_RE.test(String(f.eventType || '')) ? f.eventType : null,
186
- turnStart: iso(f.startMs),
187
- turnEnd: iso(f.endMs),
188
- durationMs: Math.max(0, f.endMs - f.startMs),
189
- outcome,
190
- ok: OK_OUTCOMES.includes(outcome),
191
- failure: failed ? (f.failureName || 'adapter_error') : null,
192
- failureSource: failed ? (f.failureSource || null) : null,
193
- failureClass: failed && typeof f.failureClass === 'string' ? f.failureClass : null,
194
- stalled: !!f.stalled,
195
- cancelled: !!f.cancelled,
196
- killed: !!f.killed,
197
- exitCode: int(f.exitCode),
198
- signal: typeof f.signal === 'string' && /^SIG[A-Z0-9]+$/.test(f.signal) ? f.signal : null,
199
- exitObserved: !!f.exitObserved,
200
- childLeftRunning: typeof f.childLeftRunning === 'boolean' ? f.childLeftRunning : null,
201
- attempts: int(f.attempts),
202
- stallMs: int(f.policy?.stallMs),
203
- cancelGraceMs: int(f.policy?.cancelGraceMs),
204
- timeoutMs: int(f.policy?.timeoutMs),
205
- };
206
- };
207
-
208
- const isChild = (value) => !!value && typeof value.once === 'function' && typeof value.kill === 'function';
209
- const isAlive = (child) => !!child && child.exitCode === null && child.signalCode === null;
210
-
211
- const waitForExit = (child, ms) => new Promise((resolve) => {
212
- if (!isAlive(child)) { resolve(); return; }
213
- const timer = setTimeout(resolve, ms);
214
- timer.unref?.();
215
- child.once('exit', () => { clearTimeout(timer); resolve(); });
216
- });
217
-
218
- /**
219
- * @param {object} args
220
- * @param {string} args.seat agent name; names the metrics file
221
- * @param {number} args.turnTimeoutMs the run loop's effective turn budget
222
- * @param {boolean} args.supervise performRun's superviseRuns
223
- * @param {(line: string) => void} [args.log]
224
- * @param {object} [args.env]
225
- * @param {Function} [args.superviseRun] injectable for tests
226
- */
227
- export const createTurnRunner = ({
228
- seat,
229
- turnTimeoutMs,
230
- supervise = false,
231
- log = () => {},
232
- env = process.env,
233
- now = Date.now,
234
- superviseRun = defaultSuperviseRun,
235
- } = {}) => {
236
- const metricsPath = metricsPathFor(seat, env);
237
- let metricsWarned = false;
238
-
239
- const record = (fields) => {
240
- try {
241
- mkdirSync(dirname(metricsPath), { recursive: true, mode: 0o700 });
242
- appendFileSync(metricsPath, `${JSON.stringify(metricLine({ seat, ...fields }))}\n`, { mode: 0o600 });
243
- } catch (error) {
244
- // Metrics never fail a turn. Said once per process, not per turn.
245
- if (!metricsWarned) {
246
- metricsWarned = true;
247
- log(`turn metrics not written (${error?.code || 'error'}); turns are unaffected`);
248
- }
249
- }
250
- };
251
-
252
- /** The boot line for the supervised path; the plain path logs nothing new. */
253
- const describe = (adapterName) => {
254
- const warnings = [];
255
- const policy = supervisedRunPolicy(turnTimeoutMs, { adapterName, env, warnings });
256
- return `supervised runs ON for ${adapterName}: stall=${policy.stallMs ? `${policy.stallMs}ms` : 'off'}, `
257
- + `cancel grace=${policy.cancelGraceMs}ms, wall=${policy.timeoutMs}ms, `
258
- + 'trace=memory (no record written); '
259
- + `metrics → ${metricsPath}${warnings.length ? `; ${warnings.join('; ')}` : ''}`;
260
- };
261
-
262
- const runSupervised = async (adapter, prompt, ctx, eventType, hold) => {
263
- const startMs = now();
264
- const policy = supervisedRunPolicy(turnTimeoutMs, { adapterName: adapter?.name, env });
265
- const store = createMetadataStore({ now });
266
- let child = null;
267
- let exit = null;
268
- const callerOnSpawn = ctx?.onSpawn;
269
- const onSpawn = (...args) => {
270
- const proc = args.find(isChild);
271
- if (proc) {
272
- child = proc;
273
- exit = null;
274
- proc.once('exit', (code, signal) => { if (child === proc) exit = { code, signal }; });
275
- }
276
- return typeof callerOnSpawn === 'function' ? callerOnSpawn(...args) : undefined;
277
- };
278
-
279
- const supervised = await superviseRun({
280
- adapter,
281
- prompt,
282
- ctx: { ...ctx, onSpawn },
283
- store,
284
- policy,
285
- label: EVENT_TYPE_RE.test(String(eventType || '')) ? eventType : undefined,
286
- });
287
-
288
- const ok = !supervised.failure;
289
- if (!ok) await waitForExit(child, EXIT_WAIT_MS);
290
- const events = store.lastRun()?.events() || [];
291
- const recovered = (action) => events.some((e) => e.type === 'liveness.recovery' && e.action === action);
292
- const killed = recovered('kill');
293
- const named = ok ? null : failureNameOf(supervised.failure);
294
- const fields = {
295
- adapter: adapter?.name,
296
- path: 'supervised',
297
- eventType,
298
- startMs,
299
- endMs: now(),
300
- outcome: ok ? undefined : 'failed',
301
- failureName: named?.name,
302
- failureSource: named?.source,
303
- failureClass: ok ? null : failureClassOf(supervised.failure),
304
- stalled: events.some((e) => e.type === 'liveness.stalled'),
305
- cancelled: recovered('cancel'),
306
- killed,
307
- exitCode: exit ? exit.code : null,
308
- signal: exit ? exit.signal : null,
309
- exitObserved: !!exit,
310
- childLeftRunning: child ? isAlive(child) : null,
311
- attempts: supervised.attempts,
312
- policy,
313
- };
314
- // A spawn that resolved is not yet an ok turn: the line waits for the run
315
- // loop to say what became of the reply.
316
- if (ok) { hold(fields); return supervised.result; }
317
- record(fields);
318
- if (named.source === 'supervisor') {
319
- // The name goes in the seat log. The throw takes the road a plain-path
320
- // failure takes: unacked, the run loop's retry policy, onError.
321
- log(`[${eventType}] supervised turn failed: ${named.name}${killed ? ' (child killed)' : ''}`
322
- + `${isAlive(child) ? ' (child still running)' : ''}`);
323
- }
324
- throw supervised.failure;
325
- };
326
-
327
- return {
328
- supervise,
329
- metricsPath,
330
- describe,
331
- /**
332
- * One model turn. `run` has adapter.spawn's contract; `finish(outcome)`
333
- * writes the line once delivery has settled. Only the first finish counts,
334
- * and a finish with no spawned turn behind it writes nothing.
335
- */
336
- begin({ eventType } = {}) {
337
- let held = null;
338
- let done = false;
339
- return {
340
- run(adapter, prompt, ctx) {
341
- if (!supervise) return adapter.spawn(prompt, ctx);
342
- return runSupervised(adapter, prompt, ctx, eventType, (fields) => { held = fields; });
343
- },
344
- finish(outcome) {
345
- if (done || !held) return;
346
- done = true;
347
- record({ ...held, outcome, settledMs: now() });
348
- },
349
- };
350
- },
351
- };
352
- };
package/src/lib/task.js DELETED
@@ -1,235 +0,0 @@
1
- /**
2
- * The task board from the CLI: identity, the request layer, and one-line
3
- * rendering. The routes are the ones the commonly_* task tools call
4
- * (backend/routes/tasksApi.ts), so the CLI and the MCP cannot disagree about
5
- * what a write means.
6
- *
7
- * Identity is an explicit token and nothing else: --token-file, then
8
- * COMMONLY_TOKEN_FILE, then COMMONLY_AGENT_TOKEN. The saved login and
9
- * COMMONLY_TOKEN are never read. A write made under an operator's saved login
10
- * lands on the row under that operator's name.
11
- */
12
-
13
- import { readFileSync } from 'fs';
14
- import { DEFAULT_URL, resolveInstance } from './config.js';
15
- import { extractToken, resolveTokenFilePath, TOKEN_FILE_ENV } from './inbox.js';
16
- import { CLI } from './cli-hint.js';
17
-
18
- export const API_URL_ENV = 'COMMONLY_API_URL';
19
- export const TASK_TOKEN_ENV = 'COMMONLY_AGENT_TOKEN';
20
- export const TASK_STATUSES = ['pending', 'claimed', 'done', 'blocked'];
21
- // `update` writes only these. A claim writes the lease, which /claim owns, and a
22
- // close stamps completedAt and the PR, which PATCH does not.
23
- export const UPDATE_STATUSES = ['pending', 'blocked'];
24
- export const TASK_PRIORITIES = ['P0', 'P1', 'P2', 'P3'];
25
- const REQUEST_TIMEOUT_MS = 30_000;
26
-
27
- /** A refusal made before any request: a bad flag, or no identity to send. */
28
- export class TaskRefusal extends Error {}
29
-
30
- /** A failed call. `status` is 0 when the server was never reached. */
31
- export class TaskRequestError extends Error {
32
- constructor(message, { status = 0, code = null, body = null } = {}) {
33
- super(message);
34
- this.name = 'TaskRequestError';
35
- this.status = status;
36
- this.code = code;
37
- this.body = body;
38
- }
39
- }
40
-
41
- export const requireText = (value, flag) => {
42
- const text = typeof value === 'string' ? value.trim() : '';
43
- if (!text) throw new TaskRefusal(`${flag} needs a value.`);
44
- return text;
45
- };
46
-
47
- export const checkPriority = (value) => {
48
- const priority = String(value).trim().toUpperCase();
49
- if (!TASK_PRIORITIES.includes(priority)) {
50
- throw new TaskRefusal(`--priority must be ${TASK_PRIORITIES.join(', ')} (got '${value}').`);
51
- }
52
- return priority;
53
- };
54
-
55
- export const checkStatusFilter = (value) => {
56
- const statuses = String(value).split(',').map((s) => s.trim()).filter(Boolean);
57
- if (statuses.length === 0 || statuses.some((s) => !TASK_STATUSES.includes(s))) {
58
- throw new TaskRefusal(`--status takes ${TASK_STATUSES.join(', ')}, comma-separated (got '${value}').`);
59
- }
60
- return statuses.join(',');
61
- };
62
-
63
- export const checkUpdateStatus = (value) => {
64
- const status = String(value).trim().toLowerCase();
65
- if (status === 'claimed') {
66
- throw new TaskRefusal(`--status claimed is refused: \`${CLI} task claim\` writes the lease, and a bare status does not.`);
67
- }
68
- if (status === 'done') {
69
- throw new TaskRefusal(`--status done is refused: \`${CLI} task close\` stamps completedAt and the PR, and PATCH does not.`);
70
- }
71
- if (!UPDATE_STATUSES.includes(status)) {
72
- throw new TaskRefusal(`--status must be ${UPDATE_STATUSES.join(' or ')} (got '${value}').`);
73
- }
74
- return status;
75
- };
76
-
77
- /** TASK-669, task-669 and 669 name one row. Anything else is refused before a request. */
78
- export const checkTaskId = (value) => {
79
- const match = String(value ?? '').trim().match(/^(?:task-)?(\d+)$/i);
80
- if (!match) throw new TaskRefusal(`task id must look like TASK-123 (got '${value ?? ''}').`);
81
- return `TASK-${String(Number(match[1])).padStart(3, '0')}`;
82
- };
83
-
84
- /**
85
- * Resolve the token. A token file that exists but holds nothing is refused by
86
- * its path. The message never suggests `commonly login`, because pointing back
87
- * at the saved login is the thing this rule exists to prevent.
88
- */
89
- export const resolveTaskToken = (opts = {}, env = process.env) => {
90
- const tokenFile = resolveTokenFilePath(opts, env);
91
- if (tokenFile) {
92
- let raw;
93
- try {
94
- raw = readFileSync(tokenFile, 'utf8');
95
- } catch (err) {
96
- throw new TaskRefusal(`cannot read the token file ${tokenFile} (${err?.code || err?.message}).`);
97
- }
98
- const token = extractToken(raw);
99
- if (!token) throw new TaskRefusal(`no token in ${tokenFile}. Expected a raw token, or JSON with a token field.`);
100
- return token;
101
- }
102
- const fromEnv = extractToken(env[TASK_TOKEN_ENV]);
103
- if (fromEnv) return fromEnv;
104
- throw new TaskRefusal(`no token. Pass --token-file <path>, or set ${TOKEN_FILE_ENV} or ${TASK_TOKEN_ENV}.`);
105
- };
106
-
107
- /**
108
- * --instance takes a URL or a saved key, and an unknown key is refused. Falling
109
- * back to the default host would send this token to an instance it was not
110
- * minted on.
111
- */
112
- export const resolveTaskBaseUrl = (opts = {}, env = process.env) => {
113
- if (opts.instance) {
114
- const inst = resolveInstance(opts.instance);
115
- if (!inst?.url) {
116
- throw new TaskRefusal(`unknown instance '${opts.instance}'. Pass its URL, or a saved instance key.`);
117
- }
118
- return inst.url.replace(/\/$/, '');
119
- }
120
- const fromEnv = String(env[API_URL_ENV] || '').trim().replace(/\/$/, '');
121
- return fromEnv || DEFAULT_URL;
122
- };
123
-
124
- /** `/api/v1/tasks/<pod>` for the board, and `…/<taskId>` for one row. */
125
- export const taskPath = (podId, taskId = null) => [
126
- '/api/v1/tasks',
127
- encodeURIComponent(podId),
128
- ...(taskId === null ? [] : [encodeURIComponent(taskId)]),
129
- ].join('/');
130
-
131
- const messageFor = (status, body, text) => {
132
- if (body && typeof body.error === 'string' && body.error) return body.error;
133
- if (body && typeof body.message === 'string' && body.message) return body.message;
134
- const trimmed = String(text || '').trim();
135
- if (!trimmed) return `HTTP ${status}`;
136
- if (/^<(?:!doctype|html)/i.test(trimmed)) {
137
- const title = trimmed.match(/<title[^>]*>([\s\S]*?)<\/title>/i)?.[1];
138
- return title ? title.replace(/\s+/g, ' ').trim() : 'HTML error page';
139
- }
140
- return trimmed.replace(/\s+/g, ' ').slice(0, 200);
141
- };
142
-
143
- /**
144
- * One attempt per call. The writes here are POSTs, and a retried create or note
145
- * lands twice, so a failure is reported and the caller decides what to do next.
146
- */
147
- export const createTaskClient = ({ baseUrl, token, fetchImpl = fetch }) => {
148
- const send = async (method, path, body) => {
149
- let res;
150
- try {
151
- res = await fetchImpl(`${baseUrl}${path}`, {
152
- method,
153
- headers: {
154
- Authorization: `Bearer ${token}`,
155
- 'Content-Type': 'application/json',
156
- },
157
- ...(body === undefined ? {} : { body: JSON.stringify(body) }),
158
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
159
- });
160
- } catch (err) {
161
- throw new TaskRequestError(`could not reach ${baseUrl} (${err?.message || err}).`);
162
- }
163
- const text = await res.text();
164
- let parsed = null;
165
- try {
166
- parsed = text ? JSON.parse(text) : null;
167
- } catch {
168
- parsed = null;
169
- }
170
- if (!res.ok) {
171
- throw new TaskRequestError(messageFor(res.status, parsed, text), {
172
- status: res.status,
173
- code: parsed?.code || null,
174
- body: parsed,
175
- });
176
- }
177
- return parsed;
178
- };
179
-
180
- const get = (path, params = {}) => {
181
- const query = new URLSearchParams(
182
- Object.entries(params).filter(([, value]) => value !== undefined && value !== ''),
183
- ).toString();
184
- return send('GET', query ? `${path}?${query}` : path);
185
- };
186
-
187
- return {
188
- get,
189
- post: (path, body = {}) => send('POST', path, body),
190
- patch: (path, body = {}) => send('PATCH', path, body),
191
- };
192
- };
193
-
194
- const clockOf = (iso) => {
195
- if (!iso) return null;
196
- const at = new Date(iso);
197
- return Number.isNaN(at.getTime()) ? null : `${at.toISOString().slice(11, 16)}Z`;
198
- };
199
-
200
- /** One row, as a human reads it. A claimed row also says whether its lease holds. */
201
- export const formatTaskLine = (task) => {
202
- const line = [task.taskId, task.status, task.priority || '-', task.assignee || '-', task.title].join(' ');
203
- if (task.status !== 'claimed') return line;
204
- const at = clockOf(task.claimExpiresAt);
205
- if (!at) return line;
206
- const lapsed = new Date(task.claimExpiresAt).getTime() < Date.now();
207
- return `${line} (${lapsed ? 'lease lapsed' : 'held until'} ${at})`;
208
- };
209
-
210
- /**
211
- * One line for a failed call. The unseen-messages hold and a lease conflict get
212
- * their own wording, because a seat acts on each differently. Everything else is
213
- * the server's own text.
214
- */
215
- export const describeFailure = (err) => {
216
- if (!(err instanceof TaskRequestError)) return `Failed: ${err?.message || err}`;
217
- if (err.status === 0) return `Failed: ${err.message}`;
218
- const body = err.body || {};
219
- let line;
220
- if (err.code === 'unseen_messages') {
221
- const count = Number(body.unseenCount) || 0;
222
- line = `Failed (HTTP ${err.status}): ${count} unseen message${count === 1 ? '' : 's'} in this pod. `
223
- + 'Read them and retry, or pass --continue-anyway to go ahead.';
224
- } else {
225
- const detail = [
226
- body.claimedBy ? `held by ${body.claimedBy}` : null,
227
- clockOf(body.claimExpiresAt) ? `lease until ${clockOf(body.claimExpiresAt)}` : null,
228
- ].filter(Boolean).join(', ');
229
- line = `Failed (HTTP ${err.status}): ${err.message}${detail ? ` (${detail})` : ''}`;
230
- if (err.status === 401) {
231
- line += '. Check the agent token: --token-file, COMMONLY_TOKEN_FILE or COMMONLY_AGENT_TOKEN.';
232
- }
233
- }
234
- return line.replace(/\s+/g, ' ');
235
- };