@cmgmyr/hive 0.1.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 (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +70 -0
  3. package/claude-plugin/.claude-plugin/plugin.json +7 -0
  4. package/claude-plugin/README.md +47 -0
  5. package/claude-plugin/hooks/hooks.json +14 -0
  6. package/claude-plugin/kickoff.mjs +41 -0
  7. package/claude-plugin/skills/cleanup/SKILL.md +60 -0
  8. package/dist/abi.js +220 -0
  9. package/dist/abiProbe.js +9 -0
  10. package/dist/backgroundTasks.js +51 -0
  11. package/dist/backup.js +378 -0
  12. package/dist/brief.js +113 -0
  13. package/dist/build-info.json +1 -0
  14. package/dist/claudeDir.js +5 -0
  15. package/dist/cli.js +2901 -0
  16. package/dist/codexHome.js +297 -0
  17. package/dist/config.js +72 -0
  18. package/dist/context.js +256 -0
  19. package/dist/contextCheckpoint.js +55 -0
  20. package/dist/dashboard.js +1158 -0
  21. package/dist/dataDir.js +89 -0
  22. package/dist/db.js +421 -0
  23. package/dist/dispatcher.js +150 -0
  24. package/dist/firstPrompt.js +6 -0
  25. package/dist/harnesses.js +173 -0
  26. package/dist/help.js +353 -0
  27. package/dist/hook.js +197 -0
  28. package/dist/hooks.js +50 -0
  29. package/dist/index.js +34 -0
  30. package/dist/kickoff.js +191 -0
  31. package/dist/leadMessage.js +98 -0
  32. package/dist/mcpConfig.js +121 -0
  33. package/dist/processes.js +161 -0
  34. package/dist/profiles.js +278 -0
  35. package/dist/projectYml.js +254 -0
  36. package/dist/prompts.js +18 -0
  37. package/dist/ptys.js +146 -0
  38. package/dist/result.js +53 -0
  39. package/dist/scheduler.js +1622 -0
  40. package/dist/sessionProbe.js +131 -0
  41. package/dist/slug.js +29 -0
  42. package/dist/spawn.js +370 -0
  43. package/dist/stateProvenance.js +97 -0
  44. package/dist/statusline.js +82 -0
  45. package/dist/strictInput.js +52 -0
  46. package/dist/teardown.js +89 -0
  47. package/dist/tmux.js +1295 -0
  48. package/dist/tools/agents.js +1246 -0
  49. package/dist/tools/kv.js +89 -0
  50. package/dist/tools/leases.js +87 -0
  51. package/dist/tools/meta.js +183 -0
  52. package/dist/tools/pads.js +279 -0
  53. package/dist/tools/params.js +14 -0
  54. package/dist/tools/todos.js +382 -0
  55. package/dist/tools/wakes.js +466 -0
  56. package/dist/transcript.js +123 -0
  57. package/dist/version.js +59 -0
  58. package/package.json +62 -0
  59. package/profiles/orchestration/posture.md +27 -0
  60. package/profiles/orchestration/runbook.md +72 -0
  61. package/profiles/orchestration/worker.md +38 -0
  62. package/profiles/simple/posture.md +14 -0
package/dist/backup.js ADDED
@@ -0,0 +1,378 @@
1
+ import SqliteDb from "better-sqlite3";
2
+ import { randomUUID } from "node:crypto";
3
+ import { cpSync, existsSync, mkdirSync, readdirSync, renameSync, rmSync, statSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { errorMessage } from "./result.js";
6
+ export const BACKUP_DIRNAME = "backups";
7
+ export function backupsDir(dataDir) {
8
+ return join(dataDir, BACKUP_DIRNAME);
9
+ }
10
+ function timestampPart(now) {
11
+ return now.toISOString().replace(/[:.]/g, "-");
12
+ }
13
+ const SQL_DATETIME_FMT = "%Y-%m-%d %H:%M:%f";
14
+ const SQL_NOW = `strftime('${SQL_DATETIME_FMT}', 'now')`;
15
+ function finalSnapshotName(reason, now, n) {
16
+ const base = `${timestampPart(now)}-${reason}`;
17
+ return n === 0 ? base : `${base}-${n}`;
18
+ }
19
+ const NAME_PATTERN = /^(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z)-(migration|hourly|manual)(?:-\d+)?$/;
20
+ function parseSnapshotDirName(name) {
21
+ const m = NAME_PATTERN.exec(name);
22
+ if (!m)
23
+ return null;
24
+ const iso = m[1].replace(/T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z$/, "T$1:$2:$3.$4Z");
25
+ const createdAt = new Date(iso);
26
+ if (Number.isNaN(createdAt.getTime()))
27
+ return null;
28
+ return { createdAt, reason: m[2] };
29
+ }
30
+ function dirSizeBytes(path) {
31
+ let total = 0;
32
+ for (const entry of readdirSync(path, { withFileTypes: true })) {
33
+ const full = join(path, entry.name);
34
+ total += entry.isDirectory() ? dirSizeBytes(full) : statSync(full).size;
35
+ }
36
+ return total;
37
+ }
38
+ function listSnapshotRefs(dataDir) {
39
+ const dir = backupsDir(dataDir);
40
+ let entries;
41
+ try {
42
+ entries = readdirSync(dir, { withFileTypes: true });
43
+ }
44
+ catch {
45
+ return [];
46
+ }
47
+ return entries
48
+ .filter((e) => e.isDirectory())
49
+ .map((e) => {
50
+ const parsed = parseSnapshotDirName(e.name);
51
+ return parsed ? { name: e.name, path: join(dir, e.name), ...parsed } : null;
52
+ })
53
+ .filter((s) => s !== null)
54
+ .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
55
+ }
56
+ export function listSnapshots(dataDir) {
57
+ return listSnapshotRefs(dataDir).map((s) => ({ ...s, sizeBytes: dirSizeBytes(s.path) }));
58
+ }
59
+ export function totalSizeBytes(snapshots) {
60
+ return snapshots.reduce((sum, s) => sum + s.sizeBytes, 0);
61
+ }
62
+ export function takeSnapshot(db, dataDir, reason, now = new Date()) {
63
+ const parent = backupsDir(dataDir);
64
+ const staging = join(parent, `.staging-${randomUUID()}`);
65
+ let completedAt = now;
66
+ try {
67
+ mkdirSync(parent, { recursive: true });
68
+ mkdirSync(staging, { recursive: true });
69
+ db.prepare("VACUUM INTO ?").run(join(staging, "hive.db"));
70
+ completedAt = new Date();
71
+ const profilesSrc = join(dataDir, "profiles");
72
+ if (existsSync(profilesSrc)) {
73
+ cpSync(profilesSrc, join(staging, "profiles"), { recursive: true });
74
+ }
75
+ }
76
+ catch (e) {
77
+ try {
78
+ rmSync(staging, { recursive: true, force: true });
79
+ }
80
+ catch {
81
+ }
82
+ return { ok: false, error: errorMessage(e) };
83
+ }
84
+ let metaDb;
85
+ try {
86
+ metaDb = new SqliteDb(join(staging, "hive.db"));
87
+ }
88
+ catch (e) {
89
+ try {
90
+ rmSync(staging, { recursive: true, force: true });
91
+ }
92
+ catch {
93
+ }
94
+ return { ok: false, error: errorMessage(e) };
95
+ }
96
+ let corruptError;
97
+ try {
98
+ const iso = completedAt.toISOString();
99
+ metaDb
100
+ .prepare(`UPDATE backup_meta SET last_attempt_at = strftime('${SQL_DATETIME_FMT}', ?), last_success_at = strftime('${SQL_DATETIME_FMT}', ?) WHERE id = 1`)
101
+ .run(iso, iso);
102
+ }
103
+ catch (e) {
104
+ const code = e.code;
105
+ if (code === "SQLITE_NOTADB" || code === "SQLITE_CORRUPT")
106
+ corruptError = e;
107
+ }
108
+ finally {
109
+ try {
110
+ metaDb.close();
111
+ }
112
+ catch {
113
+ }
114
+ }
115
+ if (corruptError !== undefined) {
116
+ try {
117
+ rmSync(staging, { recursive: true, force: true });
118
+ }
119
+ catch {
120
+ }
121
+ return { ok: false, error: errorMessage(corruptError) };
122
+ }
123
+ if (existsSync(join(staging, "hive.db-journal"))) {
124
+ try {
125
+ rmSync(staging, { recursive: true, force: true });
126
+ }
127
+ catch {
128
+ }
129
+ return { ok: false, error: "a rollback journal survived the snapshot's own backup_meta write" };
130
+ }
131
+ for (let n = 0; n < 1000; n++) {
132
+ const dir = join(parent, finalSnapshotName(reason, now, n));
133
+ try {
134
+ renameSync(staging, dir);
135
+ return { ok: true, path: dir };
136
+ }
137
+ catch (e) {
138
+ const code = e.code;
139
+ if (code !== "ENOTEMPTY" && code !== "EEXIST") {
140
+ try {
141
+ rmSync(staging, { recursive: true, force: true });
142
+ }
143
+ catch {
144
+ }
145
+ return { ok: false, error: errorMessage(e) };
146
+ }
147
+ }
148
+ }
149
+ try {
150
+ rmSync(staging, { recursive: true, force: true });
151
+ }
152
+ catch {
153
+ }
154
+ return { ok: false, error: `could not claim a snapshot name for ${reason} after 1000 attempts` };
155
+ }
156
+ function envInt(name, fallback) {
157
+ const raw = process.env[name];
158
+ if (!raw)
159
+ return fallback;
160
+ const n = Number.parseInt(raw, 10);
161
+ return Number.isInteger(n) && n >= 0 ? n : fallback;
162
+ }
163
+ export function retentionPolicy() {
164
+ return {
165
+ keepLast: envInt("HIVE_BACKUP_KEEP_LAST", 10),
166
+ keepDailyDays: envInt("HIVE_BACKUP_KEEP_DAILY_DAYS", 7),
167
+ };
168
+ }
169
+ const STALE_STAGING_MAX_AGE_MS = 60 * 60 * 1000;
170
+ function sweepStaleStagingDirs(dataDir, maxAgeMs = STALE_STAGING_MAX_AGE_MS) {
171
+ const dir = backupsDir(dataDir);
172
+ let entries;
173
+ try {
174
+ entries = readdirSync(dir, { withFileTypes: true });
175
+ }
176
+ catch {
177
+ return;
178
+ }
179
+ const cutoff = Date.now() - maxAgeMs;
180
+ for (const entry of entries) {
181
+ if (!entry.isDirectory() || !entry.name.startsWith(".staging-"))
182
+ continue;
183
+ const full = join(dir, entry.name);
184
+ try {
185
+ if (statSync(full).mtimeMs < cutoff)
186
+ rmSync(full, { recursive: true, force: true });
187
+ }
188
+ catch {
189
+ }
190
+ }
191
+ }
192
+ export function pruneSnapshots(dataDir, policy = retentionPolicy(), now = new Date(), protect = new Set()) {
193
+ sweepStaleStagingDirs(dataDir);
194
+ const snapshots = listSnapshotRefs(dataDir);
195
+ const keepLast = Math.max(1, policy.keepLast);
196
+ const keep = new Set(snapshots.slice(0, keepLast).map((s) => s.name));
197
+ for (const name of protect)
198
+ keep.add(name);
199
+ const cutoff = new Date(now);
200
+ cutoff.setUTCDate(cutoff.getUTCDate() - policy.keepDailyDays);
201
+ const seenDays = new Set();
202
+ for (const s of snapshots) {
203
+ if (s.createdAt < cutoff)
204
+ continue;
205
+ const day = s.createdAt.toISOString().slice(0, 10);
206
+ if (seenDays.has(day))
207
+ continue;
208
+ seenDays.add(day);
209
+ keep.add(s.name);
210
+ }
211
+ const removed = [];
212
+ for (const s of snapshots) {
213
+ if (keep.has(s.name))
214
+ continue;
215
+ rmSync(s.path, { recursive: true, force: true });
216
+ removed.push(s.name);
217
+ }
218
+ return removed;
219
+ }
220
+ export function backupNow(db, dataDir, reason, protect = new Set()) {
221
+ const result = takeSnapshot(db, dataDir, reason);
222
+ if (result.ok) {
223
+ try {
224
+ db.prepare(`UPDATE backup_meta SET last_success_at = ${SQL_NOW} WHERE id = 1`).run();
225
+ }
226
+ catch {
227
+ }
228
+ try {
229
+ pruneSnapshots(dataDir, retentionPolicy(), new Date(), protect);
230
+ }
231
+ catch {
232
+ }
233
+ }
234
+ else {
235
+ try {
236
+ db.prepare(`UPDATE backup_meta SET last_error = ?, last_error_at = ${SQL_NOW} WHERE id = 1`).run(result.error ?? "unknown error");
237
+ }
238
+ catch {
239
+ }
240
+ }
241
+ return result;
242
+ }
243
+ const hourlyClaimStmt = new WeakMap();
244
+ function claimStatement(db) {
245
+ let stmt = hourlyClaimStmt.get(db);
246
+ if (!stmt) {
247
+ stmt = db.prepare(`UPDATE backup_meta SET last_attempt_at = ${SQL_NOW}
248
+ WHERE id = 1 AND (
249
+ last_attempt_at IS NULL
250
+ OR last_attempt_at <= strftime('${SQL_DATETIME_FMT}', 'now', '-1 hours')
251
+ OR last_attempt_at > ${SQL_NOW}
252
+ )`);
253
+ hourlyClaimStmt.set(db, stmt);
254
+ }
255
+ return stmt;
256
+ }
257
+ export function maybeBackupHourly(db, dataDir) {
258
+ try {
259
+ if (claimStatement(db).run().changes !== 1)
260
+ return;
261
+ backupNow(db, dataDir, "hourly");
262
+ }
263
+ catch {
264
+ }
265
+ }
266
+ export function maybeBackupBeforeMigrations(db, dataDir, pendingCount) {
267
+ if (pendingCount <= 0)
268
+ return;
269
+ try {
270
+ backupNow(db, dataDir, "migration");
271
+ }
272
+ catch {
273
+ }
274
+ }
275
+ export function readBackupMeta(db) {
276
+ return db
277
+ .prepare("SELECT last_attempt_at, last_success_at, last_error, last_error_at FROM backup_meta WHERE id = 1")
278
+ .get();
279
+ }
280
+ export function backupHealth(db, dataDir) {
281
+ const meta = readBackupMeta(db);
282
+ const snapshots = listSnapshots(dataDir);
283
+ const summary = `${snapshots.length} snapshot(s), ${formatBytes(totalSizeBytes(snapshots))} total`;
284
+ const newest = snapshots.find((s) => existsSync(join(s.path, "hive.db")));
285
+ if (!newest) {
286
+ return { ok: false, message: `${summary}, nothing to restore from` };
287
+ }
288
+ const dirAt = db.prepare(`SELECT strftime('${SQL_DATETIME_FMT}', ?) AS ts`).get(newest.createdAt.toISOString()).ts;
289
+ let newestAt = dirAt;
290
+ try {
291
+ const snapDb = new SqliteDb(join(newest.path, "hive.db"), { readonly: true });
292
+ try {
293
+ const row = snapDb.prepare("SELECT last_success_at FROM backup_meta WHERE id = 1").get();
294
+ if (row?.last_success_at != null && row.last_success_at > newestAt)
295
+ newestAt = row.last_success_at;
296
+ }
297
+ finally {
298
+ snapDb.close();
299
+ }
300
+ }
301
+ catch {
302
+ }
303
+ const failedMostRecently = meta?.last_error_at != null && meta.last_error_at >= newestAt;
304
+ if (failedMostRecently) {
305
+ return { ok: false, message: `${summary}, last attempt failed at ${meta.last_error_at}: ${meta.last_error}` };
306
+ }
307
+ const staleDays = envInt("HIVE_BACKUP_STALE_DAYS", 7);
308
+ const stale = db
309
+ .prepare(`SELECT (? <= strftime('${SQL_DATETIME_FMT}', 'now', ?)) AS stale`)
310
+ .get(newestAt, `-${staleDays} days`).stale;
311
+ if (stale) {
312
+ return {
313
+ ok: false,
314
+ message: `${summary}, last success ${newestAt} is more than ${staleDays} day(s) old`,
315
+ };
316
+ }
317
+ return { ok: true, message: `${summary}, last success ${newestAt}` };
318
+ }
319
+ export function formatBytes(n) {
320
+ if (n < 1024)
321
+ return `${n}B`;
322
+ const units = ["K", "M", "G"];
323
+ let value = n / 1024;
324
+ let unit = 0;
325
+ while (value >= 1024 && unit < units.length - 1) {
326
+ value /= 1024;
327
+ unit += 1;
328
+ }
329
+ return `${value.toFixed(1)}${units[unit]}`;
330
+ }
331
+ export function findSnapshot(dataDir, name) {
332
+ return listSnapshotRefs(dataDir).find((s) => s.name === name);
333
+ }
334
+ export function previewRestore(dataDir, name) {
335
+ const ref = findSnapshot(dataDir, name);
336
+ if (!ref) {
337
+ const available = listSnapshotRefs(dataDir);
338
+ throw new Error(available.length === 0
339
+ ? `No snapshot named "${name}" (there are no backups in ${backupsDir(dataDir)} yet).`
340
+ : `No snapshot named "${name}". Available: ${available.map((s) => s.name).join(", ")}`);
341
+ }
342
+ const snapshot = { ...ref, sizeBytes: dirSizeBytes(ref.path) };
343
+ const currentDbPath = join(dataDir, "hive.db");
344
+ const currentDbExists = existsSync(currentDbPath);
345
+ return {
346
+ snapshot,
347
+ hasProfiles: existsSync(join(ref.path, "profiles")),
348
+ currentDbPath,
349
+ currentDbExists,
350
+ currentDbSizeBytes: currentDbExists ? statSync(currentDbPath).size : 0,
351
+ };
352
+ }
353
+ export function restoreSnapshot(dataDir, name) {
354
+ const preview = previewRestore(dataDir, name);
355
+ const tmpDbPath = `${preview.currentDbPath}.restoring`;
356
+ cpSync(join(preview.snapshot.path, "hive.db"), tmpDbPath);
357
+ renameSync(tmpDbPath, preview.currentDbPath);
358
+ for (const suffix of ["-wal", "-shm"]) {
359
+ rmSync(preview.currentDbPath + suffix, { force: true });
360
+ }
361
+ const liveProfiles = join(dataDir, "profiles");
362
+ const stagingProfiles = `${liveProfiles}.restoring`;
363
+ const oldProfiles = `${liveProfiles}.old`;
364
+ if (existsSync(oldProfiles) && !existsSync(liveProfiles)) {
365
+ renameSync(oldProfiles, liveProfiles);
366
+ }
367
+ const restoredProfiles = preview.hasProfiles;
368
+ if (restoredProfiles) {
369
+ rmSync(stagingProfiles, { recursive: true, force: true });
370
+ rmSync(oldProfiles, { recursive: true, force: true });
371
+ cpSync(join(preview.snapshot.path, "profiles"), stagingProfiles, { recursive: true });
372
+ if (existsSync(liveProfiles))
373
+ renameSync(liveProfiles, oldProfiles);
374
+ renameSync(stagingProfiles, liveProfiles);
375
+ rmSync(oldProfiles, { recursive: true, force: true });
376
+ }
377
+ return { restoredProfiles };
378
+ }
package/dist/brief.js ADDED
@@ -0,0 +1,113 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { gitPrimaryRoot } from "./context.js";
4
+ import { storeDir } from "./dataDir.js";
5
+ import { carriesNameFlag, harnessFor, harnessNames } from "./harnesses.js";
6
+ import { agentVarKeys } from "./projectYml.js";
7
+ import { readProfileFile, renderTemplate } from "./profiles.js";
8
+ import { withTrailingNewline } from "./result.js";
9
+ import { shellQuote } from "./tmux.js";
10
+ // A pure git fact, not a convention hive owns: gitPrimaryRoot asks git directly with no
11
+ // containment, and resolves the same primary checkout whether cwd is that checkout or a linked
12
+ // worktree of it. What a project keeps there (a session corpus, local notes, nothing at all) is a
13
+ // profile's business, never core's - see docs/patterns.md.
14
+ export function primaryRoot(cwd) {
15
+ const root = gitPrimaryRoot(cwd);
16
+ return root ? `${root}/` : null;
17
+ }
18
+ function briefVars(ctx) {
19
+ const root = primaryRoot(ctx.cwd);
20
+ return {
21
+ ...(ctx.vars ?? {}),
22
+ agent_name: ctx.name,
23
+ actor_id: ctx.actorId,
24
+ project_name: ctx.projectName,
25
+ project_path: ctx.projectPath,
26
+ cwd: ctx.cwd,
27
+ ...(root ? { primary_root: root } : {}),
28
+ };
29
+ }
30
+ const WAIT_FOR_ASSIGNMENT = "Run whoami to confirm scope, then wait for your assignment.";
31
+ // harness_claude is the default for anything that is not codex, not an equality check against
32
+ // "claude" - a presence-conditional has no else, so an unrecognised harness still needs one.
33
+ export function harnessBriefVars(harnessName) {
34
+ return harnessName === "codex" ? { harness_codex: "1" } : { harness_claude: "1" };
35
+ }
36
+ // Derived from harnessNames() rather than a hand-kept literal, so a newly registered harness
37
+ // extends the strip set with no edit here (todo 597 review finding).
38
+ function harnessVarKeys() {
39
+ return harnessNames().map((name) => `harness_${name}`);
40
+ }
41
+ // Strips harness_* and agents_* reserved keys before spreading harnessBriefVars - strip only,
42
+ // worker.md never gets an agents_* value, since nothing needs one there yet.
43
+ export function mergedBriefVars(projectVars, harnessName) {
44
+ const safe = { ...(projectVars ?? {}) };
45
+ for (const key of [...harnessVarKeys(), ...agentVarKeys()])
46
+ delete safe[key];
47
+ return { ...safe, ...harnessBriefVars(harnessName) };
48
+ }
49
+ export function workerBrief(ctx) {
50
+ if (ctx.profile) {
51
+ const template = readProfileFile(ctx.profile, "worker.md");
52
+ if (template != null)
53
+ return `${renderTemplate(template, briefVars(ctx)).trimEnd()}\n\n${WAIT_FOR_ASSIGNMENT}`;
54
+ }
55
+ return defaultWorkerBrief(ctx);
56
+ }
57
+ function defaultWorkerBrief(ctx) {
58
+ return `[HIVE CONTEXT]
59
+ You are agent "${ctx.name}" (actor id: ${ctx.actorId}) in project "${ctx.projectName}" (${ctx.projectPath}).
60
+ This session is locked to this project (HIVE_PROJECT_LOCK=1); do not try to access other projects.
61
+ Coordinate through the hive MCP tools:
62
+ - whoami confirms your identity and scope.
63
+ - pad_list / pad_read for the shared plan and findings. Record decisions there.
64
+ - todo_list(is_blocked=false, status="open") for dispatchable work; set status to in_progress while working.
65
+ - todo_comment for handoffs (changed files, tests run, remaining risk), then todo_complete.
66
+ - lease_acquire before editing shared file areas; leases expire on their own.
67
+ If the hive MCP tools are unavailable in this session, write progress and results to stdout; the orchestrator will read your terminal.
68
+ ${WAIT_FOR_ASSIGNMENT}
69
+ [END HIVE CONTEXT]`;
70
+ }
71
+ const briefsDir = () => join(storeDir(), "briefs");
72
+ export const agentBriefPath = (agentId) => join(briefsDir(), `agent-${agentId}.md`);
73
+ export function writeAgentBrief(agentId, text) {
74
+ const path = agentBriefPath(agentId);
75
+ mkdirSync(briefsDir(), { recursive: true });
76
+ writeFileSync(path, withTrailingNewline(text));
77
+ return path;
78
+ }
79
+ export function readAgentBrief(agentId) {
80
+ try {
81
+ return readFileSync(agentBriefPath(agentId), "utf8");
82
+ }
83
+ catch {
84
+ return null;
85
+ }
86
+ }
87
+ const posturesDir = () => join(storeDir(), "postures");
88
+ export const projectPosturePath = (projectId) => join(posturesDir(), `project-${projectId}.md`);
89
+ export function writeProjectPosture(projectId, text) {
90
+ const path = projectPosturePath(projectId);
91
+ mkdirSync(posturesDir(), { recursive: true });
92
+ writeFileSync(path, withTrailingNewline(text));
93
+ return path;
94
+ }
95
+ export { isClaudeCommand } from "./harnesses.js";
96
+ export function workerCommandString(spec) {
97
+ const harness = harnessFor(spec.command);
98
+ const namedByCaller = carriesNameFlag(spec.extraArgs ?? []);
99
+ return [
100
+ // Split, not passed whole: spec.command can itself be multiple tokens (a wrapped command,
101
+ // e.g. "nice claude", todo 521) and shellQuote would otherwise glue the lot into one token a
102
+ // shell can't exec. Byte-identical for every single-token command - see
103
+ // test/worker-brief.test.mjs's negative control.
104
+ ...spec.command.trim().split(/\s+/),
105
+ ...(spec.model ? ["--model", spec.model] : []),
106
+ ...harness.argsFor({ displayName: spec.displayName, namedByCaller }),
107
+ ...(harness.briefDelivery && spec.settingsPath ? harness.briefDelivery.settingsArgs(spec.settingsPath) : []),
108
+ ...(harness.briefDelivery && spec.briefPath ? harness.briefDelivery.systemPromptArgs(spec.briefPath) : []),
109
+ ...(spec.extraArgs ?? []),
110
+ ]
111
+ .map(shellQuote)
112
+ .join(" ");
113
+ }
@@ -0,0 +1 @@
1
+ {"version":"0.1.0","sha":"de83214","dirty":false}
@@ -0,0 +1,5 @@
1
+ import { homedir } from "node:os";
2
+ import { join, resolve } from "node:path";
3
+ export function claudeConfigDir() {
4
+ return process.env.CLAUDE_CONFIG_DIR ? resolve(process.env.CLAUDE_CONFIG_DIR) : join(homedir(), ".claude");
5
+ }