ftown 0.26.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/attention-cli.d.ts +32 -0
  2. package/dist/attention-cli.js +310 -0
  3. package/dist/attention-cli.js.map +1 -0
  4. package/dist/attention-events.d.ts +2 -1
  5. package/dist/attention-events.js.map +1 -1
  6. package/dist/attention-store.d.ts +6 -3
  7. package/dist/attention-store.js +60 -2
  8. package/dist/attention-store.js.map +1 -1
  9. package/dist/brief-cli.d.ts +10 -0
  10. package/dist/brief-cli.js +163 -0
  11. package/dist/brief-cli.js.map +1 -0
  12. package/dist/command-rpc.d.ts +11 -0
  13. package/dist/command-rpc.js +83 -16
  14. package/dist/command-rpc.js.map +1 -1
  15. package/dist/coordination/attention-routing.d.ts +37 -0
  16. package/dist/coordination/attention-routing.js +154 -0
  17. package/dist/coordination/attention-routing.js.map +1 -0
  18. package/dist/coordination/board.js +82 -3
  19. package/dist/coordination/board.js.map +1 -1
  20. package/dist/coordination/contract-b.d.ts +206 -0
  21. package/dist/coordination/contract-b.js +58 -0
  22. package/dist/coordination/contract-b.js.map +1 -0
  23. package/dist/coordination/contract-c.d.ts +117 -0
  24. package/dist/coordination/contract-c.js +47 -0
  25. package/dist/coordination/contract-c.js.map +1 -0
  26. package/dist/coordination/contract.d.ts +8 -1
  27. package/dist/coordination/contract.js +1 -1
  28. package/dist/coordination/contract.js.map +1 -1
  29. package/dist/coordination/format.js +51 -12
  30. package/dist/coordination/format.js.map +1 -1
  31. package/dist/coordination/plan.d.ts +74 -0
  32. package/dist/coordination/plan.js +534 -0
  33. package/dist/coordination/plan.js.map +1 -0
  34. package/dist/coordination/routes-brief.d.ts +10 -0
  35. package/dist/coordination/routes-brief.js +149 -0
  36. package/dist/coordination/routes-brief.js.map +1 -0
  37. package/dist/coordination/routes-plan.d.ts +13 -0
  38. package/dist/coordination/routes-plan.js +182 -0
  39. package/dist/coordination/routes-plan.js.map +1 -0
  40. package/dist/coordination/routes-worktrees.d.ts +29 -0
  41. package/dist/coordination/routes-worktrees.js +138 -0
  42. package/dist/coordination/routes-worktrees.js.map +1 -0
  43. package/dist/coordination/wiring.d.ts +17 -2
  44. package/dist/coordination/wiring.js +81 -11
  45. package/dist/coordination/wiring.js.map +1 -1
  46. package/dist/coordination-cli.d.ts +6 -1
  47. package/dist/coordination-cli.js +23 -7
  48. package/dist/coordination-cli.js.map +1 -1
  49. package/dist/create-ftown-session.d.ts +26 -1
  50. package/dist/create-ftown-session.js +208 -104
  51. package/dist/create-ftown-session.js.map +1 -1
  52. package/dist/ftown-cli.js +14 -2
  53. package/dist/ftown-cli.js.map +1 -1
  54. package/dist/index.js +29 -0
  55. package/dist/index.js.map +1 -1
  56. package/dist/local-api-server.d.ts +24 -0
  57. package/dist/local-api-server.js +473 -32
  58. package/dist/local-api-server.js.map +1 -1
  59. package/dist/mail-delivery.d.ts +18 -4
  60. package/dist/mail-delivery.js +50 -8
  61. package/dist/mail-delivery.js.map +1 -1
  62. package/dist/plan-cli.d.ts +52 -0
  63. package/dist/plan-cli.js +357 -0
  64. package/dist/plan-cli.js.map +1 -0
  65. package/dist/security/bridge/permissions.js +55 -5
  66. package/dist/security/bridge/permissions.js.map +1 -1
  67. package/dist/session-controller.d.ts +17 -1
  68. package/dist/session-controller.js +93 -2
  69. package/dist/session-controller.js.map +1 -1
  70. package/dist/sessions-commands.js +75 -5
  71. package/dist/sessions-commands.js.map +1 -1
  72. package/dist/types.d.ts +9 -1
  73. package/dist/worktrees-cli.d.ts +39 -0
  74. package/dist/worktrees-cli.js +168 -0
  75. package/dist/worktrees-cli.js.map +1 -0
  76. package/dist/worktrees.d.ts +16 -0
  77. package/dist/worktrees.js +292 -0
  78. package/dist/worktrees.js.map +1 -0
  79. package/hooks/ftown-antigravity-hook.sh +12 -6
  80. package/package.json +2 -2
  81. package/skills/ftown/SKILL.md +88 -5
  82. package/skills/ftown/references/orchestrator.md +32 -29
  83. package/skills/ftown/references/sessions.md +155 -12
