@titan-design/active-work 0.2.0 → 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.
@@ -30,30 +30,24 @@ function getInitiativeDir(slug) {
30
30
  function getLockPath(slug) {
31
31
  return path.join(getInitiativeDir(slug), ".lock");
32
32
  }
33
-
34
- // src/registry/json-envelope.ts
35
- function successEnvelope(data, warnings) {
36
- if (warnings && warnings.length > 0) {
37
- return { ok: true, data, warnings };
38
- }
39
- return { ok: true, data };
40
- }
41
- function errorEnvelope(error, code) {
42
- return { ok: false, error, code };
33
+ function getMinerRoot() {
34
+ return path.join(getActiveRoot(), ".miner");
43
35
  }
44
36
 
37
+ // src/registry/index.ts
38
+ import { createRegistry } from "@titan-design/registry";
39
+
45
40
  // src/registry/types.ts
41
+ import { defineCommand as pkgDefineCommand } from "@titan-design/registry";
46
42
  function defineCommand(cmd) {
47
- return cmd;
43
+ return pkgDefineCommand(cmd);
48
44
  }
49
45
 
50
46
  // src/registry/index.ts
51
- var registry = /* @__PURE__ */ new Map();
47
+ import { successEnvelope, errorEnvelope } from "@titan-design/registry";
48
+ var registry = createRegistry();
52
49
  function register(cmd) {
53
- if (registry.has(cmd.name)) {
54
- throw new Error(`Command already registered: ${cmd.name}`);
55
- }
56
- registry.set(cmd.name, cmd);
50
+ registry.register(cmd);
57
51
  }
58
52
 
59
53
  // src/errors.ts
@@ -125,9 +119,39 @@ function formatError(err) {
125
119
  return { message: String(err), code: EXIT.GENERIC };
126
120
  }
127
121
 
128
- // src/commands/open.ts
129
- import path14 from "path";
130
- import { z as z8 } from "zod";
122
+ // src/launcher-args.ts
123
+ function mergeChannels(defaultChannels, briefChannels) {
124
+ const merged = [...defaultChannels ?? [], ...briefChannels ?? []];
125
+ return [...new Set(merged)];
126
+ }
127
+ function buildChannelArgs(channels) {
128
+ if (!channels || channels.length === 0) return [];
129
+ const targets = channels.map((raw) => /^(server|plugin):/.test(raw) ? raw : `server:${raw}`);
130
+ const plugins = targets.filter((t) => t.startsWith("plugin:"));
131
+ const servers = targets.filter((t) => !t.startsWith("plugin:"));
132
+ return [
133
+ ...plugins.length > 0 ? ["--channels", ...plugins] : [],
134
+ ...servers.length > 0 ? ["--dangerously-load-development-channels", ...servers] : []
135
+ ];
136
+ }
137
+ function buildClaudeArgs(prompt, channels) {
138
+ return [...buildChannelArgs(channels), "--", prompt];
139
+ }
140
+ var ADHOC_FLAGS = ["--adhoc", "--ad-hoc"];
141
+ function parseLauncherFlags(args) {
142
+ const known = /* @__PURE__ */ new Set(["--pick", ...ADHOC_FLAGS]);
143
+ const positional = args.filter((a) => !known.has(a));
144
+ return {
145
+ pick: args.includes("--pick"),
146
+ adhoc: args.some((a) => ADHOC_FLAGS.includes(a)),
147
+ positional,
148
+ usageError: positional.some((a) => a.startsWith("-")) || positional.length > 1
149
+ };
150
+ }
151
+
152
+ // src/commands/_open-helpers.ts
153
+ import { promises as fs11 } from "fs";
154
+ import path11 from "path";
131
155
 
132
156
  // src/schemas/brief.ts
133
157
  import { z } from "zod";
@@ -161,7 +185,9 @@ var BriefFrontmatterSchema = z.object({
161
185
  // High-water mark for task ids: the largest numeric suffix ever issued
162
186
  // for this initiative's task_prefix. Optional so pre-existing brief.md
163
187
  // files (written before this field existed) keep validating; task.add
164
- // falls back to scanning on-disk task files when it's absent.
188
+ // falls back to scanning on-disk task files when it's absent. Only
189
+ // task.delete writes this field (AW-94) — and only when removing the
190
+ // current highest id — so task.add itself never rewrites brief.md.
165
191
  task_seq: TaskSeqSchema.optional()
166
192
  }).superRefine((value, ctx) => {
167
193
  if (value.state === "focused" && value.rank === void 0) {
@@ -434,8 +460,8 @@ async function writeArtifactsFile(initiativeDir, artifacts) {
434
460
  }
435
461
 
436
462
  // src/bootstrap/prompt.ts
437
- import { promises as fs11 } from "fs";
438
- import path11 from "path";
463
+ import { promises as fs10 } from "fs";
464
+ import path10 from "path";
439
465
 
440
466
  // src/schemas/task.ts
441
467
  import { z as z3 } from "zod";
@@ -528,7 +554,13 @@ var SessionFrontmatterSchema = z4.object({
528
554
  resolves: z4.array(SessionResolveSchema).default([]),
529
555
  // Written only by `wrap --no-loops`. An empty ledger alone cannot say
530
556
  // whether nothing was hanging or nothing was filed; this marker does.
531
- no_loops: z4.literal(true).optional()
557
+ no_loops: z4.literal(true).optional(),
558
+ // The session that spawned this one (AW-26). Set for agent-chat peers,
559
+ // whose parentage is known only to the spawning hook — a peer runs as its
560
+ // own `claude` process, so nothing in its transcript records who asked for
561
+ // it. Built-in subagents are linked in the miner index instead, where the
562
+ // relationship *is* derivable from the transcript tree.
563
+ parent_session_id: SessionIdSchema.optional()
532
564
  }).superRefine((value, ctx) => {
533
565
  const started = new Date(value.started).getTime();
534
566
  const ended = new Date(value.ended).getTime();
@@ -983,9 +1015,9 @@ async function writeNoteFile(initiativeDir, frontmatter, body) {
983
1015
  }
984
1016
 
985
1017
  // src/sessions/lease.ts
986
- import { promises as fs10, unlinkSync } from "fs";
1018
+ import { promises as fs9, unlinkSync } from "fs";
987
1019
  import { randomBytes as randomBytes2 } from "crypto";
988
- import path9 from "path";
1020
+ import path8 from "path";
989
1021
 
990
1022
  // src/schemas/lease.ts
991
1023
  import { z as z7 } from "zod";
@@ -1006,95 +1038,47 @@ var LeaseSchema = z7.object({
1006
1038
  mode: LeaseModeSchema,
1007
1039
  /** Present only for `launcher` leases — the `aw` process to probe. */
1008
1040
  pid: z7.number().int().positive().optional(),
1041
+ /**
1042
+ * `pid`'s command name (e.g. `node`) at the moment the lease was written.
1043
+ * Lets liveness checking tell "still the same process" apart from "the OS
1044
+ * recycled this pid onto something unrelated" — `kill(pid, 0)` alone can't.
1045
+ * Best-effort: absent when the lookup failed, in which case liveness falls
1046
+ * back to the pid check alone.
1047
+ */
1048
+ pid_comm: z7.string().min(1).optional(),
1009
1049
  started: iso86012,
1010
1050
  /** Human/role hint carried for future use (e.g. an agent-chat name). */
1011
1051
  label: z7.string().min(1).optional()
1012
1052
  });
1013
1053
 
1014
1054
  // src/server/lifecycle.ts
1015
- import { promises as fs9 } from "fs";
1016
- import path8 from "path";
1017
- function pidPath() {
1018
- return path8.join(getStateRoot(), "daemon.pid");
1019
- }
1020
- function metaPath() {
1021
- return path8.join(getStateRoot(), "daemon.meta.json");
1022
- }
1023
- async function ensureStateDir() {
1024
- await fs9.mkdir(getStateRoot(), { recursive: true });
1025
- }
1026
- async function writePidFile(pid, meta) {
1027
- await ensureStateDir();
1028
- await fs9.writeFile(pidPath(), String(pid), "utf8");
1029
- await fs9.writeFile(metaPath(), JSON.stringify(meta, null, 2), "utf8");
1055
+ import {
1056
+ daemonPaths,
1057
+ probeHealth as pkgProbeHealth,
1058
+ readPidFile as pkgReadPidFile,
1059
+ removePidFile as pkgRemovePidFile,
1060
+ writePidFile as pkgWritePidFile
1061
+ } from "@titan-design/daemon";
1062
+ import { DEFAULT_DAEMON_PORT, getProcessCommand, isProcessAlive } from "@titan-design/daemon";
1063
+ function paths2() {
1064
+ return daemonPaths(getStateRoot());
1030
1065
  }
1031
1066
  async function readPidFile() {
1032
- let pidRaw;
1033
- try {
1034
- pidRaw = await fs9.readFile(pidPath(), "utf8");
1035
- } catch (err) {
1036
- if (err.code === "ENOENT") return null;
1037
- throw err;
1038
- }
1039
- const pid = Number.parseInt(pidRaw.trim(), 10);
1040
- if (!Number.isFinite(pid)) return null;
1041
- let metaRaw;
1042
- try {
1043
- metaRaw = await fs9.readFile(metaPath(), "utf8");
1044
- } catch (err) {
1045
- if (err.code !== "ENOENT") throw err;
1046
- }
1047
- const meta = metaRaw ? JSON.parse(metaRaw) : { port: 0, version: "unknown", started: "" };
1048
- return { pid, meta };
1067
+ return pkgReadPidFile(paths2());
1049
1068
  }
1050
1069
  async function removePidFile(expectedPid) {
1051
- const current = await readPidFile();
1052
- if (current === null) return false;
1053
- if (current.pid !== expectedPid) return false;
1054
- for (const p of [pidPath(), metaPath()]) {
1055
- try {
1056
- await fs9.unlink(p);
1057
- } catch (err) {
1058
- if (err.code !== "ENOENT") throw err;
1059
- }
1060
- }
1061
- return true;
1070
+ return pkgRemovePidFile(paths2(), expectedPid);
1062
1071
  }
1063
- var DEFAULT_DAEMON_PORT = 7400;
1064
1072
  function resolveDaemonPort() {
1065
1073
  const envPort = process.env.AW_PORT;
1066
1074
  if (envPort) {
1067
1075
  const n = Number.parseInt(envPort, 10);
1068
1076
  if (Number.isFinite(n)) return n;
1069
1077
  }
1070
- return DEFAULT_DAEMON_PORT;
1078
+ return 7400;
1071
1079
  }
1072
- var HEALTH_TIMEOUT_MS = 500;
1073
1080
  async function probeHealth(port) {
1074
- const controller = new AbortController();
1075
- const timer = setTimeout(() => controller.abort(), HEALTH_TIMEOUT_MS);
1076
- try {
1077
- const res = await fetch(`http://127.0.0.1:${port}/health`, {
1078
- signal: controller.signal
1079
- });
1080
- if (!res.ok) return null;
1081
- return await res.json();
1082
- } catch {
1083
- return null;
1084
- } finally {
1085
- clearTimeout(timer);
1086
- }
1087
- }
1088
- function isProcessAlive(pid) {
1089
- if (!Number.isFinite(pid) || pid <= 0) return false;
1090
- try {
1091
- process.kill(pid, 0);
1092
- return true;
1093
- } catch (err) {
1094
- const code = err.code;
1095
- if (code === "EPERM") return true;
1096
- return false;
1097
- }
1081
+ return await pkgProbeHealth(port);
1098
1082
  }
1099
1083
 
1100
1084
  // src/sessions/lease.ts
@@ -1102,25 +1086,36 @@ var ONESHOT_TTL_MS = 90 * 6e4;
1102
1086
  var LAUNCHER_MAX_AGE_MS = 36 * 60 * 6e4;
1103
1087
  var LEASE_DIR_NAME = ".sessions";
1104
1088
  function leaseDir(activeRoot, slug) {
1105
- return path9.join(activeRoot, LEASE_DIR_NAME, slug);
1089
+ return path8.join(activeRoot, LEASE_DIR_NAME, slug);
1106
1090
  }
1107
1091
  function leasePath(activeRoot, slug, leaseId) {
1108
- return path9.join(leaseDir(activeRoot, slug), `${leaseId}.json`);
1092
+ return path8.join(leaseDir(activeRoot, slug), `${leaseId}.json`);
1109
1093
  }
1110
1094
  async function acquireLease(input) {
1111
- const { activeRoot, slug, cwd, mode, pid, label, now = /* @__PURE__ */ new Date() } = input;
1095
+ const {
1096
+ activeRoot,
1097
+ slug,
1098
+ cwd,
1099
+ mode,
1100
+ pid,
1101
+ label,
1102
+ now = /* @__PURE__ */ new Date(),
1103
+ getComm = getProcessCommand
1104
+ } = input;
1112
1105
  const leaseId = randomBytes2(8).toString("hex");
1106
+ const pidComm = mode === "launcher" && pid !== void 0 ? getComm(pid) : null;
1113
1107
  const lease = LeaseSchema.parse({
1114
1108
  lease_id: leaseId,
1115
1109
  slug,
1116
1110
  cwd,
1117
1111
  mode,
1118
1112
  ...mode === "launcher" && pid !== void 0 ? { pid } : {},
1113
+ ...pidComm ? { pid_comm: pidComm } : {},
1119
1114
  started: now.toISOString(),
1120
1115
  ...label ? { label } : {}
1121
1116
  });
1122
- await fs10.mkdir(leaseDir(activeRoot, slug), { recursive: true });
1123
- await fs10.writeFile(leasePath(activeRoot, slug, leaseId), JSON.stringify(lease, null, 2), "utf8");
1117
+ await fs9.mkdir(leaseDir(activeRoot, slug), { recursive: true });
1118
+ await fs9.writeFile(leasePath(activeRoot, slug, leaseId), JSON.stringify(lease, null, 2), "utf8");
1124
1119
  return {
1125
1120
  leaseId,
1126
1121
  release: () => releaseLease(activeRoot, slug, leaseId)
@@ -1132,7 +1127,7 @@ function isIgnorableUnlinkError(err) {
1132
1127
  }
1133
1128
  async function releaseLease(activeRoot, slug, leaseId) {
1134
1129
  try {
1135
- await fs10.unlink(leasePath(activeRoot, slug, leaseId));
1130
+ await fs9.unlink(leasePath(activeRoot, slug, leaseId));
1136
1131
  } catch (err) {
1137
1132
  if (!isIgnorableUnlinkError(err)) throw err;
1138
1133
  }
@@ -1143,12 +1138,14 @@ function releaseLeaseSync(activeRoot, slug, leaseId) {
1143
1138
  } catch {
1144
1139
  }
1145
1140
  }
1146
- function isLive(lease, now, isAlive) {
1141
+ function isLive(lease, now, isAlive, getComm) {
1147
1142
  const ageMs = now.getTime() - new Date(lease.started).getTime();
1148
1143
  if (lease.mode === "oneshot") return ageMs < ONESHOT_TTL_MS;
1149
1144
  if (lease.pid === void 0) return false;
1150
1145
  if (ageMs >= LAUNCHER_MAX_AGE_MS) return false;
1151
- return isAlive(lease.pid);
1146
+ if (!isAlive(lease.pid)) return false;
1147
+ if (lease.pid_comm === void 0) return true;
1148
+ return getComm(lease.pid) === lease.pid_comm;
1152
1149
  }
1153
1150
  function toSibling(lease) {
1154
1151
  return {
@@ -1162,7 +1159,7 @@ function toSibling(lease) {
1162
1159
  }
1163
1160
  async function readOneLease(file) {
1164
1161
  try {
1165
- const raw = await fs10.readFile(file, "utf8");
1162
+ const raw = await fs9.readFile(file, "utf8");
1166
1163
  const parsed = LeaseSchema.safeParse(JSON.parse(raw));
1167
1164
  return parsed.success ? parsed.data : null;
1168
1165
  } catch {
@@ -1171,26 +1168,33 @@ async function readOneLease(file) {
1171
1168
  }
1172
1169
  async function unlinkQuietly(file) {
1173
1170
  try {
1174
- await fs10.unlink(file);
1171
+ await fs9.unlink(file);
1175
1172
  } catch {
1176
1173
  }
1177
1174
  }
1178
1175
  async function readLiveLeases(input) {
1179
- const { activeRoot, slug, now = /* @__PURE__ */ new Date(), excludeLeaseId, isAlive = isProcessAlive } = input;
1176
+ const {
1177
+ activeRoot,
1178
+ slug,
1179
+ now = /* @__PURE__ */ new Date(),
1180
+ excludeLeaseId,
1181
+ isAlive = isProcessAlive,
1182
+ getComm = getProcessCommand
1183
+ } = input;
1180
1184
  try {
1181
1185
  const dir = leaseDir(activeRoot, slug);
1182
1186
  let entries;
1183
1187
  try {
1184
- entries = await fs10.readdir(dir);
1188
+ entries = await fs9.readdir(dir);
1185
1189
  } catch {
1186
1190
  return [];
1187
1191
  }
1188
1192
  const live = [];
1189
1193
  for (const name of entries) {
1190
1194
  if (!name.endsWith(".json")) continue;
1191
- const file = path9.join(dir, name);
1195
+ const file = path8.join(dir, name);
1192
1196
  const lease = await readOneLease(file);
1193
- if (!lease || !isLive(lease, now, isAlive)) {
1197
+ if (!lease || !isLive(lease, now, isAlive, getComm)) {
1194
1198
  await unlinkQuietly(file);
1195
1199
  continue;
1196
1200
  }
@@ -1203,10 +1207,10 @@ async function readLiveLeases(input) {
1203
1207
  }
1204
1208
  }
1205
1209
  async function sweepAllLeases(activeRoot, options = {}) {
1206
- const root = path9.join(activeRoot, LEASE_DIR_NAME);
1210
+ const root = path8.join(activeRoot, LEASE_DIR_NAME);
1207
1211
  let slugs;
1208
1212
  try {
1209
- const entries = await fs10.readdir(root, { withFileTypes: true });
1213
+ const entries = await fs9.readdir(root, { withFileTypes: true });
1210
1214
  slugs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
1211
1215
  } catch (err) {
1212
1216
  const code = err.code;
@@ -1217,7 +1221,7 @@ async function sweepAllLeases(activeRoot, options = {}) {
1217
1221
  let before = 0;
1218
1222
  for (const slug of slugs) {
1219
1223
  try {
1220
- const names = await fs10.readdir(leaseDir(activeRoot, slug));
1224
+ const names = await fs9.readdir(leaseDir(activeRoot, slug));
1221
1225
  before += names.filter((n) => n.endsWith(".json")).length;
1222
1226
  live += (await readLiveLeases({ activeRoot, slug, ...options })).length;
1223
1227
  } catch (err) {
@@ -1229,7 +1233,7 @@ async function sweepAllLeases(activeRoot, options = {}) {
1229
1233
 
1230
1234
  // src/utils/git-gh.ts
1231
1235
  import { spawn } from "child_process";
1232
- import path10 from "path";
1236
+ import path9 from "path";
1233
1237
  var DEFAULT_TIMEOUT_MS = 1e4;
1234
1238
  var defaultRunner = (bin, args, opts = {}) => new Promise((resolve, reject) => {
1235
1239
  const child = spawn(bin, args, {
@@ -1281,7 +1285,7 @@ function looksLikeOrgRepo(repo) {
1281
1285
  }
1282
1286
  function resolveLocalRepoPath(repo) {
1283
1287
  if (looksLikeOrgRepo(repo)) return null;
1284
- return path10.resolve(expandTilde(repo));
1288
+ return path9.resolve(expandTilde(repo));
1285
1289
  }
1286
1290
  async function deriveOrgRepoFromPath(repoPath) {
1287
1291
  try {
@@ -1324,7 +1328,7 @@ var MS_PER_HOUR = 1e3 * 60 * 60;
1324
1328
  var MS_PER_DAY2 = MS_PER_HOUR * 24;
1325
1329
  var FRONTMATTER_DELIM2 = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
1326
1330
  async function readMarkdownWithSchema(filePath, schema) {
1327
- const raw = await fs11.readFile(filePath, "utf8");
1331
+ const raw = await fs10.readFile(filePath, "utf8");
1328
1332
  const match = FRONTMATTER_DELIM2.exec(raw);
1329
1333
  let frontmatterText = "";
1330
1334
  let body = raw;
@@ -1352,10 +1356,10 @@ function describe2(err) {
1352
1356
  return err instanceof Error ? err.message : String(err);
1353
1357
  }
1354
1358
  async function loadTasks(initiativeDir) {
1355
- const tasksDir = path11.join(initiativeDir, "tasks");
1359
+ const tasksDir = path10.join(initiativeDir, "tasks");
1356
1360
  let entries;
1357
1361
  try {
1358
- entries = await fs11.readdir(tasksDir);
1362
+ entries = await fs10.readdir(tasksDir);
1359
1363
  } catch {
1360
1364
  return { tasks: [], malformed: [] };
1361
1365
  }
@@ -1363,7 +1367,7 @@ async function loadTasks(initiativeDir) {
1363
1367
  const tasks = [];
1364
1368
  const malformed = [];
1365
1369
  for (const filename of ymlFiles) {
1366
- const fullPath = path11.join(tasksDir, filename);
1370
+ const fullPath = path10.join(tasksDir, filename);
1367
1371
  try {
1368
1372
  tasks.push(await readYaml(fullPath, TaskSchema));
1369
1373
  } catch (err) {
@@ -1376,7 +1380,7 @@ function isMissingFile(err) {
1376
1380
  return err?.code === "ENOENT";
1377
1381
  }
1378
1382
  async function loadArtifacts(initiativeDir) {
1379
- const artifactsPath = path11.join(initiativeDir, "artifacts.yml");
1383
+ const artifactsPath = path10.join(initiativeDir, "artifacts.yml");
1380
1384
  const empty = { branches: [], stashes: [], worktrees: [] };
1381
1385
  try {
1382
1386
  return { artifacts: await readYaml(artifactsPath, ArtifactsSchema) };
@@ -1879,7 +1883,7 @@ function renderBriefState(brief, now) {
1879
1883
  ${lines.join("\n")}`;
1880
1884
  }
1881
1885
  async function loadBrief(initiativeDir, slug) {
1882
- const briefPath = path11.join(initiativeDir, "brief.md");
1886
+ const briefPath = path10.join(initiativeDir, "brief.md");
1883
1887
  try {
1884
1888
  return await readMarkdownWithSchema(briefPath, BriefFrontmatterSchema);
1885
1889
  } catch (err) {
@@ -1902,7 +1906,7 @@ async function assembleBootstrap(input) {
1902
1906
  siblingProbe = readLiveLeases,
1903
1907
  ownLeaseId
1904
1908
  } = input;
1905
- const initiativeDir = path11.join(activeRoot, slug);
1909
+ const initiativeDir = path10.join(activeRoot, slug);
1906
1910
  const { frontmatter: brief, body: briefBody } = await loadBrief(initiativeDir, slug);
1907
1911
  const [loaded, loadedTasks, loadedArtifacts, notes] = await Promise.all([
1908
1912
  loadSessionsNewestFirst(initiativeDir),
@@ -1919,7 +1923,7 @@ async function assembleBootstrap(input) {
1919
1923
  const narrativeSession = latestCanonical ?? sessions[0];
1920
1924
  const usedFallbackTrack = !latestCanonical && narrativeSession !== void 0;
1921
1925
  const parallelBody = renderParallelSessions(selectParallelSessions(sessions, narrativeSession));
1922
- const briefExcerpt = truncateLines(briefBody, BRIEF_BODY_MAX_LINES, path11.join(initiativeDir, "brief.md")) || "_(no brief body)_";
1926
+ const briefExcerpt = truncateLines(briefBody, BRIEF_BODY_MAX_LINES, path10.join(initiativeDir, "brief.md")) || "_(no brief body)_";
1923
1927
  const { body: tasksBody, count: openTaskCount } = renderTopTasks(tasks, topNTasks, slug);
1924
1928
  const { body: recentlyDoneBody, count: recentlyDoneCount } = renderRecentlyDone(
1925
1929
  tasks,
@@ -1971,7 +1975,7 @@ ${briefExcerpt}`);
1971
1975
  const sessionExcerpt = truncateLines(
1972
1976
  narrativeSession.body,
1973
1977
  SESSION_BODY_MAX_LINES,
1974
- path11.join(initiativeDir, "sessions", `${narrativeSession.sessionFile}.md`)
1978
+ path10.join(initiativeDir, "sessions", `${narrativeSession.sessionFile}.md`)
1975
1979
  ) || "_(empty session body)_";
1976
1980
  const ended = endedDate(narrativeSession.frontmatter.ended);
1977
1981
  const trackLabel = usedFallbackTrack ? ` (${narrativeSession.frontmatter.track})` : "";
@@ -2001,7 +2005,7 @@ Moved ${archivedTaskIds.length} stale done task(s) to tasks/archive/: ${archived
2001
2005
  const notesBody = renderDurableNotes(notes, slug);
2002
2006
  if (notesBody) sections.push(notesBody);
2003
2007
  if (artifactsError) {
2004
- const artifactsPath = path11.join(initiativeDir, "artifacts.yml");
2008
+ const artifactsPath = path10.join(initiativeDir, "artifacts.yml");
2005
2009
  sections.push(
2006
2010
  `# Open artifacts
2007
2011
  _${artifactsPath} exists but could not be read (${artifactsError}). Branch and stash context is MISSING from this bootstrap \u2014 do not treat the working tree as clean. Run \`active-work doctor\`._`
@@ -2045,54 +2049,11 @@ ${contextLines.join("\n")}`);
2045
2049
  return { prompt, metadata };
2046
2050
  }
2047
2051
 
2048
- // src/bootstrap/archive-tasks.ts
2049
- import { promises as fsp } from "fs";
2050
- import path12 from "path";
2051
- var MS_PER_DAY3 = 864e5;
2052
- async function archiveStaleTasks(initiativeDir, opts) {
2053
- if (!(opts.retentionDays > 0)) return [];
2054
- const tasksDir = path12.join(initiativeDir, "tasks");
2055
- let entries;
2056
- try {
2057
- entries = await fsp.readdir(tasksDir);
2058
- } catch {
2059
- return [];
2060
- }
2061
- const ymlFiles = entries.filter((n) => n.endsWith(".yml") || n.endsWith(".yaml"));
2062
- const cutoffMs = opts.now.getTime() - opts.retentionDays * MS_PER_DAY3;
2063
- const archiveDir = path12.join(tasksDir, "archive");
2064
- const archived = [];
2065
- for (const filename of ymlFiles) {
2066
- const fullPath = path12.join(tasksDir, filename);
2067
- let doneAt;
2068
- let id;
2069
- try {
2070
- const task = await readYaml(fullPath, TaskSchema);
2071
- if (task.status !== "done" || !task.done_at) continue;
2072
- doneAt = task.done_at;
2073
- id = task.id;
2074
- } catch {
2075
- continue;
2076
- }
2077
- const doneMs = new Date(doneAt).getTime();
2078
- if (Number.isNaN(doneMs) || doneMs > cutoffMs) continue;
2079
- try {
2080
- await fsp.mkdir(archiveDir, { recursive: true });
2081
- await fsp.rename(fullPath, path12.join(archiveDir, filename));
2082
- archived.push(id);
2083
- } catch {
2084
- }
2085
- }
2086
- return archived.sort();
2087
- }
2088
-
2089
2052
  // src/commands/_open-helpers.ts
2090
- import { promises as fs12 } from "fs";
2091
- import path13 from "path";
2092
2053
  async function listInitiativeSlugs(activeRoot) {
2093
2054
  let entries;
2094
2055
  try {
2095
- entries = await fs12.readdir(activeRoot, { withFileTypes: true });
2056
+ entries = await fs11.readdir(activeRoot, { withFileTypes: true });
2096
2057
  } catch {
2097
2058
  return [];
2098
2059
  }
@@ -2111,15 +2072,23 @@ async function resolveSlug(activeRoot, input) {
2111
2072
  }
2112
2073
  throw new NotFoundError(`No initiative matches '${input}'. Known: ${slugs.join(", ")}`);
2113
2074
  }
2075
+ function resolveLaunchCwd(activeRoot, slug) {
2076
+ return path11.join(activeRoot, slug);
2077
+ }
2078
+ async function resolveCwdHint(activeRoot, slug) {
2079
+ const registered = await readRegisteredWorktrees(path11.join(activeRoot, slug));
2080
+ const preferred = defaultWorktreePath(registered);
2081
+ return preferred === null ? path11.join(activeRoot, slug) : expandTilde(preferred);
2082
+ }
2114
2083
  function isInside(child, parent) {
2115
- const rel = path13.relative(parent, child);
2116
- return rel === "" || !rel.startsWith("..") && !path13.isAbsolute(rel);
2084
+ const rel = path11.relative(parent, child);
2085
+ return rel === "" || !rel.startsWith("..") && !path11.isAbsolute(rel);
2117
2086
  }
2118
2087
  async function canonicalize(p) {
2119
2088
  try {
2120
- return await fs12.realpath(p);
2089
+ return await fs11.realpath(p);
2121
2090
  } catch {
2122
- return path13.resolve(p);
2091
+ return path11.resolve(p);
2123
2092
  }
2124
2093
  }
2125
2094
  async function resolveSlugFromCwd(activeRoot, cwd) {
@@ -2128,16 +2097,16 @@ async function resolveSlugFromCwd(activeRoot, cwd) {
2128
2097
  let best = null;
2129
2098
  let tiedAtBest = false;
2130
2099
  for (const slug of slugs) {
2131
- const briefPath = path13.join(activeRoot, slug, "brief.md");
2100
+ const briefPath = path11.join(activeRoot, slug, "brief.md");
2132
2101
  try {
2133
2102
  await readMarkdownWithSchema(briefPath, BriefFrontmatterSchema);
2134
2103
  } catch {
2135
2104
  continue;
2136
2105
  }
2137
- const registered = await readRegisteredWorktrees(path13.join(activeRoot, slug));
2106
+ const registered = await readRegisteredWorktrees(path11.join(activeRoot, slug));
2138
2107
  for (const entry of registered) {
2139
2108
  const displayPath = expandTilde(entry.path);
2140
- if (!path13.isAbsolute(displayPath)) continue;
2109
+ if (!path11.isAbsolute(displayPath)) continue;
2141
2110
  const canonical = await canonicalize(displayPath);
2142
2111
  if (!isInside(resolvedCwd, canonical)) continue;
2143
2112
  const depth = canonical.length;
@@ -2153,57 +2122,131 @@ async function resolveSlugFromCwd(activeRoot, cwd) {
2153
2122
  return { slug: best.slug, worktreePath: best.worktreePath };
2154
2123
  }
2155
2124
 
2125
+ // src/commands/open.ts
2126
+ import path14 from "path";
2127
+ import { z as z9 } from "zod";
2128
+
2129
+ // src/bootstrap/archive-tasks.ts
2130
+ import { promises as fsp } from "fs";
2131
+ import path12 from "path";
2132
+ var MS_PER_DAY3 = 864e5;
2133
+ async function archiveStaleTasks(initiativeDir, opts) {
2134
+ if (!(opts.retentionDays > 0)) return [];
2135
+ const tasksDir = path12.join(initiativeDir, "tasks");
2136
+ let entries;
2137
+ try {
2138
+ entries = await fsp.readdir(tasksDir);
2139
+ } catch {
2140
+ return [];
2141
+ }
2142
+ const ymlFiles = entries.filter((n) => n.endsWith(".yml") || n.endsWith(".yaml"));
2143
+ const cutoffMs = opts.now.getTime() - opts.retentionDays * MS_PER_DAY3;
2144
+ const archiveDir = path12.join(tasksDir, "archive");
2145
+ const archived = [];
2146
+ for (const filename of ymlFiles) {
2147
+ const fullPath = path12.join(tasksDir, filename);
2148
+ let doneAt;
2149
+ let id;
2150
+ try {
2151
+ const task = await readYaml(fullPath, TaskSchema);
2152
+ if (task.status !== "done" || !task.done_at) continue;
2153
+ doneAt = task.done_at;
2154
+ id = task.id;
2155
+ } catch {
2156
+ continue;
2157
+ }
2158
+ const doneMs = new Date(doneAt).getTime();
2159
+ if (Number.isNaN(doneMs) || doneMs > cutoffMs) continue;
2160
+ try {
2161
+ await fsp.mkdir(archiveDir, { recursive: true });
2162
+ await fsp.rename(fullPath, path12.join(archiveDir, filename));
2163
+ archived.push(id);
2164
+ } catch {
2165
+ }
2166
+ }
2167
+ return archived.sort();
2168
+ }
2169
+
2170
+ // src/utils/global-config.ts
2171
+ import { promises as fs12 } from "fs";
2172
+ import path13 from "path";
2173
+ import { z as z8 } from "zod";
2174
+ var GlobalConfigSchema = z8.object({
2175
+ channels: z8.array(channelTarget).optional()
2176
+ });
2177
+ var FALLBACK_DEFAULT_CHANNELS = ["plugin:agent-chat@agent-chat-local"];
2178
+ async function readGlobalConfig(configRoot = getConfigRoot()) {
2179
+ let raw;
2180
+ try {
2181
+ raw = await fs12.readFile(path13.join(configRoot, "config.json"), "utf8");
2182
+ } catch {
2183
+ return {};
2184
+ }
2185
+ let parsed;
2186
+ try {
2187
+ parsed = JSON.parse(raw);
2188
+ } catch {
2189
+ return {};
2190
+ }
2191
+ const result = GlobalConfigSchema.safeParse(parsed);
2192
+ return result.success ? result.data : {};
2193
+ }
2194
+ async function resolveDefaultChannels(configRoot) {
2195
+ const config = await readGlobalConfig(configRoot);
2196
+ return config.channels && config.channels.length > 0 ? config.channels : FALLBACK_DEFAULT_CHANNELS;
2197
+ }
2198
+
2156
2199
  // src/commands/open.ts
2157
2200
  var ARCHIVE_DONE_AFTER_DAYS = 30;
2158
- var ArgsSchema2 = z8.object({
2159
- slug: z8.string().min(1).optional(),
2160
- offline: z8.boolean().optional(),
2201
+ var ArgsSchema2 = z9.object({
2202
+ slug: z9.string().min(1).optional(),
2203
+ offline: z9.boolean().optional(),
2161
2204
  // Directory used to auto-resolve an initiative when no slug is given.
2162
2205
  // Defaults to the process cwd; callers that do not share the user's shell
2163
2206
  // cwd (the daemon / MCP server) must pass this explicitly.
2164
- cwd: z8.string().min(1).optional(),
2207
+ cwd: z9.string().min(1).optional(),
2165
2208
  // Force the picker even when the cwd matches an initiative's worktree.
2166
- pick: z8.boolean().optional(),
2209
+ pick: z9.boolean().optional(),
2167
2210
  // Frame the bootstrap prompt as ad-hoc work related to the workstream rather
2168
2211
  // than a continuation of its handoff / top task.
2169
- adhoc: z8.boolean().optional(),
2212
+ adhoc: z9.boolean().optional(),
2170
2213
  // Skip the sibling-session probe (and the lease write that goes with it).
2171
- no_sibling_check: z8.boolean().optional(),
2214
+ no_sibling_check: z9.boolean().optional(),
2172
2215
  // Internal: `aw` calls this command in-process and holds a `launcher` lease
2173
2216
  // of its own for the same session, so it suppresses the oneshot lease here
2174
2217
  // rather than writing a second one that would then look like a sibling.
2175
- lease_mode: z8.literal("defer").optional()
2218
+ lease_mode: z9.literal("defer").optional()
2176
2219
  });
2177
- var InitiativeSummarySchema = z8.object({
2178
- slug: z8.string(),
2179
- title: z8.string(),
2180
- state: z8.enum(["focused", "backburner", "paused", "done"]),
2181
- rank: z8.number().int().positive().optional()
2220
+ var InitiativeSummarySchema = z9.object({
2221
+ slug: z9.string(),
2222
+ title: z9.string(),
2223
+ state: z9.enum(["focused", "backburner", "paused", "done"]),
2224
+ rank: z9.number().int().positive().optional()
2182
2225
  });
2183
- var PickerResultSchema = z8.object({
2184
- picker: z8.literal(true),
2185
- initiatives: z8.array(InitiativeSummarySchema)
2226
+ var PickerResultSchema = z9.object({
2227
+ picker: z9.literal(true),
2228
+ initiatives: z9.array(InitiativeSummarySchema)
2186
2229
  });
2187
- var OpenResultSchema = z8.object({
2188
- slug: z8.string(),
2189
- prompt: z8.string(),
2190
- cwd_hint: z8.string(),
2191
- channels: z8.array(z8.string()).optional(),
2192
- metadata: z8.object({
2193
- slug: z8.string(),
2194
- brief_title: z8.string(),
2195
- last_session: z8.object({ filename: z8.string(), ended: z8.string() }).optional(),
2196
- time_since_last_session_human: z8.string().optional(),
2197
- open_task_count: z8.number().int().nonnegative(),
2198
- recently_done_count: z8.number().int().nonnegative(),
2199
- bootstrap_at: z8.string(),
2200
- sibling_sessions: z8.number().int().nonnegative().optional()
2230
+ var OpenResultSchema = z9.object({
2231
+ slug: z9.string(),
2232
+ prompt: z9.string(),
2233
+ cwd_hint: z9.string(),
2234
+ channels: z9.array(z9.string()).optional(),
2235
+ metadata: z9.object({
2236
+ slug: z9.string(),
2237
+ brief_title: z9.string(),
2238
+ last_session: z9.object({ filename: z9.string(), ended: z9.string() }).optional(),
2239
+ time_since_last_session_human: z9.string().optional(),
2240
+ open_task_count: z9.number().int().nonnegative(),
2241
+ recently_done_count: z9.number().int().nonnegative(),
2242
+ bootstrap_at: z9.string(),
2243
+ sibling_sessions: z9.number().int().nonnegative().optional()
2201
2244
  }),
2202
2245
  // How the initiative was selected: an explicit/prefix slug, or a match
2203
2246
  // between the caller's cwd and one of the initiative's worktrees.
2204
- resolved_from: z8.enum(["slug", "cwd"]).optional()
2247
+ resolved_from: z9.enum(["slug", "cwd"]).optional()
2205
2248
  });
2206
- var ResultSchema2 = z8.union([OpenResultSchema, PickerResultSchema]);
2249
+ var ResultSchema2 = z9.union([OpenResultSchema, PickerResultSchema]);
2207
2250
  var STATE_ORDER = {
2208
2251
  focused: 0,
2209
2252
  backburner: 1,
@@ -2244,11 +2287,6 @@ async function collectInitiatives(activeRoot) {
2244
2287
  summaries.sort(compareInitiatives);
2245
2288
  return summaries;
2246
2289
  }
2247
- async function resolveCwdHint(activeRoot, slug) {
2248
- const registered = await readRegisteredWorktrees(path14.join(activeRoot, slug));
2249
- const preferred = defaultWorktreePath(registered);
2250
- return preferred === null ? path14.join(activeRoot, slug) : expandTilde(preferred);
2251
- }
2252
2290
  async function claimOneshotLease(activeRoot, slug, cwd) {
2253
2291
  try {
2254
2292
  await acquireLease({ activeRoot, slug, cwd, mode: "oneshot" });
@@ -2276,11 +2314,12 @@ async function bootstrapInitiative(activeRoot, slug, opts) {
2276
2314
  if (detectSiblings && !opts.deferLease) {
2277
2315
  await claimOneshotLease(activeRoot, slug, cwdHint);
2278
2316
  }
2317
+ const defaultChannels = await resolveDefaultChannels();
2279
2318
  return {
2280
2319
  slug,
2281
2320
  prompt,
2282
2321
  cwd_hint: cwdHint,
2283
- ...brief.channels && brief.channels.length > 0 ? { channels: brief.channels } : {},
2322
+ channels: mergeChannels(defaultChannels, brief.channels),
2284
2323
  metadata,
2285
2324
  resolved_from: opts.resolvedFrom
2286
2325
  };
@@ -2350,6 +2389,122 @@ var openCommand = defineCommand({
2350
2389
  });
2351
2390
  var open_default = openCommand;
2352
2391
 
2392
+ // src/commands/resume.ts
2393
+ import { z as z10 } from "zod";
2394
+
2395
+ // src/sessions/resolve-session-location.ts
2396
+ import { promises as fs13 } from "fs";
2397
+ import os2 from "os";
2398
+ import path15 from "path";
2399
+ import matter2 from "gray-matter";
2400
+ async function findInActiveWork(activeRoot, sessionId) {
2401
+ for (const slug of await listInitiativeSlugs(activeRoot)) {
2402
+ const sessionsDir = path15.join(activeRoot, slug, "sessions");
2403
+ let filenames;
2404
+ try {
2405
+ filenames = await fs13.readdir(sessionsDir);
2406
+ } catch {
2407
+ continue;
2408
+ }
2409
+ for (const filename of filenames) {
2410
+ if (!filename.endsWith(".md") || !filename.includes(sessionId)) continue;
2411
+ let raw;
2412
+ try {
2413
+ raw = await fs13.readFile(path15.join(sessionsDir, filename), "utf8");
2414
+ } catch {
2415
+ continue;
2416
+ }
2417
+ const { data } = matter2(raw);
2418
+ if (data.session_id === sessionId) {
2419
+ return { slug, cwd: resolveLaunchCwd(activeRoot, slug) };
2420
+ }
2421
+ }
2422
+ }
2423
+ return null;
2424
+ }
2425
+ function transcriptsRoot() {
2426
+ return process.env.CLAUDE_PROJECTS_ROOT ?? path15.join(os2.homedir(), ".claude", "projects");
2427
+ }
2428
+ async function extractCwd(filePath) {
2429
+ const raw = await fs13.readFile(filePath, "utf8");
2430
+ for (const line of raw.split("\n")) {
2431
+ if (!line) continue;
2432
+ let record;
2433
+ try {
2434
+ record = JSON.parse(line);
2435
+ } catch {
2436
+ continue;
2437
+ }
2438
+ if (record && typeof record === "object") {
2439
+ const cwd = record.cwd;
2440
+ if (typeof cwd === "string" && cwd.length > 0) return cwd;
2441
+ }
2442
+ }
2443
+ return null;
2444
+ }
2445
+ async function findInClaudeProjects(sessionId) {
2446
+ const root = transcriptsRoot();
2447
+ let projectDirs;
2448
+ try {
2449
+ projectDirs = await fs13.readdir(root);
2450
+ } catch {
2451
+ return null;
2452
+ }
2453
+ const targetName = `${sessionId}.jsonl`;
2454
+ for (const dir of projectDirs) {
2455
+ const candidate = path15.join(root, dir, targetName);
2456
+ try {
2457
+ await fs13.access(candidate);
2458
+ } catch {
2459
+ continue;
2460
+ }
2461
+ const cwd = await extractCwd(candidate);
2462
+ if (cwd) return cwd;
2463
+ }
2464
+ return null;
2465
+ }
2466
+ async function resolveSessionLocation(activeRoot, sessionId) {
2467
+ const viaActiveWork = await findInActiveWork(activeRoot, sessionId);
2468
+ if (viaActiveWork) {
2469
+ return { cwd: viaActiveWork.cwd, source: "active-work", slug: viaActiveWork.slug };
2470
+ }
2471
+ const viaProjects = await findInClaudeProjects(sessionId);
2472
+ if (viaProjects) {
2473
+ return { cwd: viaProjects, source: "claude-projects" };
2474
+ }
2475
+ return null;
2476
+ }
2477
+
2478
+ // src/commands/resume.ts
2479
+ var ArgsSchema3 = z10.object({
2480
+ session_id: z10.string().min(1)
2481
+ });
2482
+ var ResultSchema3 = z10.object({
2483
+ session_id: z10.string(),
2484
+ cwd: z10.string(),
2485
+ source: z10.enum(["active-work", "claude-projects"]),
2486
+ slug: z10.string().optional()
2487
+ });
2488
+ var resume_default = defineCommand({
2489
+ name: "resume",
2490
+ description: "Resolve the working directory a Claude session id belongs to, so `claude --resume` can be run from the right place.",
2491
+ args: ArgsSchema3,
2492
+ result: ResultSchema3,
2493
+ cli: {
2494
+ positional: ["session_id"],
2495
+ usage: "active-work resume <session_id>"
2496
+ },
2497
+ async run(args, ctx) {
2498
+ const resolved = await resolveSessionLocation(ctx.activeRoot, args.session_id);
2499
+ if (!resolved) {
2500
+ throw new NotFoundError(
2501
+ `No session found for '${args.session_id}' in active-work sessions or ~/.claude/projects.`
2502
+ );
2503
+ }
2504
+ return { session_id: args.session_id, ...resolved };
2505
+ }
2506
+ });
2507
+
2353
2508
  // src/utils/color.ts
2354
2509
  import pc from "picocolors";
2355
2510
  var enabled = !("NO_COLOR" in process.env) && process.stdout.isTTY === true;
@@ -2374,26 +2529,17 @@ export {
2374
2529
  getConfigRoot,
2375
2530
  getInitiativeDir,
2376
2531
  getLockPath,
2377
- BranchEntrySchema,
2378
- StashEntrySchema,
2379
- WorktreeEntrySchema,
2380
- ArtifactsSchema,
2381
- atomicWrite,
2382
- withFileLock,
2383
- hashContent,
2384
- readArtifactHashes,
2385
- readYaml,
2386
- writeYaml,
2387
- readArtifactsFile,
2388
- registeredOf,
2389
- readRegisteredWorktrees,
2390
- writeArtifactsFile,
2532
+ getMinerRoot,
2391
2533
  defineCommand,
2392
- successEnvelope,
2393
- errorEnvelope,
2394
2534
  registry,
2395
2535
  register,
2536
+ successEnvelope,
2537
+ errorEnvelope,
2396
2538
  TaskSchema,
2539
+ BranchEntrySchema,
2540
+ StashEntrySchema,
2541
+ WorktreeEntrySchema,
2542
+ ArtifactsSchema,
2397
2543
  SessionIdSchema,
2398
2544
  NextStepSchema,
2399
2545
  SessionResolveSchema,
@@ -2406,6 +2552,10 @@ export {
2406
2552
  findSessionIssues,
2407
2553
  NoteKindSchema,
2408
2554
  NOTE_TITLE_MAX_LENGTH,
2555
+ atomicWrite,
2556
+ withFileLock,
2557
+ hashContent,
2558
+ readArtifactHashes,
2409
2559
  readFrontmatter,
2410
2560
  readRawFrontmatter,
2411
2561
  writeFrontmatter,
@@ -2422,25 +2572,34 @@ export {
2422
2572
  source_add_default,
2423
2573
  loadNotesFromDir,
2424
2574
  writeNoteFile,
2425
- writePidFile,
2426
2575
  readPidFile,
2427
2576
  removePidFile,
2428
- DEFAULT_DAEMON_PORT,
2429
2577
  resolveDaemonPort,
2430
2578
  probeHealth,
2579
+ DEFAULT_DAEMON_PORT,
2431
2580
  isProcessAlive,
2432
2581
  acquireLease,
2433
2582
  releaseLease,
2434
2583
  releaseLeaseSync,
2435
2584
  sweepAllLeases,
2585
+ readYaml,
2586
+ writeYaml,
2436
2587
  getGitRunner,
2437
2588
  getGhRunner,
2438
2589
  resolveLocalRepoPath,
2439
2590
  resolveOrgRepo,
2440
2591
  assembleBootstrap,
2592
+ buildClaudeArgs,
2593
+ parseLauncherFlags,
2594
+ readArtifactsFile,
2595
+ registeredOf,
2596
+ readRegisteredWorktrees,
2597
+ writeArtifactsFile,
2441
2598
  resolveSlug,
2599
+ resolveLaunchCwd,
2442
2600
  resolveSlugFromCwd,
2443
2601
  open_default,
2602
+ resume_default,
2444
2603
  color
2445
2604
  };
2446
- //# sourceMappingURL=chunk-FM2KVFDO.js.map
2605
+ //# sourceMappingURL=chunk-HSGZOWS3.js.map