@titan-design/active-work 0.1.0 → 0.2.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.
@@ -0,0 +1,2446 @@
1
+ // src/utils/paths.ts
2
+ import os from "os";
3
+ import path from "path";
4
+ import envPaths from "env-paths";
5
+ var PROJECT_NAME = "active-work";
6
+ function paths() {
7
+ return envPaths(PROJECT_NAME, { suffix: "" });
8
+ }
9
+ function expandTilde(p) {
10
+ if (p === "~") return os.homedir();
11
+ if (p.startsWith("~/")) return path.join(os.homedir(), p.slice(2));
12
+ return p;
13
+ }
14
+ function getActiveRoot() {
15
+ const override = process.env.ACTIVE_ROOT;
16
+ if (override && override.length > 0) {
17
+ return path.resolve(expandTilde(override));
18
+ }
19
+ return paths().data;
20
+ }
21
+ function getStateRoot() {
22
+ return paths().log;
23
+ }
24
+ function getConfigRoot() {
25
+ return paths().config;
26
+ }
27
+ function getInitiativeDir(slug) {
28
+ return path.join(getActiveRoot(), slug);
29
+ }
30
+ function getLockPath(slug) {
31
+ return path.join(getInitiativeDir(slug), ".lock");
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 };
43
+ }
44
+
45
+ // src/registry/types.ts
46
+ function defineCommand(cmd) {
47
+ return cmd;
48
+ }
49
+
50
+ // src/registry/index.ts
51
+ var registry = /* @__PURE__ */ new Map();
52
+ 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);
57
+ }
58
+
59
+ // src/errors.ts
60
+ var EXIT = {
61
+ OK: 0,
62
+ GENERIC: 1,
63
+ USAGE: 64,
64
+ // EX_USAGE
65
+ DATAERR: 65,
66
+ // EX_DATAERR — invalid input data / validation
67
+ NOINPUT: 66,
68
+ // EX_NOINPUT — file/initiative not found
69
+ UNAVAILABLE: 69,
70
+ // EX_UNAVAILABLE — daemon unreachable
71
+ SOFTWARE: 70,
72
+ // EX_SOFTWARE — internal bug
73
+ CONFIG: 78
74
+ // EX_CONFIG — bad config
75
+ };
76
+ var ActiveWorkError = class extends Error {
77
+ code = EXIT.GENERIC;
78
+ constructor(message, options) {
79
+ super(message, options);
80
+ this.name = "ActiveWorkError";
81
+ }
82
+ };
83
+ var ValidationError = class extends ActiveWorkError {
84
+ code = EXIT.DATAERR;
85
+ constructor(message, options) {
86
+ super(message, options);
87
+ this.name = "ValidationError";
88
+ }
89
+ };
90
+ var NotFoundError = class extends ActiveWorkError {
91
+ code = EXIT.NOINPUT;
92
+ constructor(message, options) {
93
+ super(message, options);
94
+ this.name = "NotFoundError";
95
+ }
96
+ };
97
+ var UsageError = class extends ActiveWorkError {
98
+ code = EXIT.USAGE;
99
+ constructor(message, options) {
100
+ super(message, options);
101
+ this.name = "UsageError";
102
+ }
103
+ };
104
+ var DaemonError = class extends ActiveWorkError {
105
+ code = EXIT.UNAVAILABLE;
106
+ constructor(message, options) {
107
+ super(message, options);
108
+ this.name = "DaemonError";
109
+ }
110
+ };
111
+ var ConfigError = class extends ActiveWorkError {
112
+ code = EXIT.CONFIG;
113
+ constructor(message, options) {
114
+ super(message, options);
115
+ this.name = "ConfigError";
116
+ }
117
+ };
118
+ function formatError(err) {
119
+ if (err instanceof ActiveWorkError) {
120
+ return { message: err.message, code: err.code };
121
+ }
122
+ if (err instanceof Error) {
123
+ return { message: err.message, code: EXIT.GENERIC };
124
+ }
125
+ return { message: String(err), code: EXIT.GENERIC };
126
+ }
127
+
128
+ // src/commands/open.ts
129
+ import path14 from "path";
130
+ import { z as z8 } from "zod";
131
+
132
+ // src/schemas/brief.ts
133
+ import { z } from "zod";
134
+ var ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
135
+ var isValidIsoDate = (value) => {
136
+ if (!ISO_DATE_REGEX.test(value)) return false;
137
+ const parsed = new Date(value);
138
+ if (Number.isNaN(parsed.getTime())) return false;
139
+ return parsed.toISOString().slice(0, 10) === value;
140
+ };
141
+ var isoDate = z.string().refine(isValidIsoDate, { message: "Must be a valid zero-padded YYYY-MM-DD date" });
142
+ var positiveInt = z.number().int().positive();
143
+ var TaskSeqSchema = positiveInt;
144
+ var channelTarget = z.string().min(1).regex(/^(?:(?:server|plugin):.+|[A-Za-z0-9_-]+)$/, {
145
+ message: 'channel must be a target like "server:voltras", "plugin:name@marketplace", or a bare server name'
146
+ });
147
+ var BriefFrontmatterSchema = z.object({
148
+ schema_version: positiveInt,
149
+ title: z.string().min(1),
150
+ updated: isoDate,
151
+ state: z.enum(["focused", "backburner", "paused", "done"]),
152
+ rank: positiveInt.optional(),
153
+ paused_since: isoDate.optional(),
154
+ restart_trigger: z.string().min(1).optional(),
155
+ ship_target: z.string().optional(),
156
+ owner: z.string().optional(),
157
+ task_prefix: z.string().min(1).regex(/^[A-Z][A-Z0-9]*$/, {
158
+ message: "task_prefix must be uppercase letters/digits starting with a letter"
159
+ }),
160
+ channels: z.array(channelTarget).optional(),
161
+ // High-water mark for task ids: the largest numeric suffix ever issued
162
+ // for this initiative's task_prefix. Optional so pre-existing brief.md
163
+ // files (written before this field existed) keep validating; task.add
164
+ // falls back to scanning on-disk task files when it's absent.
165
+ task_seq: TaskSeqSchema.optional()
166
+ }).superRefine((value, ctx) => {
167
+ if (value.state === "focused" && value.rank === void 0) {
168
+ ctx.addIssue({
169
+ code: "custom",
170
+ path: ["rank"],
171
+ message: 'rank is required when state is "focused"'
172
+ });
173
+ }
174
+ if (value.state === "paused") {
175
+ if (value.paused_since === void 0) {
176
+ ctx.addIssue({
177
+ code: "custom",
178
+ path: ["paused_since"],
179
+ message: 'paused_since is required when state is "paused"'
180
+ });
181
+ }
182
+ if (value.restart_trigger === void 0) {
183
+ ctx.addIssue({
184
+ code: "custom",
185
+ path: ["restart_trigger"],
186
+ message: 'restart_trigger is required when state is "paused"'
187
+ });
188
+ }
189
+ }
190
+ });
191
+
192
+ // src/utils/registered-worktrees.ts
193
+ import { promises as fs4 } from "fs";
194
+ import path4 from "path";
195
+
196
+ // src/schemas/artifacts.ts
197
+ import { z as z2 } from "zod";
198
+ var BranchEntrySchema = z2.object({
199
+ repo: z2.string().min(1),
200
+ name: z2.string().min(1),
201
+ note: z2.string().optional()
202
+ });
203
+ var StashEntrySchema = z2.object({
204
+ repo: z2.string().min(1),
205
+ label: z2.string().min(1),
206
+ sha: z2.string().optional()
207
+ });
208
+ var WorktreeEntrySchema = z2.object({
209
+ path: z2.string().min(1),
210
+ repo: z2.string().min(1),
211
+ branch: z2.string().min(1).optional(),
212
+ holding: z2.string().min(1).optional(),
213
+ pr: z2.number().int().positive().optional(),
214
+ note: z2.string().optional(),
215
+ /** Operator's label. Present only on registered worktrees. */
216
+ name: z2.string().min(1).optional(),
217
+ /** The worktree `aw <slug>` starts in. Only meaningful alongside `name`. */
218
+ default: z2.boolean().optional()
219
+ });
220
+ var ArtifactsSchema = z2.object({
221
+ branches: z2.array(BranchEntrySchema).default([]),
222
+ stashes: z2.array(StashEntrySchema).default([]),
223
+ worktrees: z2.array(WorktreeEntrySchema).default([])
224
+ }).superRefine((value, ctx) => {
225
+ const names = /* @__PURE__ */ new Set();
226
+ let defaults = 0;
227
+ value.worktrees.forEach((entry, i) => {
228
+ if (entry.name !== void 0) {
229
+ if (names.has(entry.name)) {
230
+ ctx.addIssue({
231
+ code: "custom",
232
+ path: ["worktrees", i, "name"],
233
+ message: `duplicate worktree name: ${entry.name}`
234
+ });
235
+ }
236
+ names.add(entry.name);
237
+ }
238
+ if (entry.default === true) {
239
+ defaults += 1;
240
+ if (entry.name === void 0) {
241
+ ctx.addIssue({
242
+ code: "custom",
243
+ path: ["worktrees", i, "default"],
244
+ message: "default requires a named worktree"
245
+ });
246
+ }
247
+ }
248
+ });
249
+ if (defaults > 1) {
250
+ ctx.addIssue({
251
+ code: "custom",
252
+ path: ["worktrees"],
253
+ message: "at most one worktree may be default"
254
+ });
255
+ }
256
+ });
257
+
258
+ // src/utils/yaml-io.ts
259
+ import { promises as fs3 } from "fs";
260
+ import YAML2 from "yaml";
261
+
262
+ // src/utils/artifact-hash.ts
263
+ import { createHash } from "crypto";
264
+ import { promises as fs2 } from "fs";
265
+ import path3 from "path";
266
+ import YAML from "yaml";
267
+
268
+ // src/utils/fs-atomic.ts
269
+ import { randomBytes } from "crypto";
270
+ import { promises as fs } from "fs";
271
+ import path2 from "path";
272
+ import lockfile from "proper-lockfile";
273
+ async function atomicWrite(targetPath, content) {
274
+ const dir = path2.dirname(targetPath);
275
+ const base = path2.basename(targetPath);
276
+ const suffix = `${process.pid}.${randomBytes(6).toString("hex")}`;
277
+ const tempPath = path2.join(dir, `${base}.tmp.${suffix}`);
278
+ let handle;
279
+ try {
280
+ handle = await fs.open(tempPath, "wx");
281
+ await handle.writeFile(content);
282
+ await handle.sync();
283
+ } finally {
284
+ if (handle) await handle.close();
285
+ }
286
+ try {
287
+ await fs.rename(tempPath, targetPath);
288
+ } catch (err) {
289
+ await fs.rm(tempPath, { force: true });
290
+ throw err;
291
+ }
292
+ }
293
+ async function withFileLock(lockTarget, fn) {
294
+ await fs.mkdir(path2.dirname(lockTarget), { recursive: true });
295
+ const release = await lockfile.lock(lockTarget, {
296
+ realpath: false,
297
+ retries: { retries: 5, factor: 1.5, minTimeout: 50 }
298
+ });
299
+ try {
300
+ return await fn();
301
+ } finally {
302
+ await release();
303
+ }
304
+ }
305
+
306
+ // src/utils/artifact-hash.ts
307
+ var MANIFEST_FILENAME = ".artifact-hashes.yml";
308
+ function classifyStructuredArtifact(filePath) {
309
+ const base = path3.basename(filePath);
310
+ const dir = path3.dirname(filePath);
311
+ if (base === "artifacts.yml" || base === "brief.md") {
312
+ return { initiativeDir: dir, relPath: base };
313
+ }
314
+ if (base.endsWith(".yml") && path3.basename(dir) === "tasks") {
315
+ return { initiativeDir: path3.dirname(dir), relPath: path3.posix.join("tasks", base) };
316
+ }
317
+ return null;
318
+ }
319
+ function hashContent(content) {
320
+ return createHash("sha256").update(content, "utf8").digest("hex");
321
+ }
322
+ async function readManifest(initiativeDir) {
323
+ const manifestPath = path3.join(initiativeDir, MANIFEST_FILENAME);
324
+ let raw;
325
+ try {
326
+ raw = await fs2.readFile(manifestPath, "utf8");
327
+ } catch {
328
+ return {};
329
+ }
330
+ let parsed;
331
+ try {
332
+ parsed = YAML.parse(raw);
333
+ } catch {
334
+ return {};
335
+ }
336
+ if (!parsed || typeof parsed !== "object") return {};
337
+ return parsed;
338
+ }
339
+ async function readArtifactHashes(initiativeDir) {
340
+ return readManifest(initiativeDir);
341
+ }
342
+ async function recordArtifactHash(initiativeDir, relPath, content) {
343
+ const manifest = await readManifest(initiativeDir);
344
+ manifest[relPath] = hashContent(content);
345
+ const manifestPath = path3.join(initiativeDir, MANIFEST_FILENAME);
346
+ await atomicWrite(manifestPath, YAML.stringify(manifest));
347
+ }
348
+
349
+ // src/utils/coerce-dates.ts
350
+ function dateToString(d) {
351
+ if (d.getUTCHours() === 0 && d.getUTCMinutes() === 0 && d.getUTCSeconds() === 0 && d.getUTCMilliseconds() === 0) {
352
+ return d.toISOString().slice(0, 10);
353
+ }
354
+ return d.toISOString();
355
+ }
356
+ function coerceDates(value) {
357
+ if (value instanceof Date) {
358
+ return dateToString(value);
359
+ }
360
+ if (Array.isArray(value)) {
361
+ return value.map(coerceDates);
362
+ }
363
+ if (value !== null && typeof value === "object") {
364
+ const out = {};
365
+ for (const [k, v] of Object.entries(value)) {
366
+ out[k] = coerceDates(v);
367
+ }
368
+ return out;
369
+ }
370
+ return value;
371
+ }
372
+
373
+ // src/utils/yaml-io.ts
374
+ async function readYaml(filePath, schema) {
375
+ const raw = await fs3.readFile(filePath, "utf8");
376
+ let parsed;
377
+ try {
378
+ parsed = YAML2.parse(raw);
379
+ } catch (err) {
380
+ const reason = err instanceof Error ? err.message : String(err);
381
+ throw new Error(`Failed to parse YAML at ${filePath}: ${reason}`);
382
+ }
383
+ const coerced = coerceDates(parsed);
384
+ const result = schema.safeParse(coerced);
385
+ if (!result.success) {
386
+ throw new Error(`Schema validation failed for ${filePath}: ${result.error.message}`);
387
+ }
388
+ return result.data;
389
+ }
390
+ async function writeYaml(filePath, data, schema) {
391
+ const result = schema.safeParse(data);
392
+ if (!result.success) {
393
+ throw new Error(`Schema validation failed for ${filePath}: ${result.error.message}`);
394
+ }
395
+ const yaml = YAML2.stringify(result.data);
396
+ await atomicWrite(filePath, yaml);
397
+ const artifact = classifyStructuredArtifact(filePath);
398
+ if (artifact) await recordArtifactHash(artifact.initiativeDir, artifact.relPath, yaml);
399
+ }
400
+
401
+ // src/utils/registered-worktrees.ts
402
+ function artifactsPathFor(initiativeDir) {
403
+ return path4.join(initiativeDir, "artifacts.yml");
404
+ }
405
+ var EMPTY = { branches: [], stashes: [], worktrees: [] };
406
+ async function readArtifactsFile(initiativeDir) {
407
+ const file = artifactsPathFor(initiativeDir);
408
+ try {
409
+ await fs4.access(file);
410
+ } catch {
411
+ return { ...EMPTY, branches: [], stashes: [], worktrees: [] };
412
+ }
413
+ return readYaml(file, ArtifactsSchema);
414
+ }
415
+ function registeredOf(artifacts) {
416
+ return artifacts.worktrees.filter(
417
+ (entry) => entry.name !== void 0
418
+ );
419
+ }
420
+ async function readRegisteredWorktrees(initiativeDir) {
421
+ try {
422
+ return registeredOf(await readArtifactsFile(initiativeDir));
423
+ } catch {
424
+ return [];
425
+ }
426
+ }
427
+ function defaultWorktreePath(registered) {
428
+ const explicit = registered.find((entry) => entry.default === true);
429
+ if (explicit) return explicit.path;
430
+ return registered.length === 1 ? registered[0].path : null;
431
+ }
432
+ async function writeArtifactsFile(initiativeDir, artifacts) {
433
+ await writeYaml(artifactsPathFor(initiativeDir), artifacts, ArtifactsSchema);
434
+ }
435
+
436
+ // src/bootstrap/prompt.ts
437
+ import { promises as fs11 } from "fs";
438
+ import path11 from "path";
439
+
440
+ // src/schemas/task.ts
441
+ import { z as z3 } from "zod";
442
+ var ISO_DATE_REGEX2 = /^\d{4}-\d{2}-\d{2}$/;
443
+ var isValidIsoDate2 = (value) => {
444
+ if (!ISO_DATE_REGEX2.test(value)) return false;
445
+ const parsed = new Date(value);
446
+ if (Number.isNaN(parsed.getTime())) return false;
447
+ return parsed.toISOString().slice(0, 10) === value;
448
+ };
449
+ var isoDate2 = z3.string().refine(isValidIsoDate2, { message: "Must be a valid zero-padded YYYY-MM-DD date" });
450
+ var isoDateOrNull = z3.union([isoDate2, z3.null()]);
451
+ var TaskSchema = z3.object({
452
+ id: z3.string().regex(/^[A-Z][A-Z0-9]*-\d+$/, {
453
+ message: "id must match /^[A-Z][A-Z0-9]*-\\d+$/ (e.g. EC-1)"
454
+ }),
455
+ title: z3.string().min(1),
456
+ priority: z3.number().int().positive(),
457
+ severity: z3.enum(["critical", "high", "medium", "low"]).optional(),
458
+ estimate: z3.number().positive().optional(),
459
+ done_when: z3.string().min(1).optional(),
460
+ status: z3.enum(["open", "done"]),
461
+ tags: z3.array(z3.string()).optional(),
462
+ notes: z3.string().optional(),
463
+ created: isoDate2,
464
+ updated: isoDate2,
465
+ done_at: isoDateOrNull
466
+ });
467
+
468
+ // src/sessions/open-loops.ts
469
+ import { promises as fs5 } from "fs";
470
+ import path5 from "path";
471
+ import YAML3 from "yaml";
472
+
473
+ // src/schemas/session.ts
474
+ import { z as z4 } from "zod";
475
+ var ISO_8601_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
476
+ var isValidIso8601 = (value) => {
477
+ if (!ISO_8601_REGEX.test(value)) return false;
478
+ const parsed = new Date(value);
479
+ return !Number.isNaN(parsed.getTime());
480
+ };
481
+ var iso8601 = z4.string().refine(isValidIso8601, { message: "Must be a valid ISO 8601 datetime with timezone" });
482
+ var REF_SEGMENT_REGEX = /^[^#\s/]+$/;
483
+ var REF_SEGMENT_MESSAGE = 'must not contain whitespace, "#" or "/"';
484
+ var SessionIdSchema = z4.string().min(1).regex(REF_SEGMENT_REGEX, { message: `session_id ${REF_SEGMENT_MESSAGE}` });
485
+ var NextStepSchema = z4.object({
486
+ id: z4.string().min(1).regex(REF_SEGMENT_REGEX, { message: `next_steps id ${REF_SEGMENT_MESSAGE}` }),
487
+ text: z4.string().min(1),
488
+ kind: z4.enum(["task", "pr", "prose"]),
489
+ ref: z4.string().min(1).optional()
490
+ });
491
+ var SessionResolveSchema = z4.object({
492
+ ref: z4.string().regex(/^[^#\s]+#[^#\s]+$/, {
493
+ message: "ref must be '<session file stem>#<next_step id>'"
494
+ }),
495
+ outcome: z4.enum(["done", "abandoned"]),
496
+ note: z4.string().min(1).optional()
497
+ });
498
+ function checkUniqueStepIds(steps, ctx) {
499
+ const seen = /* @__PURE__ */ new Set();
500
+ steps.forEach((step, index) => {
501
+ if (seen.has(step.id)) {
502
+ ctx.addIssue({
503
+ code: "custom",
504
+ path: ["next_steps", index, "id"],
505
+ message: `next_steps ids must be unique within a session: ${step.id}`
506
+ });
507
+ }
508
+ seen.add(step.id);
509
+ });
510
+ }
511
+ function checkAbandonedHasNote(entries, ctx) {
512
+ entries.forEach((entry, index) => {
513
+ if (entry.outcome === "abandoned" && entry.note === void 0) {
514
+ ctx.addIssue({
515
+ code: "custom",
516
+ path: ["resolves", index, "note"],
517
+ message: 'note is required when outcome is "abandoned"'
518
+ });
519
+ }
520
+ });
521
+ }
522
+ var SessionFrontmatterSchema = z4.object({
523
+ session_id: SessionIdSchema,
524
+ started: iso8601,
525
+ ended: iso8601,
526
+ track: z4.enum(["canonical", "sidecar", "adhoc"]),
527
+ next_steps: z4.array(NextStepSchema).default([]),
528
+ resolves: z4.array(SessionResolveSchema).default([]),
529
+ // Written only by `wrap --no-loops`. An empty ledger alone cannot say
530
+ // whether nothing was hanging or nothing was filed; this marker does.
531
+ no_loops: z4.literal(true).optional()
532
+ }).superRefine((value, ctx) => {
533
+ const started = new Date(value.started).getTime();
534
+ const ended = new Date(value.ended).getTime();
535
+ if (Number.isFinite(started) && Number.isFinite(ended) && ended < started) {
536
+ ctx.addIssue({
537
+ code: "custom",
538
+ path: ["ended"],
539
+ message: "ended must be greater than or equal to started"
540
+ });
541
+ }
542
+ checkUniqueStepIds(value.next_steps, ctx);
543
+ checkAbandonedHasNote(value.resolves, ctx);
544
+ if (value.no_loops === true && (value.next_steps.length > 0 || value.resolves.length > 0)) {
545
+ ctx.addIssue({
546
+ code: "custom",
547
+ path: ["no_loops"],
548
+ message: "no_loops cannot be set alongside next_steps or resolves"
549
+ });
550
+ }
551
+ });
552
+
553
+ // src/sessions/open-loops.ts
554
+ var MS_PER_DAY = 24 * 60 * 60 * 1e3;
555
+ var FRONTMATTER_DELIM = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
556
+ function describe(err) {
557
+ return err instanceof Error ? err.message : String(err);
558
+ }
559
+ async function loadSession(fullPath, sessionFile) {
560
+ const fail = (reason) => ({
561
+ ok: false,
562
+ problem: { file: `${sessionFile}.md`, reason }
563
+ });
564
+ let raw;
565
+ try {
566
+ raw = await fs5.readFile(fullPath, "utf8");
567
+ } catch (err) {
568
+ return fail(`unreadable: ${describe(err)}`);
569
+ }
570
+ const match = FRONTMATTER_DELIM.exec(raw);
571
+ if (!match) return fail("no frontmatter block");
572
+ let parsed;
573
+ try {
574
+ parsed = YAML3.parse(match[1] ?? "");
575
+ } catch (err) {
576
+ return fail(`invalid YAML: ${describe(err)}`);
577
+ }
578
+ const result = SessionFrontmatterSchema.safeParse(parsed);
579
+ if (!result.success) return fail(`invalid frontmatter: ${summarizeIssues(result.error)}`);
580
+ return { ok: true, session: toLoadedSession(sessionFile, result.data, match[2] ?? "") };
581
+ }
582
+ function summarizeIssues(error) {
583
+ return error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join(", ");
584
+ }
585
+ function toLoadedSession(sessionFile, frontmatter, body) {
586
+ return {
587
+ sessionFile,
588
+ sessionId: frontmatter.session_id,
589
+ ended: frontmatter.ended,
590
+ endedMs: new Date(frontmatter.ended).getTime(),
591
+ frontmatter,
592
+ body
593
+ };
594
+ }
595
+ async function loadSessionsFromDir(initiativeDir) {
596
+ const sessionsDir = path5.join(initiativeDir, "sessions");
597
+ let entries;
598
+ try {
599
+ entries = await fs5.readdir(sessionsDir);
600
+ } catch {
601
+ return { sessions: [], malformed: [] };
602
+ }
603
+ const sessions = [];
604
+ const malformed = [];
605
+ for (const filename of entries.filter((n) => n.endsWith(".md")).sort()) {
606
+ const result = await loadSession(
607
+ path5.join(sessionsDir, filename),
608
+ filename.slice(0, -".md".length)
609
+ );
610
+ if (result.ok) sessions.push(result.session);
611
+ else malformed.push(result.problem);
612
+ }
613
+ return { sessions, malformed };
614
+ }
615
+ function indexLoops(sessions) {
616
+ const loops = /* @__PURE__ */ new Map();
617
+ for (const session of sessions) {
618
+ for (const step of session.frontmatter.next_steps) {
619
+ loops.set(`${session.sessionFile}#${step.id}`, {
620
+ ref: `${session.sessionFile}#${step.id}`,
621
+ step,
622
+ session
623
+ });
624
+ }
625
+ }
626
+ return loops;
627
+ }
628
+ function refStem(ref) {
629
+ const hash = ref.indexOf("#");
630
+ return hash < 0 ? ref : ref.slice(0, hash);
631
+ }
632
+ function classifyResolve(ref, session, loops) {
633
+ if (refStem(ref) === session.sessionFile) return "self";
634
+ const target = loops.get(ref);
635
+ if (!target) return "missing";
636
+ if (target.session.sessionId === session.sessionId) return "self";
637
+ if (target.session.endedMs >= session.endedMs) return "not-prior";
638
+ return null;
639
+ }
640
+ function applyResolves(sessions, loops) {
641
+ const resolutions = /* @__PURE__ */ new Map();
642
+ const dangling = [];
643
+ for (const session of sessions) {
644
+ for (const entry of session.frontmatter.resolves) {
645
+ const kind = classifyResolve(entry.ref, session, loops);
646
+ if (kind !== null) {
647
+ dangling.push({ sessionFile: session.sessionFile, ref: entry.ref, kind });
648
+ continue;
649
+ }
650
+ const existing = resolutions.get(entry.ref);
651
+ if (existing && existing.closedAtMs > session.endedMs) continue;
652
+ resolutions.set(entry.ref, {
653
+ outcome: entry.outcome,
654
+ ...entry.note !== void 0 ? { note: entry.note } : {},
655
+ closedBy: session.sessionFile,
656
+ closedAt: session.frontmatter.ended,
657
+ closedAtMs: session.endedMs
658
+ });
659
+ }
660
+ }
661
+ return { resolutions, dangling };
662
+ }
663
+ function analyzeLoaded({ sessions, malformed }) {
664
+ const loops = indexLoops(sessions);
665
+ const { resolutions, dangling } = applyResolves(sessions, loops);
666
+ return { loops: [...loops.values()], resolutions, dangling, malformed };
667
+ }
668
+ async function analyze(initiativeDir) {
669
+ return analyzeLoaded(await loadSessionsFromDir(initiativeDir));
670
+ }
671
+ function normalizePrRef(ref) {
672
+ const trimmed = ref.trim();
673
+ const fromUrl = /\/pull\/(\d+)/.exec(trimmed);
674
+ return fromUrl?.[1] ?? trimmed.replace(/^#/, "");
675
+ }
676
+ function isAutoResolved(entry, opts) {
677
+ const target = entry.step.ref;
678
+ if (target === void 0) return false;
679
+ if (entry.step.kind === "task" && opts.tasks) {
680
+ return opts.tasks.some((t) => t.id === target && t.status === "done");
681
+ }
682
+ if (entry.step.kind === "pr" && opts.mergedPrs) {
683
+ const wanted = normalizePrRef(target);
684
+ return opts.mergedPrs.some((ref) => normalizePrRef(ref) === wanted);
685
+ }
686
+ return false;
687
+ }
688
+ function toOpenLoop(entry, now) {
689
+ const ageMs = now.getTime() - entry.session.endedMs;
690
+ return {
691
+ ref: entry.ref,
692
+ text: entry.step.text,
693
+ ...entry.step.ref !== void 0 ? { targetRef: entry.step.ref } : {},
694
+ kind: entry.step.kind,
695
+ sessionFile: entry.session.sessionFile,
696
+ sessionId: entry.session.sessionId,
697
+ openedAt: entry.session.ended,
698
+ ageDays: Math.max(0, Math.floor(ageMs / MS_PER_DAY))
699
+ };
700
+ }
701
+ function deriveOpenLoopsFrom(loaded, opts) {
702
+ const { loops, resolutions } = analyzeLoaded(loaded);
703
+ return loops.filter((entry) => !resolutions.has(entry.ref) && !isAutoResolved(entry, opts)).map((entry) => toOpenLoop(entry, opts.now)).sort(
704
+ (a, b) => new Date(a.openedAt).getTime() - new Date(b.openedAt).getTime() || a.ref.localeCompare(b.ref)
705
+ );
706
+ }
707
+ async function deriveOpenLoops(initiativeDir, opts) {
708
+ return deriveOpenLoopsFrom(await loadSessionsFromDir(initiativeDir), opts);
709
+ }
710
+ function toResolvedLoop(entry, resolution, now) {
711
+ const ageMs = now.getTime() - new Date(resolution.closedAt).getTime();
712
+ return {
713
+ ref: entry.ref,
714
+ text: entry.step.text,
715
+ kind: entry.step.kind,
716
+ outcome: resolution.outcome,
717
+ ...resolution.note !== void 0 ? { note: resolution.note } : {},
718
+ sessionFile: entry.session.sessionFile,
719
+ closedBy: resolution.closedBy,
720
+ openedAt: entry.session.ended,
721
+ closedAt: resolution.closedAt,
722
+ ageDays: Math.max(0, Math.floor(ageMs / MS_PER_DAY))
723
+ };
724
+ }
725
+ function deriveResolvedLoopsFrom(loaded, opts) {
726
+ const { loops, resolutions } = analyzeLoaded(loaded);
727
+ return loops.flatMap((entry) => {
728
+ const resolution = resolutions.get(entry.ref);
729
+ return resolution ? [toResolvedLoop(entry, resolution, opts.now)] : [];
730
+ }).sort(
731
+ (a, b) => new Date(b.closedAt).getTime() - new Date(a.closedAt).getTime() || a.ref.localeCompare(b.ref)
732
+ );
733
+ }
734
+ async function findDanglingResolves(initiativeDir) {
735
+ const { dangling } = await analyze(initiativeDir);
736
+ return dangling;
737
+ }
738
+ async function findSessionIssues(initiativeDir) {
739
+ const { dangling, malformed } = await analyze(initiativeDir);
740
+ return { dangling, malformed };
741
+ }
742
+
743
+ // src/notes/note-file.ts
744
+ import { promises as fs8 } from "fs";
745
+ import path7 from "path";
746
+
747
+ // src/schemas/note.ts
748
+ import { z as z5 } from "zod";
749
+ var ISO_DATE_REGEX3 = /^\d{4}-\d{2}-\d{2}$/;
750
+ var isValidIsoDate3 = (value) => {
751
+ if (!ISO_DATE_REGEX3.test(value)) return false;
752
+ const parsed = new Date(value);
753
+ if (Number.isNaN(parsed.getTime())) return false;
754
+ return parsed.toISOString().slice(0, 10) === value;
755
+ };
756
+ var isoDate3 = z5.string().refine(isValidIsoDate3, { message: "Must be a valid zero-padded YYYY-MM-DD date" });
757
+ var NoteKindSchema = z5.enum(["process", "gotcha", "fyi", "decision"]);
758
+ var NOTE_TITLE_MAX_LENGTH = 120;
759
+ var NoteFrontmatterSchema = z5.object({
760
+ kind: NoteKindSchema,
761
+ title: z5.string().min(1),
762
+ created: isoDate3,
763
+ tags: z5.array(z5.string().min(1)).optional()
764
+ });
765
+
766
+ // src/utils/gray-matter-io.ts
767
+ import { promises as fs6 } from "fs";
768
+ import matter from "gray-matter";
769
+ async function readFrontmatter(filePath, schema) {
770
+ const raw = await fs6.readFile(filePath, "utf8");
771
+ const parsed = matter(raw);
772
+ const coerced = coerceDates(parsed.data);
773
+ const result = schema.safeParse(coerced);
774
+ if (!result.success) {
775
+ throw new Error(`Frontmatter validation failed for ${filePath}: ${result.error.message}`);
776
+ }
777
+ return { frontmatter: result.data, body: parsed.content };
778
+ }
779
+ async function readRawFrontmatter(filePath) {
780
+ const raw = await fs6.readFile(filePath, "utf8");
781
+ const parsed = matter(raw);
782
+ const coerced = coerceDates(parsed.data);
783
+ return {
784
+ frontmatter: { ...coerced },
785
+ body: parsed.content
786
+ };
787
+ }
788
+ async function writeFrontmatter(filePath, frontmatter, body, schema) {
789
+ const result = schema.safeParse(frontmatter);
790
+ if (!result.success) {
791
+ throw new Error(`Frontmatter validation failed for ${filePath}: ${result.error.message}`);
792
+ }
793
+ const stringified = matter.stringify(body, result.data);
794
+ await atomicWrite(filePath, stringified);
795
+ const artifact = classifyStructuredArtifact(filePath);
796
+ if (artifact) await recordArtifactHash(artifact.initiativeDir, artifact.relPath, stringified);
797
+ }
798
+
799
+ // src/commands/source-add.ts
800
+ import { promises as fs7 } from "fs";
801
+ import path6 from "path";
802
+ import { z as z6 } from "zod";
803
+
804
+ // src/utils/today.ts
805
+ function today() {
806
+ const now = /* @__PURE__ */ new Date();
807
+ const year = now.getFullYear();
808
+ const month = String(now.getMonth() + 1).padStart(2, "0");
809
+ const day = String(now.getDate()).padStart(2, "0");
810
+ return `${year}-${month}-${day}`;
811
+ }
812
+ function nowIso() {
813
+ return (/* @__PURE__ */ new Date()).toISOString();
814
+ }
815
+
816
+ // src/commands/source-add.ts
817
+ var ArgsSchema = z6.object({
818
+ slug: z6.string().min(1),
819
+ file: z6.string().min(1),
820
+ type: z6.enum(["pr", "deepdive", "session", "pointer"]),
821
+ label: z6.string().optional(),
822
+ topic: z6.string().optional(),
823
+ pr_number: z6.number().int().positive().optional(),
824
+ date: z6.string().optional(),
825
+ force: z6.boolean().optional()
826
+ });
827
+ var ResultSchema = z6.object({
828
+ moved_to: z6.string(),
829
+ noop: z6.boolean().optional()
830
+ });
831
+ function slugifyLabel(input) {
832
+ const cleaned = input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
833
+ return cleaned.length > 0 ? cleaned : "untitled";
834
+ }
835
+ function deriveFilename(args) {
836
+ switch (args.type) {
837
+ case "pr": {
838
+ if (args.pr_number === void 0) {
839
+ throw new ValidationError("source.add type=pr requires --pr-number");
840
+ }
841
+ if (!args.label) {
842
+ throw new ValidationError("source.add type=pr requires --label");
843
+ }
844
+ return `pr-${args.pr_number}-${slugifyLabel(args.label)}.md`;
845
+ }
846
+ case "deepdive": {
847
+ if (!args.topic) {
848
+ throw new ValidationError("source.add type=deepdive requires --topic");
849
+ }
850
+ return `deepdive-${slugifyLabel(args.topic)}.md`;
851
+ }
852
+ case "session": {
853
+ if (!args.label) {
854
+ throw new ValidationError("source.add type=session requires --label");
855
+ }
856
+ const date = args.date ?? today();
857
+ return `${date}-${slugifyLabel(args.label)}.md`;
858
+ }
859
+ case "pointer": {
860
+ if (!args.label) {
861
+ throw new ValidationError("source.add type=pointer requires --label");
862
+ }
863
+ return `${slugifyLabel(args.label)}.md`;
864
+ }
865
+ }
866
+ }
867
+ async function pathExists(p) {
868
+ try {
869
+ await fs7.access(p);
870
+ return true;
871
+ } catch {
872
+ return false;
873
+ }
874
+ }
875
+ async function movePath(src, dest) {
876
+ try {
877
+ await fs7.rename(src, dest);
878
+ } catch (err) {
879
+ const code = err.code;
880
+ if (code === "EXDEV") {
881
+ await fs7.copyFile(src, dest);
882
+ await fs7.unlink(src);
883
+ return;
884
+ }
885
+ throw err;
886
+ }
887
+ }
888
+ var source_add_default = defineCommand({
889
+ name: "source.add",
890
+ description: "Move a source file into <slug>/sources/ with a conventional filename.",
891
+ args: ArgsSchema,
892
+ result: ResultSchema,
893
+ cli: {
894
+ positional: ["slug", "file"],
895
+ options: {
896
+ type: {
897
+ long: "--type",
898
+ description: "Source type: pr | deepdive | session | pointer",
899
+ required: true
900
+ },
901
+ label: { long: "--label", description: "Short label (slugified into filename)" },
902
+ topic: { long: "--topic", description: "Topic for deepdive type" },
903
+ pr_number: { long: "--pr-number", description: "PR number for type=pr" },
904
+ date: { long: "--date", description: "Date YYYY-MM-DD for type=session" },
905
+ force: { long: "--force", description: "Overwrite if target exists" }
906
+ }
907
+ },
908
+ async run(args) {
909
+ const sourcePath = path6.resolve(args.file);
910
+ if (!await pathExists(sourcePath)) {
911
+ throw new NotFoundError(`source file not found: ${sourcePath}`);
912
+ }
913
+ const filename = deriveFilename(args);
914
+ const sourcesDir = path6.join(getInitiativeDir(args.slug), "sources");
915
+ const targetPath = path6.join(sourcesDir, filename);
916
+ if (path6.resolve(sourcePath) === path6.resolve(targetPath)) {
917
+ return { moved_to: targetPath, noop: true };
918
+ }
919
+ await fs7.mkdir(sourcesDir, { recursive: true });
920
+ if (await pathExists(targetPath) && !args.force) {
921
+ throw new ValidationError(`target already exists: ${targetPath} (use --force to overwrite)`);
922
+ }
923
+ await movePath(sourcePath, targetPath);
924
+ return { moved_to: targetPath };
925
+ }
926
+ });
927
+
928
+ // src/notes/note-file.ts
929
+ function getNotesDir(initiativeDir) {
930
+ return path7.join(initiativeDir, "sources", "notes");
931
+ }
932
+ function compareNewestFirst(a, b) {
933
+ return b.filename.localeCompare(a.filename);
934
+ }
935
+ async function loadNotesFromDir(initiativeDir) {
936
+ const dir = getNotesDir(initiativeDir);
937
+ let entries;
938
+ try {
939
+ entries = await fs8.readdir(dir);
940
+ } catch {
941
+ return { notes: [], malformed: [] };
942
+ }
943
+ const notes = [];
944
+ const malformed = [];
945
+ for (const filename of entries.filter((n) => n.endsWith(".md")).sort()) {
946
+ const fullPath = path7.join(dir, filename);
947
+ try {
948
+ const { frontmatter, body } = await readFrontmatter(fullPath, NoteFrontmatterSchema);
949
+ notes.push({ filename, path: fullPath, frontmatter, body });
950
+ } catch (err) {
951
+ malformed.push({
952
+ file: filename,
953
+ reason: err instanceof Error ? err.message : String(err)
954
+ });
955
+ }
956
+ }
957
+ return { notes: notes.sort(compareNewestFirst), malformed };
958
+ }
959
+ async function exists(p) {
960
+ try {
961
+ await fs8.access(p);
962
+ return true;
963
+ } catch {
964
+ return false;
965
+ }
966
+ }
967
+ async function pickAvailableFilename(dir, baseName) {
968
+ if (!await exists(path7.join(dir, `${baseName}.md`))) return `${baseName}.md`;
969
+ for (let i = 1; i < 1e4; i++) {
970
+ const candidate = `${baseName}-${i}.md`;
971
+ if (!await exists(path7.join(dir, candidate))) return candidate;
972
+ }
973
+ throw new Error(`Could not find an available filename for ${baseName}`);
974
+ }
975
+ async function writeNoteFile(initiativeDir, frontmatter, body) {
976
+ const dir = getNotesDir(initiativeDir);
977
+ await fs8.mkdir(dir, { recursive: true });
978
+ const baseName = `${frontmatter.created}-${slugifyLabel(frontmatter.title)}`;
979
+ const filename = await pickAvailableFilename(dir, baseName);
980
+ const fullPath = path7.join(dir, filename);
981
+ await writeFrontmatter(fullPath, frontmatter, body, NoteFrontmatterSchema);
982
+ return { path: fullPath, filename };
983
+ }
984
+
985
+ // src/sessions/lease.ts
986
+ import { promises as fs10, unlinkSync } from "fs";
987
+ import { randomBytes as randomBytes2 } from "crypto";
988
+ import path9 from "path";
989
+
990
+ // src/schemas/lease.ts
991
+ import { z as z7 } from "zod";
992
+ var ISO_8601_REGEX2 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
993
+ var isValidIso86012 = (value) => {
994
+ if (!ISO_8601_REGEX2.test(value)) return false;
995
+ const parsed = new Date(value);
996
+ return !Number.isNaN(parsed.getTime());
997
+ };
998
+ var iso86012 = z7.string().refine(isValidIso86012, { message: "Must be a valid ISO 8601 datetime with timezone" });
999
+ var LeaseModeSchema = z7.enum(["launcher", "oneshot"]);
1000
+ var LeaseSchema = z7.object({
1001
+ /** Random hex; also the filename stem (`<lease_id>.json`). */
1002
+ lease_id: z7.string().min(1),
1003
+ slug: z7.string().min(1),
1004
+ /** The launch cwd, so a sibling can be told apart from a second checkout. */
1005
+ cwd: z7.string().min(1),
1006
+ mode: LeaseModeSchema,
1007
+ /** Present only for `launcher` leases — the `aw` process to probe. */
1008
+ pid: z7.number().int().positive().optional(),
1009
+ started: iso86012,
1010
+ /** Human/role hint carried for future use (e.g. an agent-chat name). */
1011
+ label: z7.string().min(1).optional()
1012
+ });
1013
+
1014
+ // 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");
1030
+ }
1031
+ 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 };
1049
+ }
1050
+ 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;
1062
+ }
1063
+ var DEFAULT_DAEMON_PORT = 7400;
1064
+ function resolveDaemonPort() {
1065
+ const envPort = process.env.AW_PORT;
1066
+ if (envPort) {
1067
+ const n = Number.parseInt(envPort, 10);
1068
+ if (Number.isFinite(n)) return n;
1069
+ }
1070
+ return DEFAULT_DAEMON_PORT;
1071
+ }
1072
+ var HEALTH_TIMEOUT_MS = 500;
1073
+ 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
+ }
1098
+ }
1099
+
1100
+ // src/sessions/lease.ts
1101
+ var ONESHOT_TTL_MS = 90 * 6e4;
1102
+ var LAUNCHER_MAX_AGE_MS = 36 * 60 * 6e4;
1103
+ var LEASE_DIR_NAME = ".sessions";
1104
+ function leaseDir(activeRoot, slug) {
1105
+ return path9.join(activeRoot, LEASE_DIR_NAME, slug);
1106
+ }
1107
+ function leasePath(activeRoot, slug, leaseId) {
1108
+ return path9.join(leaseDir(activeRoot, slug), `${leaseId}.json`);
1109
+ }
1110
+ async function acquireLease(input) {
1111
+ const { activeRoot, slug, cwd, mode, pid, label, now = /* @__PURE__ */ new Date() } = input;
1112
+ const leaseId = randomBytes2(8).toString("hex");
1113
+ const lease = LeaseSchema.parse({
1114
+ lease_id: leaseId,
1115
+ slug,
1116
+ cwd,
1117
+ mode,
1118
+ ...mode === "launcher" && pid !== void 0 ? { pid } : {},
1119
+ started: now.toISOString(),
1120
+ ...label ? { label } : {}
1121
+ });
1122
+ await fs10.mkdir(leaseDir(activeRoot, slug), { recursive: true });
1123
+ await fs10.writeFile(leasePath(activeRoot, slug, leaseId), JSON.stringify(lease, null, 2), "utf8");
1124
+ return {
1125
+ leaseId,
1126
+ release: () => releaseLease(activeRoot, slug, leaseId)
1127
+ };
1128
+ }
1129
+ function isIgnorableUnlinkError(err) {
1130
+ const code = err?.code;
1131
+ return code === "ENOENT";
1132
+ }
1133
+ async function releaseLease(activeRoot, slug, leaseId) {
1134
+ try {
1135
+ await fs10.unlink(leasePath(activeRoot, slug, leaseId));
1136
+ } catch (err) {
1137
+ if (!isIgnorableUnlinkError(err)) throw err;
1138
+ }
1139
+ }
1140
+ function releaseLeaseSync(activeRoot, slug, leaseId) {
1141
+ try {
1142
+ unlinkSync(leasePath(activeRoot, slug, leaseId));
1143
+ } catch {
1144
+ }
1145
+ }
1146
+ function isLive(lease, now, isAlive) {
1147
+ const ageMs = now.getTime() - new Date(lease.started).getTime();
1148
+ if (lease.mode === "oneshot") return ageMs < ONESHOT_TTL_MS;
1149
+ if (lease.pid === void 0) return false;
1150
+ if (ageMs >= LAUNCHER_MAX_AGE_MS) return false;
1151
+ return isAlive(lease.pid);
1152
+ }
1153
+ function toSibling(lease) {
1154
+ return {
1155
+ lease_id: lease.lease_id,
1156
+ cwd: lease.cwd,
1157
+ mode: lease.mode,
1158
+ started: lease.started,
1159
+ ...lease.pid !== void 0 ? { pid: lease.pid } : {},
1160
+ ...lease.label !== void 0 ? { label: lease.label } : {}
1161
+ };
1162
+ }
1163
+ async function readOneLease(file) {
1164
+ try {
1165
+ const raw = await fs10.readFile(file, "utf8");
1166
+ const parsed = LeaseSchema.safeParse(JSON.parse(raw));
1167
+ return parsed.success ? parsed.data : null;
1168
+ } catch {
1169
+ return null;
1170
+ }
1171
+ }
1172
+ async function unlinkQuietly(file) {
1173
+ try {
1174
+ await fs10.unlink(file);
1175
+ } catch {
1176
+ }
1177
+ }
1178
+ async function readLiveLeases(input) {
1179
+ const { activeRoot, slug, now = /* @__PURE__ */ new Date(), excludeLeaseId, isAlive = isProcessAlive } = input;
1180
+ try {
1181
+ const dir = leaseDir(activeRoot, slug);
1182
+ let entries;
1183
+ try {
1184
+ entries = await fs10.readdir(dir);
1185
+ } catch {
1186
+ return [];
1187
+ }
1188
+ const live = [];
1189
+ for (const name of entries) {
1190
+ if (!name.endsWith(".json")) continue;
1191
+ const file = path9.join(dir, name);
1192
+ const lease = await readOneLease(file);
1193
+ if (!lease || !isLive(lease, now, isAlive)) {
1194
+ await unlinkQuietly(file);
1195
+ continue;
1196
+ }
1197
+ if (lease.lease_id === excludeLeaseId) continue;
1198
+ live.push(toSibling(lease));
1199
+ }
1200
+ return live.sort((a, b) => a.started.localeCompare(b.started));
1201
+ } catch {
1202
+ return [];
1203
+ }
1204
+ }
1205
+ async function sweepAllLeases(activeRoot, options = {}) {
1206
+ const root = path9.join(activeRoot, LEASE_DIR_NAME);
1207
+ let slugs;
1208
+ try {
1209
+ const entries = await fs10.readdir(root, { withFileTypes: true });
1210
+ slugs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
1211
+ } catch (err) {
1212
+ const code = err.code;
1213
+ if (code === "ENOENT") return { live: 0, pruned: 0 };
1214
+ return { live: 0, pruned: 0, error: err.message };
1215
+ }
1216
+ let live = 0;
1217
+ let before = 0;
1218
+ for (const slug of slugs) {
1219
+ try {
1220
+ const names = await fs10.readdir(leaseDir(activeRoot, slug));
1221
+ before += names.filter((n) => n.endsWith(".json")).length;
1222
+ live += (await readLiveLeases({ activeRoot, slug, ...options })).length;
1223
+ } catch (err) {
1224
+ return { live, pruned: Math.max(before - live, 0), error: err.message };
1225
+ }
1226
+ }
1227
+ return { live, pruned: Math.max(before - live, 0) };
1228
+ }
1229
+
1230
+ // src/utils/git-gh.ts
1231
+ import { spawn } from "child_process";
1232
+ import path10 from "path";
1233
+ var DEFAULT_TIMEOUT_MS = 1e4;
1234
+ var defaultRunner = (bin, args, opts = {}) => new Promise((resolve, reject) => {
1235
+ const child = spawn(bin, args, {
1236
+ cwd: opts.cwd,
1237
+ stdio: ["ignore", "pipe", "pipe"]
1238
+ });
1239
+ const stdoutChunks = [];
1240
+ const stderrChunks = [];
1241
+ let settled = false;
1242
+ const timer = setTimeout(() => {
1243
+ if (settled) return;
1244
+ settled = true;
1245
+ child.kill("SIGKILL");
1246
+ reject(new Error(`${bin} timed out after ${opts.timeoutMs ?? DEFAULT_TIMEOUT_MS}ms`));
1247
+ }, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
1248
+ child.stdout?.on("data", (chunk) => stdoutChunks.push(chunk));
1249
+ child.stderr?.on("data", (chunk) => stderrChunks.push(chunk));
1250
+ child.on("error", (err) => {
1251
+ if (settled) return;
1252
+ settled = true;
1253
+ clearTimeout(timer);
1254
+ reject(err);
1255
+ });
1256
+ child.on("close", (code) => {
1257
+ if (settled) return;
1258
+ settled = true;
1259
+ clearTimeout(timer);
1260
+ resolve({
1261
+ code,
1262
+ stdout: Buffer.concat(stdoutChunks).toString("utf8"),
1263
+ stderr: Buffer.concat(stderrChunks).toString("utf8")
1264
+ });
1265
+ });
1266
+ });
1267
+ var gitRunner = defaultRunner;
1268
+ var ghRunner = defaultRunner;
1269
+ function getGitRunner() {
1270
+ return gitRunner;
1271
+ }
1272
+ function getGhRunner() {
1273
+ return ghRunner;
1274
+ }
1275
+ function looksLikeOrgRepo(repo) {
1276
+ if (!repo) return false;
1277
+ if (repo.startsWith("/") || repo.startsWith("~") || repo.startsWith(".")) return false;
1278
+ if (/\s/.test(repo)) return false;
1279
+ const slashCount = (repo.match(/\//g) ?? []).length;
1280
+ return slashCount === 1;
1281
+ }
1282
+ function resolveLocalRepoPath(repo) {
1283
+ if (looksLikeOrgRepo(repo)) return null;
1284
+ return path10.resolve(expandTilde(repo));
1285
+ }
1286
+ async function deriveOrgRepoFromPath(repoPath) {
1287
+ try {
1288
+ const res = await gitRunner("git", ["-C", repoPath, "remote", "get-url", "origin"]);
1289
+ if (res.code !== 0) return null;
1290
+ return parseOrgRepoFromRemoteUrl(res.stdout.trim());
1291
+ } catch {
1292
+ return null;
1293
+ }
1294
+ }
1295
+ function parseOrgRepoFromRemoteUrl(url) {
1296
+ const trimmed = url.trim().replace(/\.git$/, "");
1297
+ const sshMatch = /^[^@]+@[^:]+:([^/]+)\/(.+)$/.exec(trimmed);
1298
+ if (sshMatch) return `${sshMatch[1]}/${sshMatch[2]}`;
1299
+ try {
1300
+ const u = new URL(trimmed);
1301
+ const parts = u.pathname.replace(/^\//, "").split("/");
1302
+ if (parts.length >= 2 && parts[0] && parts[1]) {
1303
+ return `${parts[0]}/${parts[1]}`;
1304
+ }
1305
+ } catch {
1306
+ }
1307
+ return null;
1308
+ }
1309
+ async function resolveOrgRepo(repo) {
1310
+ if (looksLikeOrgRepo(repo)) return repo;
1311
+ const localPath = resolveLocalRepoPath(repo);
1312
+ if (!localPath) return null;
1313
+ return deriveOrgRepoFromPath(localPath);
1314
+ }
1315
+
1316
+ // src/bootstrap/prompt.ts
1317
+ import YAML4 from "yaml";
1318
+ var BRIEF_BODY_MAX_LINES = 40;
1319
+ var SESSION_BODY_MAX_LINES = 25;
1320
+ var DEFAULT_TOP_N_TASKS = 5;
1321
+ var DEFAULT_RECENTLY_DONE_DAYS = 14;
1322
+ var RECENT_THRESHOLD_DAYS = 14;
1323
+ var MS_PER_HOUR = 1e3 * 60 * 60;
1324
+ var MS_PER_DAY2 = MS_PER_HOUR * 24;
1325
+ var FRONTMATTER_DELIM2 = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
1326
+ async function readMarkdownWithSchema(filePath, schema) {
1327
+ const raw = await fs11.readFile(filePath, "utf8");
1328
+ const match = FRONTMATTER_DELIM2.exec(raw);
1329
+ let frontmatterText = "";
1330
+ let body = raw;
1331
+ if (match) {
1332
+ frontmatterText = match[1] ?? "";
1333
+ body = match[2] ?? "";
1334
+ }
1335
+ const parsed = frontmatterText ? YAML4.parse(frontmatterText) : {};
1336
+ const result = schema.safeParse(parsed);
1337
+ if (!result.success) {
1338
+ throw new Error(`Frontmatter validation failed for ${filePath}: ${result.error.message}`);
1339
+ }
1340
+ return { frontmatter: result.data, body };
1341
+ }
1342
+ function compareSessionsNewestFirst(a, b) {
1343
+ const endedDelta = new Date(b.frontmatter.ended).getTime() - new Date(a.frontmatter.ended).getTime();
1344
+ if (endedDelta !== 0) return endedDelta;
1345
+ return new Date(b.frontmatter.started).getTime() - new Date(a.frontmatter.started).getTime();
1346
+ }
1347
+ async function loadSessionsNewestFirst(initiativeDir) {
1348
+ const { sessions, malformed } = await loadSessionsFromDir(initiativeDir);
1349
+ return { sessions: [...sessions].sort(compareSessionsNewestFirst), malformed };
1350
+ }
1351
+ function describe2(err) {
1352
+ return err instanceof Error ? err.message : String(err);
1353
+ }
1354
+ async function loadTasks(initiativeDir) {
1355
+ const tasksDir = path11.join(initiativeDir, "tasks");
1356
+ let entries;
1357
+ try {
1358
+ entries = await fs11.readdir(tasksDir);
1359
+ } catch {
1360
+ return { tasks: [], malformed: [] };
1361
+ }
1362
+ const ymlFiles = entries.filter((n) => n.endsWith(".yml") || n.endsWith(".yaml"));
1363
+ const tasks = [];
1364
+ const malformed = [];
1365
+ for (const filename of ymlFiles) {
1366
+ const fullPath = path11.join(tasksDir, filename);
1367
+ try {
1368
+ tasks.push(await readYaml(fullPath, TaskSchema));
1369
+ } catch (err) {
1370
+ malformed.push({ file: filename, reason: describe2(err) });
1371
+ }
1372
+ }
1373
+ return { tasks, malformed };
1374
+ }
1375
+ function isMissingFile(err) {
1376
+ return err?.code === "ENOENT";
1377
+ }
1378
+ async function loadArtifacts(initiativeDir) {
1379
+ const artifactsPath = path11.join(initiativeDir, "artifacts.yml");
1380
+ const empty = { branches: [], stashes: [], worktrees: [] };
1381
+ try {
1382
+ return { artifacts: await readYaml(artifactsPath, ArtifactsSchema) };
1383
+ } catch (err) {
1384
+ if (isMissingFile(err)) return { artifacts: empty };
1385
+ return { artifacts: empty, error: describe2(err) };
1386
+ }
1387
+ }
1388
+ function truncateLines(body, max, source) {
1389
+ const lines = body.split("\n");
1390
+ const trimmed = [];
1391
+ let count = 0;
1392
+ let consumed = 0;
1393
+ for (const line of lines) {
1394
+ if (count >= max) break;
1395
+ trimmed.push(line);
1396
+ consumed++;
1397
+ if (line.trim().length > 0) count++;
1398
+ }
1399
+ const dropped = lines.slice(consumed).filter((l) => l.trim().length > 0).length;
1400
+ const kept = trimmed.join("\n").replace(/\s+$/, "");
1401
+ if (dropped === 0) return kept;
1402
+ return `${kept}
1403
+ \u2026(+${dropped} lines \u2014 see ${source})`;
1404
+ }
1405
+ function formatTimeSince(from, now) {
1406
+ const diffMs = now.getTime() - from.getTime();
1407
+ if (diffMs < MS_PER_HOUR) return "just now";
1408
+ if (diffMs < MS_PER_DAY2) {
1409
+ const hours = Math.floor(diffMs / MS_PER_HOUR);
1410
+ return `${hours} hour${hours === 1 ? "" : "s"} ago`;
1411
+ }
1412
+ const days = Math.floor(diffMs / MS_PER_DAY2);
1413
+ const base = `${days} day${days === 1 ? "" : "s"} ago`;
1414
+ if (days >= RECENT_THRESHOLD_DAYS) {
1415
+ return `${base} \u2014 likely needs context refresher`;
1416
+ }
1417
+ return base;
1418
+ }
1419
+ function compareTasksByPriority(a, b) {
1420
+ if (a.priority !== b.priority) return a.priority - b.priority;
1421
+ return a.id.localeCompare(b.id);
1422
+ }
1423
+ function nonBlankLines(text) {
1424
+ if (!text) return [];
1425
+ return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
1426
+ }
1427
+ function firstLine(text) {
1428
+ return nonBlankLines(text)[0];
1429
+ }
1430
+ var TASK_SUMMARY_MAX_LINES = 2;
1431
+ var TASK_SUMMARY_MAX_CHARS = 200;
1432
+ function summarizeField(label, lines, pointer) {
1433
+ const kept = lines.slice(0, TASK_SUMMARY_MAX_LINES);
1434
+ const joined = kept.join(" ");
1435
+ const clamped = joined.length > TASK_SUMMARY_MAX_CHARS ? joined.slice(0, TASK_SUMMARY_MAX_CHARS).replace(/\s+\S*$/, "").trimEnd() : joined;
1436
+ const remaining = lines.length - kept.length + (clamped === joined ? 0 : 1);
1437
+ if (remaining === 0) return `${label}${clamped}`;
1438
+ const noun = remaining === 1 ? "line" : "lines";
1439
+ return `${label}${clamped}\u2026(+${remaining} ${noun} \u2014 see ${pointer})`;
1440
+ }
1441
+ function renderTaskSummary(task, slug) {
1442
+ const pointer = `\`active-work task list ${slug} --json\``;
1443
+ const doneWhen = nonBlankLines(task.done_when);
1444
+ if (doneWhen.length > 0) {
1445
+ return summarizeField("done when: ", doneWhen, pointer);
1446
+ }
1447
+ const notes = nonBlankLines(task.notes);
1448
+ if (notes.length > 0) return summarizeField("notes: ", notes, pointer);
1449
+ return void 0;
1450
+ }
1451
+ function renderTaskLine(idx, task, slug) {
1452
+ const meta = [`priority ${task.priority}`];
1453
+ if (task.severity) meta.push(`severity ${task.severity}`);
1454
+ if (task.estimate !== void 0) meta.push(`est ${task.estimate}`);
1455
+ let line = `${idx}. [${task.id}] (${meta.join(", ")}) ${task.title}`;
1456
+ const summary = renderTaskSummary(task, slug);
1457
+ if (summary) line += `
1458
+ ${summary}`;
1459
+ return line;
1460
+ }
1461
+ function renderTopTasks(tasks, topN, slug) {
1462
+ const openTasks = tasks.filter((t) => t.status === "open").sort(compareTasksByPriority);
1463
+ if (openTasks.length === 0) {
1464
+ return { body: "_No open tasks._", count: 0 };
1465
+ }
1466
+ const shown = openTasks.slice(0, topN);
1467
+ return {
1468
+ body: shown.map((task, i) => renderTaskLine(i + 1, task, slug)).join("\n"),
1469
+ count: openTasks.length
1470
+ };
1471
+ }
1472
+ function renderRecentlyDone(tasks, windowDays, now, slug) {
1473
+ const cutoff = now.getTime() - windowDays * MS_PER_DAY2;
1474
+ const done = tasks.filter((t) => t.status === "done" && t.done_at).filter((t) => {
1475
+ const ts = new Date(t.done_at).getTime();
1476
+ return Number.isFinite(ts) && ts >= cutoff;
1477
+ }).sort((a, b) => a.done_at < b.done_at ? 1 : -1);
1478
+ if (done.length === 0) return { body: null, count: 0 };
1479
+ const noun = done.length === 1 ? "task" : "tasks";
1480
+ const body = `${done.length} ${noun} completed \u2014 \`active-work task list ${slug} --status done --json\``;
1481
+ return { body, count: done.length };
1482
+ }
1483
+ var DURABLE_NOTES_LIMIT = 12;
1484
+ function renderNoteLine(note) {
1485
+ const { kind, title, created } = note.frontmatter;
1486
+ return `- [${kind}] ${title} (${created})`;
1487
+ }
1488
+ function renderDurableNotes(loaded, slug) {
1489
+ const { notes, malformed } = loaded;
1490
+ if (notes.length === 0 && malformed.length === 0) return null;
1491
+ const shown = notes.slice(0, DURABLE_NOTES_LIMIT);
1492
+ const lines = shown.map(renderNoteLine);
1493
+ const overflow = notes.length - shown.length;
1494
+ if (overflow > 0) {
1495
+ lines.push(`(+${overflow} older \u2014 \`active-work note list ${slug}\`)`);
1496
+ }
1497
+ if (malformed.length > 0) {
1498
+ lines.push(
1499
+ `(${malformed.length} note file(s) unreadable \u2014 run \`active-work note list ${slug}\`)`
1500
+ );
1501
+ }
1502
+ const heading = overflow > 0 ? `# Durable notes (newest ${shown.length} of ${notes.length})` : `# Durable notes (${notes.length})`;
1503
+ return `${heading}
1504
+ ${lines.join("\n")}`;
1505
+ }
1506
+ function renderNoOpenLoops(newestSession) {
1507
+ if (newestSession?.frontmatter.no_loops === true) {
1508
+ return `Nothing hanging \u2014 the ${endedDate(newestSession.ended)} session asserted the ledger is clear.`;
1509
+ }
1510
+ return "Nothing hanging \u2014 no unresolved loops, but no session has asserted the ledger is clear.";
1511
+ }
1512
+ function malformedNote(malformed) {
1513
+ if (malformed.length === 0) return "";
1514
+ return ` (${malformed.length} session file(s) unreadable \u2014 run \`active-work doctor\`)`;
1515
+ }
1516
+ function malformedTaskNote(malformed) {
1517
+ if (malformed.length === 0) return "";
1518
+ const files = malformed.map((m) => m.file).join(", ");
1519
+ return ` \u2014 ${malformed.length} task file(s) unreadable (${files}); run \`active-work doctor\``;
1520
+ }
1521
+ function textOpensWithRef(text, ref, kind) {
1522
+ const head = text.replace(/^\s*(PR\s*)?#?/i, "");
1523
+ if (kind === "pr" && normalizePrRef(head.split(/\s/)[0] ?? "") === ref) return true;
1524
+ if (!head.toLowerCase().startsWith(ref.toLowerCase())) return false;
1525
+ const next = head.charAt(ref.length);
1526
+ return next === "" || !/[A-Za-z0-9]/.test(next);
1527
+ }
1528
+ function loopLabel(loop) {
1529
+ if (loop.targetRef === void 0) return loop.text;
1530
+ const ref = loop.kind === "pr" ? normalizePrRef(loop.targetRef) : loop.targetRef;
1531
+ if (textOpensWithRef(loop.text, ref, loop.kind)) return loop.text;
1532
+ return loop.kind === "pr" ? `PR #${ref} ${loop.text}` : `${ref} ${loop.text}`;
1533
+ }
1534
+ function renderOpenLoops(loops, malformed, newestSession) {
1535
+ const note = malformedNote(malformed);
1536
+ if (loops.length === 0) {
1537
+ return `# Open loops${note}
1538
+ ${renderNoOpenLoops(newestSession)}`;
1539
+ }
1540
+ const labels = loops.map(loopLabel);
1541
+ const ageWidth = Math.max(...loops.map((l) => String(l.ageDays).length));
1542
+ const labelWidth = Math.max(...labels.map((l) => l.length));
1543
+ const lines = loops.map((loop, i) => {
1544
+ const age = `[${String(loop.ageDays).padStart(ageWidth)}d]`;
1545
+ const label = labels[i].padEnd(labelWidth);
1546
+ const from = loop.openedAt.slice(0, 10);
1547
+ return `- ${age} ${label} (from ${from}, ref ${loop.ref})`;
1548
+ });
1549
+ const oldest = loops[0].ageDays;
1550
+ return `# Open loops (${loops.length} hanging, oldest ${oldest}d)${note}
1551
+ ${lines.join("\n")}`;
1552
+ }
1553
+ var WRAP_DIRECTIVE = "Update tasks via `active-work task done` and close the session out via `active-work wrap` when wrapping up \u2014 it records the session, files the loops you leave open, and stamps the brief in one step.";
1554
+ function renderClosingInstruction(openLoops) {
1555
+ if (openLoops.length === 0) {
1556
+ return `Work the top task unless redirected. ${WRAP_DIRECTIVE}`;
1557
+ }
1558
+ const count = openLoops.length;
1559
+ const noun = count === 1 ? "loop" : "loops";
1560
+ const verb = count === 1 ? "is" : "are";
1561
+ return `Start with the open ${noun}: ${count} ${verb} still hanging from prior sessions, and unfinished threads take precedence over the backlog. Work them first, citing each one by the \`ref\` printed under "Open loops", and pass every loop you settle to \`active-work wrap --resolves\` with outcome \`done\` or \`abandoned\` \u2014 deciding not to do a loop still closes it. Once the loops are handled, or if the user redirects, work the top task by priority. ${WRAP_DIRECTIVE}`;
1562
+ }
1563
+ function renderAbandonedLoops(resolved, windowDays) {
1564
+ const abandoned = resolved.filter(
1565
+ (loop) => loop.outcome === "abandoned" && loop.ageDays <= windowDays
1566
+ );
1567
+ if (abandoned.length === 0) return null;
1568
+ const lines = abandoned.map((loop) => {
1569
+ const head = `- ${loop.text} (dropped ${loop.closedAt.slice(0, 10)})`;
1570
+ return loop.note ? `${head}
1571
+ why: ${loop.note}` : head;
1572
+ });
1573
+ return `# Abandoned in the last ${windowDays} days (${abandoned.length})
1574
+ ${lines.join("\n")}`;
1575
+ }
1576
+ var LIVE_RENDER_LIMIT = 10;
1577
+ function renderStaticBranchLine(branch) {
1578
+ const head = `- ${branch.name} (${branch.repo})`;
1579
+ return branch.note ? `${head} \u2014 ${branch.note}` : head;
1580
+ }
1581
+ function renderLiveBranchLine(status) {
1582
+ const parts = [`- ${status.name} (${status.repo})`];
1583
+ if (!status.present) {
1584
+ parts.push("[missing locally]");
1585
+ } else if (status.ahead !== null && status.behind !== null) {
1586
+ parts.push(`+${status.ahead}/-${status.behind}`);
1587
+ }
1588
+ if (status.last_commit_iso) {
1589
+ parts.push(`last commit ${status.last_commit_iso.slice(0, 10)}`);
1590
+ }
1591
+ if (status.pr) {
1592
+ const checks = status.pr.checks ? ` ${status.pr.checks}` : "";
1593
+ parts.push(`PR #${status.pr.number} ${status.pr.state}${checks}`);
1594
+ }
1595
+ let line = parts.join(" ");
1596
+ if (status.note) line += ` \u2014 ${status.note}`;
1597
+ if (status.pr) {
1598
+ line += `
1599
+ PR: ${status.pr.title} \u2014 ${status.pr.url}`;
1600
+ }
1601
+ return line;
1602
+ }
1603
+ var WORKTREE_RENDER_LIMIT = 8;
1604
+ function renderWorktreeLine(entry) {
1605
+ const parts = [`- ${entry.name}${entry.default ? " (default)" : ""}: ${entry.path}`];
1606
+ if (entry.branch) parts.push(`[${entry.branch}]`);
1607
+ if (entry.pr) parts.push(`PR #${entry.pr}`);
1608
+ const trailer = entry.holding ?? entry.note;
1609
+ return trailer ? `${parts.join(" ")} \u2014 ${trailer}` : parts.join(" ");
1610
+ }
1611
+ function renderWorktrees(artifacts, slug) {
1612
+ const registered = artifacts.worktrees.filter((w) => w.name !== void 0);
1613
+ const observed = artifacts.worktrees.length - registered.length;
1614
+ if (registered.length === 0 && observed === 0) return null;
1615
+ const lines = [];
1616
+ const shown = registered.slice(0, WORKTREE_RENDER_LIMIT);
1617
+ if (shown.length > 0) {
1618
+ lines.push(...shown.map(renderWorktreeLine));
1619
+ } else {
1620
+ lines.push("_None registered._");
1621
+ }
1622
+ const overflow = registered.length - shown.length;
1623
+ if (overflow > 0) {
1624
+ lines.push(`(+${overflow} more registered \u2014 \`active-work artifact list ${slug}\`)`);
1625
+ }
1626
+ if (observed > 0) {
1627
+ const noun = observed === 1 ? "worktree" : "worktrees";
1628
+ lines.push(
1629
+ `(+${observed} observed ${noun} swept from git, not registered \u2014 \`active-work artifact list ${slug}\`)`
1630
+ );
1631
+ }
1632
+ return `Worktrees (registered):
1633
+ ${lines.join("\n")}`;
1634
+ }
1635
+ function renderStashes(artifacts) {
1636
+ if (artifacts.stashes.length === 0) return null;
1637
+ return artifacts.stashes.map((s) => `- ${s.repo}: ${s.label}${s.sha ? ` (${s.sha.slice(0, 12)})` : ""}`).join("\n");
1638
+ }
1639
+ function renderStaticArtifacts(artifacts, slug) {
1640
+ const sections = [];
1641
+ if (artifacts.branches.length > 0) {
1642
+ const branchLines = artifacts.branches.map(renderStaticBranchLine).join("\n");
1643
+ sections.push(`Branches:
1644
+ ${branchLines}`);
1645
+ }
1646
+ const worktreeBody = renderWorktrees(artifacts, slug);
1647
+ if (worktreeBody) sections.push(worktreeBody);
1648
+ const stashBody = renderStashes(artifacts);
1649
+ if (stashBody) sections.push(`Stashes:
1650
+ ${stashBody}`);
1651
+ return sections.length > 0 ? sections.join("\n\n") : null;
1652
+ }
1653
+ function renderLiveArtifacts(artifacts, statuses, slug) {
1654
+ const sections = [];
1655
+ if (statuses.length > 0) {
1656
+ const shown = statuses.slice(0, LIVE_RENDER_LIMIT);
1657
+ const lines = shown.map(renderLiveBranchLine).join("\n");
1658
+ const overflow = statuses.length - shown.length;
1659
+ const suffix = overflow > 0 ? `
1660
+ (+${overflow} more)` : "";
1661
+ sections.push(`Branches (live):
1662
+ ${lines}${suffix}`);
1663
+ } else if (artifacts.branches.length > 0) {
1664
+ const branchLines = artifacts.branches.map(renderStaticBranchLine).join("\n");
1665
+ sections.push(`Branches:
1666
+ ${branchLines}`);
1667
+ }
1668
+ const worktreeBody = renderWorktrees(artifacts, slug);
1669
+ if (worktreeBody) sections.push(worktreeBody);
1670
+ const stashBody = renderStashes(artifacts);
1671
+ if (stashBody) sections.push(`Stashes:
1672
+ ${stashBody}`);
1673
+ return sections.length > 0 ? sections.join("\n\n") : null;
1674
+ }
1675
+ async function defaultLiveStatusFetcher(branches) {
1676
+ const results = [];
1677
+ const limit = Math.min(branches.length, LIVE_RENDER_LIMIT);
1678
+ for (let i = 0; i < limit; i++) {
1679
+ results.push(await fetchOne(branches[i]));
1680
+ }
1681
+ return results;
1682
+ }
1683
+ async function fetchOne(branch) {
1684
+ const out = {
1685
+ repo: branch.repo,
1686
+ name: branch.name,
1687
+ ...branch.note ? { note: branch.note } : {},
1688
+ present: false,
1689
+ last_commit_iso: null,
1690
+ ahead: null,
1691
+ behind: null,
1692
+ pr: null
1693
+ };
1694
+ const repoPath = resolveLocalRepoPath(branch.repo);
1695
+ const git = getGitRunner();
1696
+ const gh = getGhRunner();
1697
+ if (repoPath) {
1698
+ try {
1699
+ const exists2 = await git("git", [
1700
+ "-C",
1701
+ repoPath,
1702
+ "rev-parse",
1703
+ "--verify",
1704
+ `refs/heads/${branch.name}`
1705
+ ]);
1706
+ out.present = exists2.code === 0;
1707
+ } catch {
1708
+ }
1709
+ if (out.present) {
1710
+ try {
1711
+ const lc = await git("git", ["-C", repoPath, "log", "-1", "--format=%cI", branch.name]);
1712
+ if (lc.code === 0) {
1713
+ const s = lc.stdout.trim();
1714
+ out.last_commit_iso = s.length > 0 ? s : null;
1715
+ }
1716
+ } catch {
1717
+ }
1718
+ for (const base of ["main", "master"]) {
1719
+ try {
1720
+ const verify = await git("git", [
1721
+ "-C",
1722
+ repoPath,
1723
+ "rev-parse",
1724
+ "--verify",
1725
+ `refs/remotes/origin/${base}`
1726
+ ]);
1727
+ if (verify.code !== 0) continue;
1728
+ const counts = await git("git", [
1729
+ "-C",
1730
+ repoPath,
1731
+ "rev-list",
1732
+ "--left-right",
1733
+ "--count",
1734
+ `origin/${base}...${branch.name}`
1735
+ ]);
1736
+ if (counts.code === 0) {
1737
+ const parts = counts.stdout.trim().split(/\s+/);
1738
+ if (parts.length === 2) {
1739
+ const b = Number(parts[0]);
1740
+ const a = Number(parts[1]);
1741
+ if (Number.isFinite(a) && Number.isFinite(b)) {
1742
+ out.ahead = a;
1743
+ out.behind = b;
1744
+ }
1745
+ }
1746
+ }
1747
+ break;
1748
+ } catch {
1749
+ }
1750
+ }
1751
+ }
1752
+ }
1753
+ try {
1754
+ const orgRepo = await resolveOrgRepo(branch.repo);
1755
+ if (orgRepo) {
1756
+ const res = await gh("gh", [
1757
+ "pr",
1758
+ "list",
1759
+ "--head",
1760
+ branch.name,
1761
+ "--repo",
1762
+ orgRepo,
1763
+ "--json",
1764
+ "number,state,title,url,statusCheckRollup",
1765
+ "--limit",
1766
+ "1"
1767
+ ]);
1768
+ if (res.code === 0) {
1769
+ const parsed = JSON.parse(res.stdout);
1770
+ if (Array.isArray(parsed) && parsed.length > 0) {
1771
+ const first = parsed[0];
1772
+ if (typeof first.number === "number" && typeof first.state === "string" && typeof first.title === "string" && typeof first.url === "string") {
1773
+ const rollup = first.statusCheckRollup ?? [];
1774
+ let pass = 0;
1775
+ let fail = 0;
1776
+ let pending = 0;
1777
+ for (const entry of rollup) {
1778
+ const tag = (entry.conclusion ?? entry.state ?? "").toUpperCase();
1779
+ if (tag === "SUCCESS") pass++;
1780
+ else if (tag === "FAILURE" || tag === "CANCELLED" || tag === "TIMED_OUT") fail++;
1781
+ else pending++;
1782
+ }
1783
+ let checks;
1784
+ if (rollup.length > 0) {
1785
+ if (fail > 0) checks = `fail (${fail}/${rollup.length})`;
1786
+ else if (pending > 0) checks = `pending (${pending}/${rollup.length})`;
1787
+ else checks = `pass (${pass}/${rollup.length})`;
1788
+ }
1789
+ out.pr = {
1790
+ number: first.number,
1791
+ state: first.state,
1792
+ title: first.title,
1793
+ url: first.url,
1794
+ ...checks ? { checks } : {}
1795
+ };
1796
+ }
1797
+ }
1798
+ }
1799
+ }
1800
+ } catch {
1801
+ }
1802
+ return out;
1803
+ }
1804
+ function endedDate(iso) {
1805
+ return iso.slice(0, 10);
1806
+ }
1807
+ function selectParallelSessions(sessions, narrativeSession) {
1808
+ if (!narrativeSession) return [];
1809
+ const cutoff = new Date(narrativeSession.frontmatter.ended).getTime();
1810
+ return sessions.filter(
1811
+ (s) => s !== narrativeSession && s.frontmatter.track !== "canonical" && new Date(s.frontmatter.ended).getTime() > cutoff
1812
+ );
1813
+ }
1814
+ function renderParallelSessions(sessions) {
1815
+ if (sessions.length === 0) return null;
1816
+ const lines = sessions.map((s) => {
1817
+ const { ended, session_id, track } = s.frontmatter;
1818
+ const summary = firstLine(s.body) ?? "_(empty session body)_";
1819
+ return `- ${endedDate(ended)} (${track}, ${session_id}) \u2014 ${summary}`;
1820
+ });
1821
+ return `# Parallel sessions since then
1822
+ ${lines.join("\n")}`;
1823
+ }
1824
+ var MS_PER_MINUTE = 6e4;
1825
+ function formatElapsedShort(from, now) {
1826
+ const diffMs = now.getTime() - from.getTime();
1827
+ if (!Number.isFinite(diffMs) || diffMs < MS_PER_MINUTE) return "just started";
1828
+ const minutes = Math.floor(diffMs / MS_PER_MINUTE);
1829
+ if (minutes < 60) return `${minutes}m ago`;
1830
+ const hours = Math.floor(minutes / 60);
1831
+ const rest = minutes % 60;
1832
+ return rest === 0 ? `${hours}h ago` : `${hours}h ${rest}m ago`;
1833
+ }
1834
+ function hasAgentChatChannel(brief) {
1835
+ return (brief.channels ?? []).some((raw) => {
1836
+ const name = raw.replace(/^(?:server|plugin):/, "").split("@")[0] ?? "";
1837
+ return name === "agent-chat";
1838
+ });
1839
+ }
1840
+ function renderSiblingLine(sibling, now) {
1841
+ const elapsed = formatElapsedShort(new Date(sibling.started), now);
1842
+ const where = `in \`${sibling.cwd}\``;
1843
+ if (sibling.mode === "launcher") {
1844
+ const pid = sibling.pid === void 0 ? "" : ` (pid ${sibling.pid})`;
1845
+ return `- started ${elapsed} ${where} \u2014 launched via \`aw\`, process still running${pid}.`;
1846
+ }
1847
+ return `- bootstrapped ${elapsed} ${where} \u2014 no live process to confirm, so it may have already exited.`;
1848
+ }
1849
+ var SIBLING_HEADING = "# Another session may already be live on this initiative";
1850
+ function renderSiblingSessions(siblings, brief, topTaskTitle, now) {
1851
+ if (siblings.length === 0) return "";
1852
+ const lines = siblings.map((s) => renderSiblingLine(s, now));
1853
+ const topTask = topTaskTitle ? ` ("${topTaskTitle}")` : "";
1854
+ lines.push(
1855
+ `Before starting the top task${topTask}, ask the user which session owns it. If this is the second session, take distinct scope and record it with \`active-work wrap --track adhoc\` \u2014 not \`canonical\`, which would bury the other session's mainline thread in the next bootstrap.`
1856
+ );
1857
+ if (hasAgentChatChannel(brief)) {
1858
+ lines.push(
1859
+ "This initiative carries an agent-chat channel: register under a name that distinguishes you from the other session and coordinate scope there."
1860
+ );
1861
+ }
1862
+ return `${SIBLING_HEADING}
1863
+ ${lines.join("\n")}`;
1864
+ }
1865
+ function renderBriefState(brief, now) {
1866
+ if (brief.state === "focused") return null;
1867
+ const lines = [];
1868
+ if (brief.paused_since) {
1869
+ const since = formatTimeSince(new Date(brief.paused_since), now);
1870
+ lines.push(`Paused since ${brief.paused_since} (${since}).`);
1871
+ }
1872
+ if (brief.restart_trigger) {
1873
+ lines.push(`Restart trigger: ${brief.restart_trigger}`);
1874
+ }
1875
+ lines.push(
1876
+ `This initiative is \`${brief.state}\`, not \`focused\` \u2014 confirm with the user before treating its tasks as current work.`
1877
+ );
1878
+ return `# Initiative state: ${brief.state}
1879
+ ${lines.join("\n")}`;
1880
+ }
1881
+ async function loadBrief(initiativeDir, slug) {
1882
+ const briefPath = path11.join(initiativeDir, "brief.md");
1883
+ try {
1884
+ return await readMarkdownWithSchema(briefPath, BriefFrontmatterSchema);
1885
+ } catch (err) {
1886
+ const reason = err instanceof Error ? err.message : String(err);
1887
+ throw new NotFoundError(`Initiative '${slug}' has no readable brief.md (${reason})`);
1888
+ }
1889
+ }
1890
+ async function assembleBootstrap(input) {
1891
+ const {
1892
+ activeRoot,
1893
+ slug,
1894
+ now = /* @__PURE__ */ new Date(),
1895
+ topNTasks = DEFAULT_TOP_N_TASKS,
1896
+ recentlyDoneDays = DEFAULT_RECENTLY_DONE_DAYS,
1897
+ includeLiveStatus = true,
1898
+ liveStatusFetcher,
1899
+ archivedTaskIds,
1900
+ adhoc = false,
1901
+ detectSiblings = true,
1902
+ siblingProbe = readLiveLeases,
1903
+ ownLeaseId
1904
+ } = input;
1905
+ const initiativeDir = path11.join(activeRoot, slug);
1906
+ const { frontmatter: brief, body: briefBody } = await loadBrief(initiativeDir, slug);
1907
+ const [loaded, loadedTasks, loadedArtifacts, notes] = await Promise.all([
1908
+ loadSessionsNewestFirst(initiativeDir),
1909
+ loadTasks(initiativeDir),
1910
+ loadArtifacts(initiativeDir),
1911
+ loadNotesFromDir(initiativeDir)
1912
+ ]);
1913
+ const { sessions, malformed } = loaded;
1914
+ const { tasks, malformed: malformedTasks } = loadedTasks;
1915
+ const { artifacts, error: artifactsError } = loadedArtifacts;
1916
+ const openLoops = deriveOpenLoopsFrom(loaded, { now, tasks });
1917
+ const resolvedLoops = deriveResolvedLoopsFrom(loaded, { now, tasks });
1918
+ const latestCanonical = sessions.find((s) => s.frontmatter.track === "canonical");
1919
+ const narrativeSession = latestCanonical ?? sessions[0];
1920
+ const usedFallbackTrack = !latestCanonical && narrativeSession !== void 0;
1921
+ const parallelBody = renderParallelSessions(selectParallelSessions(sessions, narrativeSession));
1922
+ const briefExcerpt = truncateLines(briefBody, BRIEF_BODY_MAX_LINES, path11.join(initiativeDir, "brief.md")) || "_(no brief body)_";
1923
+ const { body: tasksBody, count: openTaskCount } = renderTopTasks(tasks, topNTasks, slug);
1924
+ const { body: recentlyDoneBody, count: recentlyDoneCount } = renderRecentlyDone(
1925
+ tasks,
1926
+ recentlyDoneDays,
1927
+ now,
1928
+ slug
1929
+ );
1930
+ let artifactsBody = null;
1931
+ if (!includeLiveStatus || artifacts.branches.length === 0) {
1932
+ artifactsBody = renderStaticArtifacts(artifacts, slug);
1933
+ } else {
1934
+ const fetcher = liveStatusFetcher ?? defaultLiveStatusFetcher;
1935
+ try {
1936
+ const statuses = await fetcher(artifacts.branches);
1937
+ artifactsBody = renderLiveArtifacts(artifacts, statuses, slug);
1938
+ } catch {
1939
+ artifactsBody = renderStaticArtifacts(artifacts, slug);
1940
+ }
1941
+ }
1942
+ const timeSinceHuman = narrativeSession ? formatTimeSince(new Date(narrativeSession.frontmatter.ended), now) : void 0;
1943
+ let siblings = [];
1944
+ if (detectSiblings) {
1945
+ try {
1946
+ siblings = await siblingProbe({
1947
+ activeRoot,
1948
+ slug,
1949
+ now,
1950
+ ...ownLeaseId ? { excludeLeaseId: ownLeaseId } : {}
1951
+ });
1952
+ } catch {
1953
+ siblings = [];
1954
+ }
1955
+ }
1956
+ const topTaskTitle = tasks.filter((t) => t.status === "open").sort(compareTasksByPriority)[0]?.title;
1957
+ const sections = [];
1958
+ sections.push(
1959
+ adhoc ? `Starting an ad-hoc session on \`${slug}\` (${brief.title}). This session is scoped to ad-hoc work related to this workstream \u2014 not necessarily its handoff or current top task. The context below is background so you're oriented; wait for the user to describe the specific ad-hoc task before acting.` : `Starting a session on \`${slug}\` (${brief.title}).`
1960
+ );
1961
+ const siblingBody = renderSiblingSessions(siblings, brief, topTaskTitle, now);
1962
+ if (siblingBody) sections.push(siblingBody);
1963
+ const stateBody = renderBriefState(brief, now);
1964
+ if (stateBody) sections.push(stateBody);
1965
+ sections.push(`# Why we're doing this
1966
+ ${briefExcerpt}`);
1967
+ sections.push(renderOpenLoops(openLoops, malformed, sessions[0]));
1968
+ const abandonedBody = renderAbandonedLoops(resolvedLoops, recentlyDoneDays);
1969
+ if (abandonedBody) sections.push(abandonedBody);
1970
+ if (narrativeSession) {
1971
+ const sessionExcerpt = truncateLines(
1972
+ narrativeSession.body,
1973
+ SESSION_BODY_MAX_LINES,
1974
+ path11.join(initiativeDir, "sessions", `${narrativeSession.sessionFile}.md`)
1975
+ ) || "_(empty session body)_";
1976
+ const ended = endedDate(narrativeSession.frontmatter.ended);
1977
+ const trackLabel = usedFallbackTrack ? ` (${narrativeSession.frontmatter.track})` : "";
1978
+ sections.push(
1979
+ `# Last session${trackLabel} (${ended}, ${narrativeSession.frontmatter.session_id}) \u2014 ${timeSinceHuman}
1980
+ ${sessionExcerpt}`
1981
+ );
1982
+ } else {
1983
+ sections.push(`# Last session
1984
+ No previous sessions recorded.`);
1985
+ }
1986
+ if (parallelBody) sections.push(parallelBody);
1987
+ sections.push(
1988
+ `# Tasks (top ${topNTasks} open by priority)${malformedTaskNote(malformedTasks)}
1989
+ ${tasksBody}`
1990
+ );
1991
+ if (recentlyDoneBody) {
1992
+ sections.push(`# Recently done (last ${recentlyDoneDays} days)
1993
+ ${recentlyDoneBody}`);
1994
+ }
1995
+ if (archivedTaskIds && archivedTaskIds.length > 0) {
1996
+ sections.push(
1997
+ `# Archived (housekeeping)
1998
+ Moved ${archivedTaskIds.length} stale done task(s) to tasks/archive/: ${archivedTaskIds.join(", ")}`
1999
+ );
2000
+ }
2001
+ const notesBody = renderDurableNotes(notes, slug);
2002
+ if (notesBody) sections.push(notesBody);
2003
+ if (artifactsError) {
2004
+ const artifactsPath = path11.join(initiativeDir, "artifacts.yml");
2005
+ sections.push(
2006
+ `# Open artifacts
2007
+ _${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\`._`
2008
+ );
2009
+ } else if (artifactsBody) {
2010
+ sections.push(`# Open artifacts
2011
+ ${artifactsBody}`);
2012
+ }
2013
+ const bootstrapAt = nowIso();
2014
+ const todayStr = today();
2015
+ const contextLines = [`- Today: ${todayStr}`, `- Bootstrap: ${bootstrapAt}`];
2016
+ if (timeSinceHuman) {
2017
+ contextLines.push(`- Time since last session: ${timeSinceHuman}`);
2018
+ }
2019
+ sections.push(`# Context
2020
+ ${contextLines.join("\n")}`);
2021
+ sections.push(
2022
+ adhoc ? `This is an ad-hoc session: treat the context above as background, not a directive. Do not assume we're continuing the top task or the handoff \u2014 the user will describe the specific ad-hoc task. Once they do, work it with the workstream context in mind. If it turns out to be substantive, still capture it via \`active-work task add\` / \`active-work wrap --track adhoc\`. The \`--track adhoc\` flag is required: this session runs alongside the mainline thread, and recording it as canonical would bury the real last session for the next bootstrap.` : renderClosingInstruction(openLoops)
2023
+ );
2024
+ const prompt = sections.join("\n\n") + "\n";
2025
+ const metadata = {
2026
+ slug,
2027
+ brief_title: brief.title,
2028
+ open_task_count: openTaskCount,
2029
+ open_loop_count: openLoops.length,
2030
+ recently_done_count: recentlyDoneCount,
2031
+ bootstrap_at: bootstrapAt
2032
+ };
2033
+ if (narrativeSession) {
2034
+ metadata.last_session = {
2035
+ filename: `${narrativeSession.sessionFile}.md`,
2036
+ ended: narrativeSession.frontmatter.ended
2037
+ };
2038
+ }
2039
+ if (timeSinceHuman) {
2040
+ metadata.time_since_last_session_human = timeSinceHuman;
2041
+ }
2042
+ if (siblings.length > 0) {
2043
+ metadata.sibling_sessions = siblings.length;
2044
+ }
2045
+ return { prompt, metadata };
2046
+ }
2047
+
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
+ // src/commands/_open-helpers.ts
2090
+ import { promises as fs12 } from "fs";
2091
+ import path13 from "path";
2092
+ async function listInitiativeSlugs(activeRoot) {
2093
+ let entries;
2094
+ try {
2095
+ entries = await fs12.readdir(activeRoot, { withFileTypes: true });
2096
+ } catch {
2097
+ return [];
2098
+ }
2099
+ return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name).sort();
2100
+ }
2101
+ async function resolveSlug(activeRoot, input) {
2102
+ const slugs = await listInitiativeSlugs(activeRoot);
2103
+ if (slugs.includes(input)) return input;
2104
+ const matches = slugs.filter((s) => s.startsWith(input));
2105
+ if (matches.length === 1) return matches[0];
2106
+ if (matches.length > 1) {
2107
+ throw new NotFoundError(`Ambiguous slug '${input}'. Candidates: ${matches.join(", ")}`);
2108
+ }
2109
+ if (slugs.length === 0) {
2110
+ throw new NotFoundError(`No initiatives found under ${activeRoot}`);
2111
+ }
2112
+ throw new NotFoundError(`No initiative matches '${input}'. Known: ${slugs.join(", ")}`);
2113
+ }
2114
+ function isInside(child, parent) {
2115
+ const rel = path13.relative(parent, child);
2116
+ return rel === "" || !rel.startsWith("..") && !path13.isAbsolute(rel);
2117
+ }
2118
+ async function canonicalize(p) {
2119
+ try {
2120
+ return await fs12.realpath(p);
2121
+ } catch {
2122
+ return path13.resolve(p);
2123
+ }
2124
+ }
2125
+ async function resolveSlugFromCwd(activeRoot, cwd) {
2126
+ const resolvedCwd = await canonicalize(cwd);
2127
+ const slugs = await listInitiativeSlugs(activeRoot);
2128
+ let best = null;
2129
+ let tiedAtBest = false;
2130
+ for (const slug of slugs) {
2131
+ const briefPath = path13.join(activeRoot, slug, "brief.md");
2132
+ try {
2133
+ await readMarkdownWithSchema(briefPath, BriefFrontmatterSchema);
2134
+ } catch {
2135
+ continue;
2136
+ }
2137
+ const registered = await readRegisteredWorktrees(path13.join(activeRoot, slug));
2138
+ for (const entry of registered) {
2139
+ const displayPath = expandTilde(entry.path);
2140
+ if (!path13.isAbsolute(displayPath)) continue;
2141
+ const canonical = await canonicalize(displayPath);
2142
+ if (!isInside(resolvedCwd, canonical)) continue;
2143
+ const depth = canonical.length;
2144
+ if (best === null || depth > best.depth) {
2145
+ best = { slug, worktreePath: displayPath, depth };
2146
+ tiedAtBest = false;
2147
+ } else if (depth === best.depth && slug !== best.slug) {
2148
+ tiedAtBest = true;
2149
+ }
2150
+ }
2151
+ }
2152
+ if (best === null || tiedAtBest) return null;
2153
+ return { slug: best.slug, worktreePath: best.worktreePath };
2154
+ }
2155
+
2156
+ // src/commands/open.ts
2157
+ var ARCHIVE_DONE_AFTER_DAYS = 30;
2158
+ var ArgsSchema2 = z8.object({
2159
+ slug: z8.string().min(1).optional(),
2160
+ offline: z8.boolean().optional(),
2161
+ // Directory used to auto-resolve an initiative when no slug is given.
2162
+ // Defaults to the process cwd; callers that do not share the user's shell
2163
+ // cwd (the daemon / MCP server) must pass this explicitly.
2164
+ cwd: z8.string().min(1).optional(),
2165
+ // Force the picker even when the cwd matches an initiative's worktree.
2166
+ pick: z8.boolean().optional(),
2167
+ // Frame the bootstrap prompt as ad-hoc work related to the workstream rather
2168
+ // than a continuation of its handoff / top task.
2169
+ adhoc: z8.boolean().optional(),
2170
+ // Skip the sibling-session probe (and the lease write that goes with it).
2171
+ no_sibling_check: z8.boolean().optional(),
2172
+ // Internal: `aw` calls this command in-process and holds a `launcher` lease
2173
+ // of its own for the same session, so it suppresses the oneshot lease here
2174
+ // rather than writing a second one that would then look like a sibling.
2175
+ lease_mode: z8.literal("defer").optional()
2176
+ });
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()
2182
+ });
2183
+ var PickerResultSchema = z8.object({
2184
+ picker: z8.literal(true),
2185
+ initiatives: z8.array(InitiativeSummarySchema)
2186
+ });
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()
2201
+ }),
2202
+ // How the initiative was selected: an explicit/prefix slug, or a match
2203
+ // between the caller's cwd and one of the initiative's worktrees.
2204
+ resolved_from: z8.enum(["slug", "cwd"]).optional()
2205
+ });
2206
+ var ResultSchema2 = z8.union([OpenResultSchema, PickerResultSchema]);
2207
+ var STATE_ORDER = {
2208
+ focused: 0,
2209
+ backburner: 1,
2210
+ paused: 2,
2211
+ done: 3
2212
+ };
2213
+ async function loadInitiativeSummary(activeRoot, slug) {
2214
+ const briefPath = path14.join(activeRoot, slug, "brief.md");
2215
+ try {
2216
+ const { frontmatter } = await readMarkdownWithSchema(briefPath, BriefFrontmatterSchema);
2217
+ return {
2218
+ slug,
2219
+ title: frontmatter.title,
2220
+ state: frontmatter.state,
2221
+ rank: frontmatter.rank
2222
+ };
2223
+ } catch {
2224
+ return null;
2225
+ }
2226
+ }
2227
+ function compareInitiatives(a, b) {
2228
+ const stateDiff = STATE_ORDER[a.state] - STATE_ORDER[b.state];
2229
+ if (stateDiff !== 0) return stateDiff;
2230
+ if (a.rank !== void 0 && b.rank !== void 0 && a.rank !== b.rank) {
2231
+ return a.rank - b.rank;
2232
+ }
2233
+ if (a.rank !== void 0 && b.rank === void 0) return -1;
2234
+ if (a.rank === void 0 && b.rank !== void 0) return 1;
2235
+ return a.slug.localeCompare(b.slug);
2236
+ }
2237
+ async function collectInitiatives(activeRoot) {
2238
+ const slugs = await listInitiativeSlugs(activeRoot);
2239
+ const summaries = [];
2240
+ for (const slug of slugs) {
2241
+ const summary = await loadInitiativeSummary(activeRoot, slug);
2242
+ if (summary) summaries.push(summary);
2243
+ }
2244
+ summaries.sort(compareInitiatives);
2245
+ return summaries;
2246
+ }
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
+ async function claimOneshotLease(activeRoot, slug, cwd) {
2253
+ try {
2254
+ await acquireLease({ activeRoot, slug, cwd, mode: "oneshot" });
2255
+ } catch {
2256
+ }
2257
+ }
2258
+ async function bootstrapInitiative(activeRoot, slug, opts) {
2259
+ const briefPath = path14.join(activeRoot, slug, "brief.md");
2260
+ const { frontmatter: brief } = await readMarkdownWithSchema(briefPath, BriefFrontmatterSchema);
2261
+ const cwdHint = opts.cwdHintOverride ?? await resolveCwdHint(activeRoot, slug);
2262
+ const archivedTaskIds = await archiveStaleTasks(path14.join(activeRoot, slug), {
2263
+ retentionDays: ARCHIVE_DONE_AFTER_DAYS,
2264
+ now: /* @__PURE__ */ new Date()
2265
+ });
2266
+ const detectSiblings = opts.detectSiblings !== false;
2267
+ const { prompt, metadata } = await assembleBootstrap({
2268
+ activeRoot,
2269
+ slug,
2270
+ includeLiveStatus: !opts.offline,
2271
+ archivedTaskIds,
2272
+ adhoc: opts.adhoc,
2273
+ detectSiblings,
2274
+ ...process.env.AW_LEASE_ID ? { ownLeaseId: process.env.AW_LEASE_ID } : {}
2275
+ });
2276
+ if (detectSiblings && !opts.deferLease) {
2277
+ await claimOneshotLease(activeRoot, slug, cwdHint);
2278
+ }
2279
+ return {
2280
+ slug,
2281
+ prompt,
2282
+ cwd_hint: cwdHint,
2283
+ ...brief.channels && brief.channels.length > 0 ? { channels: brief.channels } : {},
2284
+ metadata,
2285
+ resolved_from: opts.resolvedFrom
2286
+ };
2287
+ }
2288
+ var openCommand = defineCommand({
2289
+ name: "open",
2290
+ description: "Bootstrap a Claude session for an initiative. Without a slug, resolves the initiative whose worktree contains the caller's cwd; falls back to the picker list when nothing matches.",
2291
+ args: ArgsSchema2,
2292
+ result: ResultSchema2,
2293
+ cli: {
2294
+ positional: ["slug"],
2295
+ options: {
2296
+ offline: {
2297
+ long: "--offline",
2298
+ description: "Skip the live `gh`/`git` artifact lookup; render artifacts statically."
2299
+ },
2300
+ cwd: {
2301
+ long: "--cwd",
2302
+ description: "Directory to resolve the initiative from when no slug is given (default: current directory)."
2303
+ },
2304
+ pick: {
2305
+ long: "--pick",
2306
+ description: "Always return the picker list; skip resolving the initiative from the current directory."
2307
+ },
2308
+ adhoc: {
2309
+ long: "--adhoc",
2310
+ description: "Frame the prompt as ad-hoc work on the workstream (awaiting the user\u2019s task), not a continuation of the handoff / top task."
2311
+ },
2312
+ no_sibling_check: {
2313
+ long: "--no-sibling-check",
2314
+ description: "Skip the check for another session already live on this initiative, and do not record a lease for this one."
2315
+ }
2316
+ },
2317
+ usage: "active-work open [slug] [--offline] [--cwd <dir>] [--pick] [--adhoc] [--no-sibling-check]"
2318
+ },
2319
+ async run(args, ctx) {
2320
+ const activeRoot = ctx.activeRoot ?? getActiveRoot();
2321
+ const detectSiblings = !args.no_sibling_check && !args.offline;
2322
+ const deferLease = args.lease_mode === "defer";
2323
+ if (args.slug) {
2324
+ const slug = await resolveSlug(activeRoot, args.slug);
2325
+ return bootstrapInitiative(activeRoot, slug, {
2326
+ offline: args.offline,
2327
+ resolvedFrom: "slug",
2328
+ adhoc: args.adhoc,
2329
+ detectSiblings,
2330
+ deferLease
2331
+ });
2332
+ }
2333
+ const cwd = args.cwd ?? ctx.cwd;
2334
+ if (!args.pick && cwd) {
2335
+ const matched = await resolveSlugFromCwd(activeRoot, cwd);
2336
+ if (matched) {
2337
+ return bootstrapInitiative(activeRoot, matched.slug, {
2338
+ offline: args.offline,
2339
+ resolvedFrom: "cwd",
2340
+ cwdHintOverride: matched.worktreePath,
2341
+ adhoc: args.adhoc,
2342
+ detectSiblings,
2343
+ deferLease
2344
+ });
2345
+ }
2346
+ }
2347
+ const initiatives = await collectInitiatives(activeRoot);
2348
+ return { picker: true, initiatives };
2349
+ }
2350
+ });
2351
+ var open_default = openCommand;
2352
+
2353
+ // src/utils/color.ts
2354
+ import pc from "picocolors";
2355
+ var enabled = !("NO_COLOR" in process.env) && process.stdout.isTTY === true;
2356
+ var identity = (s) => s;
2357
+ var color = {
2358
+ enabled,
2359
+ bold: enabled ? pc.bold : identity,
2360
+ dim: enabled ? pc.dim : identity,
2361
+ green: enabled ? pc.green : identity,
2362
+ yellow: enabled ? pc.yellow : identity,
2363
+ red: enabled ? pc.red : identity,
2364
+ cyan: enabled ? pc.cyan : identity,
2365
+ gray: enabled ? pc.gray : identity
2366
+ };
2367
+
2368
+ export {
2369
+ TaskSeqSchema,
2370
+ BriefFrontmatterSchema,
2371
+ expandTilde,
2372
+ getActiveRoot,
2373
+ getStateRoot,
2374
+ getConfigRoot,
2375
+ getInitiativeDir,
2376
+ 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,
2391
+ defineCommand,
2392
+ successEnvelope,
2393
+ errorEnvelope,
2394
+ registry,
2395
+ register,
2396
+ TaskSchema,
2397
+ SessionIdSchema,
2398
+ NextStepSchema,
2399
+ SessionResolveSchema,
2400
+ SessionFrontmatterSchema,
2401
+ loadSessionsFromDir,
2402
+ deriveOpenLoopsFrom,
2403
+ deriveOpenLoops,
2404
+ deriveResolvedLoopsFrom,
2405
+ findDanglingResolves,
2406
+ findSessionIssues,
2407
+ NoteKindSchema,
2408
+ NOTE_TITLE_MAX_LENGTH,
2409
+ readFrontmatter,
2410
+ readRawFrontmatter,
2411
+ writeFrontmatter,
2412
+ today,
2413
+ nowIso,
2414
+ EXIT,
2415
+ ActiveWorkError,
2416
+ ValidationError,
2417
+ NotFoundError,
2418
+ UsageError,
2419
+ DaemonError,
2420
+ ConfigError,
2421
+ formatError,
2422
+ source_add_default,
2423
+ loadNotesFromDir,
2424
+ writeNoteFile,
2425
+ writePidFile,
2426
+ readPidFile,
2427
+ removePidFile,
2428
+ DEFAULT_DAEMON_PORT,
2429
+ resolveDaemonPort,
2430
+ probeHealth,
2431
+ isProcessAlive,
2432
+ acquireLease,
2433
+ releaseLease,
2434
+ releaseLeaseSync,
2435
+ sweepAllLeases,
2436
+ getGitRunner,
2437
+ getGhRunner,
2438
+ resolveLocalRepoPath,
2439
+ resolveOrgRepo,
2440
+ assembleBootstrap,
2441
+ resolveSlug,
2442
+ resolveSlugFromCwd,
2443
+ open_default,
2444
+ color
2445
+ };
2446
+ //# sourceMappingURL=chunk-FM2KVFDO.js.map