@@ -0,0 +1,149 @@
1
+ import { BRIEF_LIMITS, RouteError } from './contract-b.js';
2
+ const BRIEF_PATTERN = /^\/api\/sessions\/([^/]+)\/brief$/;
3
+ const NOTES_PATTERN = /^\/api\/sessions\/([^/]+)\/brief\/notes$/;
4
+ function hasOwn(record, key) {
5
+ return Object.prototype.hasOwnProperty.call(record, key);
6
+ }
7
+ /** Strips control characters except \n and \t (C0, DEL, and C1 U+0080–U+009F). */
8
+ function stripControlChars(value) {
9
+ return [...value].map((char) => {
10
+ const code = char.codePointAt(0) ?? 0;
11
+ const isControl = (code <= 31 && code !== 9 && code !== 10)
12
+ || code === 127
13
+ || (code >= 0x80 && code <= 0x9f);
14
+ return isControl ? '' : char;
15
+ }).join('');
16
+ }
17
+ function requireOnlyKeys(body, allowed) {
18
+ for (const key of Object.keys(body)) {
19
+ if (!allowed.includes(key))
20
+ throw new RouteError(400, `Unknown body key: ${key}`);
21
+ }
22
+ }
23
+ /** Cleans markdown brief text; keeps newlines/tabs; over-limit → 400. Blank-after-cleaning clears the text. */
24
+ function parseBriefText(value) {
25
+ if (value === null)
26
+ return null;
27
+ if (typeof value !== 'string')
28
+ throw new RouteError(400, 'text must be a string or null');
29
+ const cleaned = stripControlChars(value);
30
+ if ([...cleaned].length > BRIEF_LIMITS.text) {
31
+ throw new RouteError(400, `text must be at most ${BRIEF_LIMITS.text} characters`);
32
+ }
33
+ return cleaned.trim() === '' ? null : cleaned;
34
+ }
35
+ /** Cleans a single note: control chars stripped, single line, 1..BRIEF_LIMITS.note characters. */
36
+ function parseNoteText(value) {
37
+ if (typeof value !== 'string')
38
+ throw new RouteError(400, 'text must be a string');
39
+ const cleaned = stripControlChars(value).replace(/\s+/g, ' ').trim();
40
+ const length = [...cleaned].length;
41
+ if (length < 1)
42
+ throw new RouteError(400, 'text must not be empty');
43
+ if (length > BRIEF_LIMITS.note) {
44
+ throw new RouteError(400, `text must be at most ${BRIEF_LIMITS.note} characters`);
45
+ }
46
+ return cleaned;
47
+ }
48
+ function parseBodyText(body) {
49
+ requireOnlyKeys(body, ['text']);
50
+ const hasText = hasOwn(body, 'text') && body.text !== undefined;
51
+ if (!hasText)
52
+ throw new RouteError(400, 'text is required');
53
+ return body.text;
54
+ }
55
+ function buildBrief(existing, text) {
56
+ const brief = { notes: existing?.notes ?? [], updatedAt: new Date().toISOString() };
57
+ if (text !== null)
58
+ brief.text = text;
59
+ return brief;
60
+ }
61
+ function appendNote(existing, note) {
62
+ const brief = { notes: [...(existing?.notes ?? []), note], updatedAt: new Date().toISOString() };
63
+ if (existing?.text !== undefined)
64
+ brief.text = existing.text;
65
+ // Newest last, capped by dropping the oldest.
66
+ brief.notes = brief.notes.slice(-BRIEF_LIMITS.notes);
67
+ return brief;
68
+ }
69
+ async function loadSessionOr404(ctx, id) {
70
+ const session = await ctx.loadSession(id);
71
+ if (!session)
72
+ throw new RouteError(404, 'Session not found');
73
+ return session;
74
+ }
75
+ /**
76
+ * Reloads the session just before saving and derives the next brief from it via `mutate`,
77
+ * so concurrent writers never lose each other's changes. undefined removes the brief.
78
+ */
79
+ async function mutateBrief(ctx, id, mutate) {
80
+ const fresh = await ctx.loadSession(id);
81
+ if (!fresh)
82
+ throw new RouteError(404, 'Session not found');
83
+ const next = mutate(fresh.brief);
84
+ const saved = { ...fresh };
85
+ if (next === undefined) {
86
+ delete saved.brief;
87
+ }
88
+ else {
89
+ saved.brief = next;
90
+ }
91
+ await ctx.saveSession(saved);
92
+ ctx.publishSession(saved);
93
+ return next;
94
+ }
95
+ /**
96
+ * Route module for BRIEF_ROUTES:
97
+ * GET /api/sessions/:id/brief → { brief: SessionBrief | null }
98
+ * PUT /api/sessions/:id/brief → { brief } (body { text: string | null })
99
+ * DELETE /api/sessions/:id/brief → { ok: true }
100
+ * POST /api/sessions/:id/brief/notes → { brief } (body { text: string })
101
+ * Returns false when the path does not match. Throws RouteError for HTTP errors.
102
+ */
103
+ export async function handleBriefRoutes(ctx) {
104
+ const briefMatch = BRIEF_PATTERN.exec(ctx.path);
105
+ if (briefMatch) {
106
+ const id = briefMatch[1];
107
+ if (ctx.method === 'GET') {
108
+ const session = await loadSessionOr404(ctx, id);
109
+ ctx.json(200, { brief: session.brief ?? null });
110
+ return true;
111
+ }
112
+ if (ctx.method === 'PUT') {
113
+ await loadSessionOr404(ctx, id);
114
+ const text = parseBodyText(await ctx.parseBody());
115
+ const parsed = parseBriefText(text);
116
+ const next = await mutateBrief(ctx, id, (existing) => {
117
+ const brief = buildBrief(existing, parsed);
118
+ return brief.text === undefined && brief.notes.length === 0 ? undefined : brief;
119
+ });
120
+ ctx.json(200, { brief: next ?? null });
121
+ return true;
122
+ }
123
+ if (ctx.method === 'DELETE') {
124
+ await loadSessionOr404(ctx, id);
125
+ await mutateBrief(ctx, id, () => undefined);
126
+ ctx.json(200, { ok: true });
127
+ return true;
128
+ }
129
+ throw new RouteError(405, 'Method not allowed');
130
+ }
131
+ const notesMatch = NOTES_PATTERN.exec(ctx.path);
132
+ if (notesMatch) {
133
+ const id = notesMatch[1];
134
+ if (ctx.method === 'POST') {
135
+ await loadSessionOr404(ctx, id);
136
+ const text = parseBodyText(await ctx.parseBody());
137
+ const noteText = parseNoteText(text);
138
+ const next = await mutateBrief(ctx, id, (existing) => {
139
+ const note = { ts: new Date().toISOString(), text: noteText };
140
+ return appendNote(existing, note);
141
+ });
142
+ ctx.json(200, { brief: next });
143
+ return true;
144
+ }
145
+ throw new RouteError(405, 'Method not allowed');
146
+ }
147
+ return false;
148
+ }
149
+ //# sourceMappingURL=routes-brief.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes-brief.js","sourceRoot":"","sources":["../../src/coordination/routes-brief.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import { type PlanStore } from './plan.js';
2
+ import { type RouteModule } from './contract-b.js';
3
+ /**
4
+ * Route module factory for PLAN_ROUTES:
5
+ * GET /api/sessions/:id/plan → PlanView (ordered via orderPlan)
6
+ * POST /api/sessions/:id/plan → { item } (body { title, note?, after?, priority? })
7
+ * PATCH /api/sessions/:id/plan/:itemId → { item } (body { title?, note?: string|null, after?, priority?, state?, sessionId?: string|null })
8
+ * DELETE /api/sessions/:id/plan/:itemId → { ok: true }
9
+ * Strict bodies (unknown keys → 400), PlanValidationError → 400 (a store-reported missing
10
+ * item or session → 404), unknown session/item → 404, wrong method → 405.
11
+ * Item ids must match p\d{1,6}; other paths fall through (returns false).
12
+ */
13
+ export declare function createPlanRoutes(store: PlanStore): RouteModule;
@@ -0,0 +1,182 @@
1
+ import { PLAN_STATES, PlanValidationError } from './contract-c.js';
2
+ import { orderPlan } from './plan.js';
3
+ import { RouteError } from './contract-b.js';
4
+ const PLAN_PATTERN = /^\/api\/sessions\/([^/]+)\/plan$/;
5
+ const ITEM_PATTERN = /^\/api\/sessions\/([^/]+)\/plan\/(p\d{1,6})$/;
6
+ function hasOwn(record, key) {
7
+ return Object.prototype.hasOwnProperty.call(record, key);
8
+ }
9
+ function requireOnlyKeys(body, allowed) {
10
+ for (const key of Object.keys(body)) {
11
+ if (!allowed.includes(key))
12
+ throw new RouteError(400, `Unknown body key: ${key}`);
13
+ }
14
+ }
15
+ function requireString(body, key) {
16
+ const value = body[key];
17
+ if (typeof value !== 'string')
18
+ throw new RouteError(400, `${key} must be a string`);
19
+ return value;
20
+ }
21
+ function parseOptionalString(body, key) {
22
+ if (!hasOwn(body, key) || body[key] === undefined)
23
+ return undefined;
24
+ return requireString(body, key);
25
+ }
26
+ function parseNullableString(body, key) {
27
+ if (!hasOwn(body, key) || body[key] === undefined)
28
+ return undefined;
29
+ if (body[key] === null)
30
+ return null;
31
+ return requireString(body, key);
32
+ }
33
+ function parseOptionalAfter(body) {
34
+ if (!hasOwn(body, 'after') || body.after === undefined)
35
+ return undefined;
36
+ const value = body.after;
37
+ if (!Array.isArray(value) || value.some((id) => typeof id !== 'string')) {
38
+ throw new RouteError(400, 'after must be an array of item ids');
39
+ }
40
+ return value;
41
+ }
42
+ function parseOptionalPriority(body) {
43
+ if (!hasOwn(body, 'priority') || body.priority === undefined)
44
+ return undefined;
45
+ const value = body.priority;
46
+ if (value !== 1 && value !== 2 && value !== 3)
47
+ throw new RouteError(400, 'priority must be 1, 2 or 3');
48
+ return value;
49
+ }
50
+ function parseOptionalState(body) {
51
+ if (!hasOwn(body, 'state') || body.state === undefined)
52
+ return undefined;
53
+ const value = body.state;
54
+ if (typeof value !== 'string' || !PLAN_STATES.includes(value)) {
55
+ throw new RouteError(400, `state must be one of: ${PLAN_STATES.join(', ')}`);
56
+ }
57
+ return value;
58
+ }
59
+ function parseAddBody(body) {
60
+ requireOnlyKeys(body, ['title', 'note', 'after', 'priority']);
61
+ if (!hasOwn(body, 'title') || body.title === undefined)
62
+ throw new RouteError(400, 'title is required');
63
+ const input = { title: requireString(body, 'title') };
64
+ const note = parseOptionalString(body, 'note');
65
+ if (note !== undefined)
66
+ input.note = note;
67
+ const after = parseOptionalAfter(body);
68
+ if (after !== undefined)
69
+ input.after = after;
70
+ const priority = parseOptionalPriority(body);
71
+ if (priority !== undefined)
72
+ input.priority = priority;
73
+ return input;
74
+ }
75
+ function parsePatchBody(body) {
76
+ requireOnlyKeys(body, ['title', 'note', 'after', 'priority', 'state', 'sessionId']);
77
+ const input = {};
78
+ const title = parseOptionalString(body, 'title');
79
+ if (title !== undefined)
80
+ input.title = title;
81
+ const note = parseNullableString(body, 'note');
82
+ if (note !== undefined)
83
+ input.note = note;
84
+ const after = parseOptionalAfter(body);
85
+ if (after !== undefined)
86
+ input.after = after;
87
+ const priority = parseOptionalPriority(body);
88
+ if (priority !== undefined)
89
+ input.priority = priority;
90
+ const state = parseOptionalState(body);
91
+ if (state !== undefined)
92
+ input.state = state;
93
+ const sessionId = parseNullableString(body, 'sessionId');
94
+ if (sessionId !== undefined)
95
+ input.sessionId = sessionId;
96
+ return input;
97
+ }
98
+ /**
99
+ * Maps the store's errors to HTTP statuses: a vanished item or session → 404,
100
+ * any other PlanValidationError → 400, everything else untouched.
101
+ */
102
+ async function storeCall(call) {
103
+ try {
104
+ return await call();
105
+ }
106
+ catch (error) {
107
+ if (error instanceof PlanValidationError) {
108
+ if (/does not exist/.test(error.message))
109
+ throw new RouteError(404, error.message);
110
+ throw new RouteError(400, error.message);
111
+ }
112
+ if (error instanceof Error && error.message === 'Session not found') {
113
+ throw new RouteError(404, 'Session not found');
114
+ }
115
+ throw error;
116
+ }
117
+ }
118
+ async function loadSessionOr404(ctx, id) {
119
+ const session = await ctx.loadSession(id);
120
+ if (!session)
121
+ throw new RouteError(404, 'Session not found');
122
+ return session;
123
+ }
124
+ function requireItemOr404(session, itemId) {
125
+ if (!session.plan?.items.some((item) => item.id === itemId)) {
126
+ throw new RouteError(404, 'Plan item not found');
127
+ }
128
+ }
129
+ /**
130
+ * Route module factory for PLAN_ROUTES:
131
+ * GET /api/sessions/:id/plan → PlanView (ordered via orderPlan)
132
+ * POST /api/sessions/:id/plan → { item } (body { title, note?, after?, priority? })
133
+ * PATCH /api/sessions/:id/plan/:itemId → { item } (body { title?, note?: string|null, after?, priority?, state?, sessionId?: string|null })
134
+ * DELETE /api/sessions/:id/plan/:itemId → { ok: true }
135
+ * Strict bodies (unknown keys → 400), PlanValidationError → 400 (a store-reported missing
136
+ * item or session → 404), unknown session/item → 404, wrong method → 405.
137
+ * Item ids must match p\d{1,6}; other paths fall through (returns false).
138
+ */
139
+ export function createPlanRoutes(store) {
140
+ return async (ctx) => {
141
+ const planMatch = PLAN_PATTERN.exec(ctx.path);
142
+ if (planMatch) {
143
+ const id = planMatch[1];
144
+ if (ctx.method === 'GET') {
145
+ await loadSessionOr404(ctx, id);
146
+ const plan = await store.get(id);
147
+ ctx.json(200, orderPlan(plan?.items ?? []));
148
+ return true;
149
+ }
150
+ if (ctx.method === 'POST') {
151
+ await loadSessionOr404(ctx, id);
152
+ const input = parseAddBody(await ctx.parseBody());
153
+ const item = await storeCall(() => store.add(id, input));
154
+ ctx.json(200, { item });
155
+ return true;
156
+ }
157
+ throw new RouteError(405, 'Method not allowed');
158
+ }
159
+ const itemMatch = ITEM_PATTERN.exec(ctx.path);
160
+ if (itemMatch) {
161
+ const [id, itemId] = [itemMatch[1], itemMatch[2]];
162
+ if (ctx.method === 'PATCH') {
163
+ const session = await loadSessionOr404(ctx, id);
164
+ requireItemOr404(session, itemId);
165
+ const input = parsePatchBody(await ctx.parseBody());
166
+ const item = await storeCall(() => store.patch(id, itemId, input));
167
+ ctx.json(200, { item });
168
+ return true;
169
+ }
170
+ if (ctx.method === 'DELETE') {
171
+ const session = await loadSessionOr404(ctx, id);
172
+ requireItemOr404(session, itemId);
173
+ await storeCall(() => store.remove(id, itemId));
174
+ ctx.json(200, { ok: true });
175
+ return true;
176
+ }
177
+ throw new RouteError(405, 'Method not allowed');
178
+ }
179
+ return false;
180
+ };
181
+ }
182
+ //# sourceMappingURL=routes-plan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes-plan.js","sourceRoot":"","sources":["../../src/coordination/routes-plan.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ import { type RouteModule, type SessionWorktree, type WorktreeApi, type WorktreeStatus } from './contract-b.js';
2
+ import type { Session } from '../types.js';
3
+ export interface WorktreeRoutesDeps {
4
+ listLive(): Promise<Session[]>;
5
+ listArchived(): Promise<Session[]>;
6
+ worktrees: WorktreeApi;
7
+ markRetained(sessionId: string, reason: string): Promise<void>;
8
+ }
9
+ export interface WorktreeRouteEntry {
10
+ sessionId: string;
11
+ sessionName: string;
12
+ archived: boolean;
13
+ worktree: SessionWorktree;
14
+ status: WorktreeStatus | null;
15
+ error?: string;
16
+ }
17
+ export interface WorktreePruneResponse {
18
+ removed: string[];
19
+ retained: Array<{
20
+ path: string;
21
+ reason: string;
22
+ }>;
23
+ }
24
+ /**
25
+ * Factory for WORKTREE_ROUTES:
26
+ * GET /api/worktrees → { worktrees: WorktreeRouteEntry[] }
27
+ * POST /api/worktrees/prune → { removed: string[]; retained: Array<{ path: string; reason: string }> }
28
+ */
29
+ export declare function createWorktreeRoutes(deps: WorktreeRoutesDeps): RouteModule;
@@ -0,0 +1,138 @@
1
+ import { RouteError, WORKTREE_ROUTES, } from './contract-b.js';
2
+ async function safeMarkRetained(deps, sessionId, reason) {
3
+ try {
4
+ await deps.markRetained(sessionId, reason);
5
+ }
6
+ catch (error) {
7
+ console.warn('[WorktreeRoutes] Failed to mark worktree retained:', error instanceof Error ? error.message : String(error));
8
+ }
9
+ }
10
+ function hasBridgeWorktree(session) {
11
+ return Boolean(session.worktree && session.worktree.createdByBridge);
12
+ }
13
+ /**
14
+ * Deduplicate archived tombstones (newest wins per session id) and exclude
15
+ * sessions or worktree paths that are currently live.
16
+ */
17
+ function selectArchivedWithWorktree(archived, liveIds, livePaths) {
18
+ const lastIndexById = new Map();
19
+ for (let i = 0; i < archived.length; i++) {
20
+ lastIndexById.set(archived[i].id, i);
21
+ }
22
+ const selected = [];
23
+ for (let i = 0; i < archived.length; i++) {
24
+ const session = archived[i];
25
+ if (lastIndexById.get(session.id) !== i)
26
+ continue;
27
+ if (liveIds.has(session.id))
28
+ continue;
29
+ if (!hasBridgeWorktree(session))
30
+ continue;
31
+ if (livePaths.has(session.worktree.path))
32
+ continue;
33
+ selected.push(session);
34
+ }
35
+ return selected;
36
+ }
37
+ function requireNoQuery(ctx) {
38
+ for (const key of ctx.query.keys()) {
39
+ throw new RouteError(400, `Unknown query parameter: ${key}`);
40
+ }
41
+ }
42
+ /**
43
+ * Factory for WORKTREE_ROUTES:
44
+ * GET /api/worktrees → { worktrees: WorktreeRouteEntry[] }
45
+ * POST /api/worktrees/prune → { removed: string[]; retained: Array<{ path: string; reason: string }> }
46
+ */
47
+ export function createWorktreeRoutes(deps) {
48
+ return async function routeWorktrees(ctx) {
49
+ if (ctx.path === WORKTREE_ROUTES.list) {
50
+ if (ctx.method !== 'GET') {
51
+ throw new RouteError(405, 'Method not allowed');
52
+ }
53
+ requireNoQuery(ctx);
54
+ const [live, archived] = await Promise.all([
55
+ deps.listLive(),
56
+ deps.listArchived(),
57
+ ]);
58
+ const liveWithWorktree = live.filter(hasBridgeWorktree);
59
+ const liveIds = new Set(live.map((s) => s.id));
60
+ const livePaths = new Set(liveWithWorktree.map((s) => s.worktree.path));
61
+ const archivedWithWorktree = selectArchivedWithWorktree(archived, liveIds, livePaths);
62
+ const candidates = [
63
+ ...liveWithWorktree.map((session) => ({ session, archived: false })),
64
+ ...archivedWithWorktree.map((session) => ({ session, archived: true })),
65
+ ];
66
+ const worktrees = [];
67
+ for (const { session, archived: isArchived } of candidates) {
68
+ try {
69
+ const status = await deps.worktrees.status(session.worktree);
70
+ worktrees.push({
71
+ sessionId: session.id,
72
+ sessionName: session.name,
73
+ archived: isArchived,
74
+ worktree: session.worktree,
75
+ status,
76
+ });
77
+ }
78
+ catch (error) {
79
+ const message = error instanceof Error ? error.message : String(error);
80
+ worktrees.push({
81
+ sessionId: session.id,
82
+ sessionName: session.name,
83
+ archived: isArchived,
84
+ worktree: session.worktree,
85
+ status: null,
86
+ error: message,
87
+ });
88
+ }
89
+ }
90
+ ctx.json(200, { worktrees });
91
+ return true;
92
+ }
93
+ if (ctx.path === WORKTREE_ROUTES.prune) {
94
+ if (ctx.method !== 'POST') {
95
+ throw new RouteError(405, 'Method not allowed');
96
+ }
97
+ requireNoQuery(ctx);
98
+ const [live, archived] = await Promise.all([
99
+ deps.listLive(),
100
+ deps.listArchived(),
101
+ ]);
102
+ const liveWithWorktree = live.filter(hasBridgeWorktree);
103
+ const liveIds = new Set(live.map((s) => s.id));
104
+ const livePaths = new Set(liveWithWorktree.map((s) => s.worktree.path));
105
+ const archivedWithWorktree = selectArchivedWithWorktree(archived, liveIds, livePaths);
106
+ const removed = [];
107
+ const retained = [];
108
+ for (const session of archivedWithWorktree) {
109
+ const wt = session.worktree;
110
+ try {
111
+ const status = await deps.worktrees.status(wt);
112
+ const decision = deps.worktrees.decide(status);
113
+ if (decision.action === 'remove') {
114
+ await deps.worktrees.remove(wt, {
115
+ force: false,
116
+ deleteBranch: decision.deleteBranch,
117
+ });
118
+ removed.push(wt.path);
119
+ }
120
+ else {
121
+ await safeMarkRetained(deps, session.id, decision.reason);
122
+ retained.push({ path: wt.path, reason: decision.reason });
123
+ }
124
+ }
125
+ catch (error) {
126
+ const reason = error instanceof Error ? error.message : String(error);
127
+ await safeMarkRetained(deps, session.id, reason);
128
+ retained.push({ path: wt.path, reason });
129
+ }
130
+ }
131
+ const result = { removed, retained };
132
+ ctx.json(200, result);
133
+ return true;
134
+ }
135
+ return false;
136
+ };
137
+ }
138
+ //# sourceMappingURL=routes-worktrees.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes-worktrees.js","sourceRoot":"","sources":["../../src/coordination/routes-worktrees.ts"],"names":[],"mappings":"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"}
@@ -1,11 +1,15 @@
1
- import type { Activity } from '../attention-events.js';
1
+ import type { Activity, AttentionItem } from '../attention-events.js';
2
2
  import type { PullRequestState } from '../pull-request-status-protocol.js';
