@ravefox/space 0.2.1 → 0.4.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 (167) hide show
  1. package/LICENSE +21 -205
  2. package/README.md +104 -31
  3. package/bin/space.js +113 -0
  4. package/lib/agents.js +181 -0
  5. package/lib/boardTools.js +89 -0
  6. package/lib/credentials.js +95 -0
  7. package/lib/daemon.js +294 -0
  8. package/lib/harness.js +166 -0
  9. package/lib/http.js +101 -0
  10. package/lib/login.js +68 -0
  11. package/lib/mcp.js +39 -0
  12. package/lib/runtime.js +253 -0
  13. package/lib/store.js +96 -0
  14. package/lib/tools.js +93 -0
  15. package/lib/workspaceSandbox.js +77 -0
  16. package/lib/workspaceTools.js +160 -0
  17. package/lib/workspaceWorker.js +17 -0
  18. package/package.json +27 -46
  19. package/NOTICE +0 -55
  20. package/assets/webhook-sdk/python/bot.py +0 -63
  21. package/assets/webhook-sdk/python/commonly.py +0 -232
  22. package/node_modules/@commonlyai/seats/README.md +0 -25
  23. package/node_modules/@commonlyai/seats/package.json +0 -22
  24. package/node_modules/@commonlyai/seats/src/adapters/hermes.js +0 -264
  25. package/node_modules/@commonlyai/seats/src/adapters/stub.js +0 -24
  26. package/node_modules/@commonlyai/seats/src/contract.js +0 -48
  27. package/node_modules/@commonlyai/seats/src/failures.js +0 -44
  28. package/node_modules/@commonlyai/seats/src/harness-plan.js +0 -329
  29. package/node_modules/@commonlyai/seats/src/index.js +0 -22
  30. package/node_modules/@commonlyai/seats/src/process-group.js +0 -269
  31. package/node_modules/@commonlyai/seats/src/record/format.js +0 -737
  32. package/node_modules/@commonlyai/seats/src/record/otel.js +0 -238
  33. package/node_modules/@commonlyai/seats/src/runtime-observation.js +0 -29
  34. package/node_modules/@commonlyai/seats/src/seat-model.js +0 -17
  35. package/node_modules/@commonlyai/seats/src/supervisor.js +0 -308
  36. package/node_modules/@commonlyai/seats/src/trace/events.js +0 -46
  37. package/node_modules/@commonlyai/seats/src/trace/store.js +0 -276
  38. package/node_modules/@commonlyai/seats/src/turn-processes.js +0 -226
  39. package/node_modules/@commonlyai/seats/src/upstream-refusal.js +0 -418
  40. package/node_modules/@commonlyai/seats/src/valet/env.js +0 -201
  41. package/node_modules/@commonlyai/seats/src/valet/grant.js +0 -123
  42. package/node_modules/@commonlyai/seats/src/valet/permissions.js +0 -213
  43. package/node_modules/@commonlyai/seats/src/wire/provider-proxy.js +0 -529
  44. package/skills/commonly/SKILL.md +0 -233
  45. package/src/commands/agent.js +0 -6334
  46. package/src/commands/daemon.js +0 -933
  47. package/src/commands/dev.js +0 -153
  48. package/src/commands/doctor.js +0 -141
  49. package/src/commands/inbox.js +0 -330
  50. package/src/commands/login.js +0 -194
  51. package/src/commands/mcp.js +0 -56
  52. package/src/commands/pod.js +0 -120
  53. package/src/commands/task.js +0 -279
  54. package/src/commands/verify.js +0 -48
  55. package/src/index.js +0 -99
  56. package/src/lib/adapter-login.js +0 -76
  57. package/src/lib/adapters/broker-bridge-entry.js +0 -76
  58. package/src/lib/adapters/claude.js +0 -2207
  59. package/src/lib/adapters/codex-broker-bridge.mjs +0 -298
  60. package/src/lib/adapters/codex.js +0 -1898
  61. package/src/lib/adapters/dsh.js +0 -513
  62. package/src/lib/adapters/goose.js +0 -510
  63. package/src/lib/adapters/hermes.js +0 -18
  64. package/src/lib/adapters/index.js +0 -68
  65. package/src/lib/adapters/opencode.js +0 -1681
  66. package/src/lib/adapters/pi-commonly-mcp.mjs +0 -71
  67. package/src/lib/adapters/pi-mcp-client.mjs +0 -617
  68. package/src/lib/adapters/pi-shell-env.mjs +0 -41
  69. package/src/lib/adapters/pi.js +0 -1025
  70. package/src/lib/adapters/qwen.js +0 -523
  71. package/src/lib/adapters/seat-model.js +0 -8
  72. package/src/lib/adapters/stub.js +0 -1
  73. package/src/lib/adapters/tested-version.js +0 -106
  74. package/src/lib/api.js +0 -205
  75. package/src/lib/claim-outcome.js +0 -128
  76. package/src/lib/claimed-work-gate.js +0 -73
  77. package/src/lib/cli-hint.js +0 -33
  78. package/src/lib/commit-attribution.js +0 -391
  79. package/src/lib/config.js +0 -155
  80. package/src/lib/content-refusal.js +0 -94
  81. package/src/lib/credential-file.js +0 -208
  82. package/src/lib/daemon-agent-states.js +0 -95
  83. package/src/lib/daemon-hint.js +0 -18
  84. package/src/lib/daemon-logs.js +0 -43
  85. package/src/lib/daemon-service-windows.js +0 -263
  86. package/src/lib/daemon-service.js +0 -486
  87. package/src/lib/daemon-state.js +0 -84
  88. package/src/lib/daemon-store.js +0 -119
  89. package/src/lib/daemon-supervisor.js +0 -629
  90. package/src/lib/daemon-watch.js +0 -115
  91. package/src/lib/declared-mcp-guard.js +0 -284
  92. package/src/lib/default-environment.js +0 -207
  93. package/src/lib/device-login.js +0 -126
  94. package/src/lib/digest-frame.js +0 -151
  95. package/src/lib/doctor.js +0 -291
  96. package/src/lib/egress-proxy.js +0 -160
  97. package/src/lib/enforcement.js +0 -946
  98. package/src/lib/environment.js +0 -681
  99. package/src/lib/first-output-watchdog.js +0 -191
  100. package/src/lib/git-readonly-server.js +0 -138
  101. package/src/lib/git-readonly.js +0 -463
  102. package/src/lib/git-remote-commonly.mjs +0 -100
  103. package/src/lib/grant-broker-guard.js +0 -345
  104. package/src/lib/hooks-config.js +0 -238
  105. package/src/lib/host-gh-broker.js +0 -152
  106. package/src/lib/host-gh-shim.mjs +0 -98
  107. package/src/lib/inbox.js +0 -483
  108. package/src/lib/instance-proxy.js +0 -76
  109. package/src/lib/machine-host.js +0 -29
  110. package/src/lib/mcp-credential-delivery.js +0 -121
  111. package/src/lib/mcp-first-run.js +0 -50
  112. package/src/lib/mcp-home.js +0 -644
  113. package/src/lib/mcp-ready.js +0 -68
  114. package/src/lib/mcp-server-version.js +0 -123
  115. package/src/lib/mcp-token.js +0 -36
  116. package/src/lib/mcp-warm-child.mjs +0 -412
  117. package/src/lib/memory-bridge.js +0 -191
  118. package/src/lib/memory-import.js +0 -156
  119. package/src/lib/missing-harness.js +0 -31
  120. package/src/lib/model-unavailable.js +0 -154
  121. package/src/lib/native-runtime-inventory.js +0 -40
  122. package/src/lib/outsider-grant-mcp.js +0 -82
  123. package/src/lib/own-key-launch.js +0 -42
  124. package/src/lib/pod-catch-up.js +0 -55
  125. package/src/lib/pod-focus.js +0 -224
  126. package/src/lib/pod-tail.js +0 -68
  127. package/src/lib/poll-liveness.js +0 -103
  128. package/src/lib/poll-retry.js +0 -81
  129. package/src/lib/poller.js +0 -200
  130. package/src/lib/provider-proxy.js +0 -42
  131. package/src/lib/public-seat-mcp-flag.js +0 -36
  132. package/src/lib/public-tool-error.js +0 -60
  133. package/src/lib/public-withheld-tools.js +0 -48
  134. package/src/lib/reply-receipt.js +0 -55
  135. package/src/lib/resolve-on-path.js +0 -22
  136. package/src/lib/runtime-inventory.js +0 -56
  137. package/src/lib/runtime-observation.js +0 -37
  138. package/src/lib/sandbox/bwrap.js +0 -275
  139. package/src/lib/sandbox/codex-perimeter.js +0 -16
  140. package/src/lib/sandbox/host-check.js +0 -268
  141. package/src/lib/sandbox/mode.js +0 -49
  142. package/src/lib/sandbox/opencode-loopback.mjs +0 -68
  143. package/src/lib/sandbox/seatbelt.js +0 -684
  144. package/src/lib/sandbox/turn-dir.js +0 -56
  145. package/src/lib/sandbox/windows-unsandboxed.js +0 -183
  146. package/src/lib/sandbox/workspace-git-config.js +0 -243
  147. package/src/lib/sandbox/workspace-pin.js +0 -158
  148. package/src/lib/seat-runtime-controls.js +0 -61
  149. package/src/lib/seat-stop.js +0 -97
  150. package/src/lib/server-skills.js +0 -32
  151. package/src/lib/session-recycle.js +0 -219
  152. package/src/lib/session-store.js +0 -215
  153. package/src/lib/skill-prompt.js +0 -74
  154. package/src/lib/skills-import.js +0 -113
  155. package/src/lib/spawn-credential.js +0 -402
  156. package/src/lib/spawn-retry.js +0 -311
  157. package/src/lib/stable-install.js +0 -176
  158. package/src/lib/supervised-turn.js +0 -352
  159. package/src/lib/task.js +0 -235
  160. package/src/lib/turn-failure-notice.js +0 -177
  161. package/src/lib/unconfined-seat-guard.js +0 -94
  162. package/src/lib/update-check.js +0 -192
  163. package/src/lib/upstream-refusal.js +0 -1
  164. package/src/lib/webhook-server.js +0 -109
  165. package/src/lib/windows-command.js +0 -393
  166. package/src/lib/workspace-git.js +0 -87
  167. 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
- };