3
3
  import type { Session } from '../types.js';
4
+ import type { AttentionRoute, AttentionRoutingFields } from './contract-b.js';
4
5
  import type { BoardDeps, BoardServiceApi, CardStoreApi, CardStoreDeps, DigestProvider, MailEventSink, TreeEventKind, TreeEventLogApi, TreeEventLogDeps } from './contract.js';
6
+ import { PlanStore } from './plan.js';
5
7
  export interface CoordinationSessionStore {
6
8
  loadSession(id: string): Promise<Session | null>;
7
9
  saveSession(session: Session): Promise<void>;
8
10
  listSessions(): Promise<Session[]>;
11
+ /** Archive tombstones, same source the worktree routes read. */
12
+ listArchived?(): Promise<Session[]>;
9
13
  sessionDir(id: string): string;
10
14
  }
11
15
  export interface CoordinationAttentionStore {
@@ -13,6 +17,8 @@ export interface CoordinationAttentionStore {
13
17
  activity: Activity;
14
18
  openCount: number;
15
19
  }>;
20
+ listRouted?(childId: string): Promise<AttentionItem[]>;
21
+ setRoute?(sessionId: string, eventId: string, fields: AttentionRoutingFields, expectRoute?: AttentionRoute): Promise<AttentionItem | undefined>;
16
22
  }
17
23
  export interface CoordinationPrStatusResult {
18
24
  state: PullRequestState;
@@ -29,19 +35,28 @@ export interface CoordinationDeps {
29
35
  mail: CoordinationMailStore;
30
36
  publishSession: (s: Session) => void;
31
37
  now: () => number;
38
+ onAttentionEscalated?: (sessionId: string, item: AttentionItem) => void;
32
39
  /** Optional factory seams for unit tests using *Api fakes. */
33
40
  createCards?: (deps: CardStoreDeps) => CardStoreApi;
34
41
  createEvents?: (deps: TreeEventLogDeps) => TreeEventLogApi;
35
42
  createBoard?: (deps: BoardDeps) => BoardServiceApi;
43
+ createPlan?: (deps: ConstructorParameters<typeof PlanStore>[0]) => PlanStore;
36
44
  }
37
45
  export interface Coordination {
38
46
  cards: CardStoreApi;
39
47
  events: TreeEventLogApi;
40
48
  board: BoardServiceApi;
49
+ /** Same instance handed to session create (`--plan-item`) and the plan routes. */
50
+ plan: PlanStore;
41
51
  digest: DigestProvider;
42
52
  mailSink: MailEventSink;
43
53
  emit(session: Session, kind: TreeEventKind, summary: string, ref?: string): Promise<void>;
44
- onSessionExit(sessionId: string): Promise<void>;
54
+ /**
55
+ * `snapshot`, when passed, is the session as it existed before the record was
56
+ * deleted. Callers that already hold it (stop, remove/archive) must pass it:
57
+ * reloading after delete returns null and skips escalation.
58
+ */
59
+ onSessionExit(sessionId: string, snapshot?: Session): Promise<void>;
45
60
  onSessionStart(sessionId: string): void;
46
61
  }
47
62
  export declare function rootOf(session: Pick<Session, 'id' | 'parentSessionId'>): string;