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,206 @@
1
+ /**
2
+ * FROZEN CONTRACT — coordination phase B: parent-first attention, manager brief +
3
+ * dashboard overview, optional bridge-owned worktrees. Types/constants/interfaces only.
4
+ * Implementers must not modify this file.
5
+ */
6
+ import type { IncomingMessage, ServerResponse } from 'node:http';
7
+ import type { AttentionAnswer, AttentionPrompt } from '../attention-prompt-protocol.js';
8
+ import type { AttentionItem } from '../attention-events.js';
9
+ import type { Session } from '../types.js';
10
+ /**
11
+ * Routing rules (single hop; the tree is one level deep):
12
+ * - An attention item created by a session S (POST /api/sessions/:id/attention or /ask) gets
13
+ * route 'parent' iff S.parentSessionId is set, the parent session exists, status 'running',
14
+ * shellType !== 'shell', runner reports it running, AND the request did not ask `to: 'human'`.
15
+ * Otherwise route 'human' (today's behaviour).
16
+ * - route 'parent': NO dashboard notice publish and NO push for this item. The parent receives ONE
17
+ * waking mail, type 'escalation', from S, body = PARENT_QUESTION_MAIL(...) below. A tree event
18
+ * kind 'attention' is emitted (phase A already emits it). S's card is untouched (the agent may set it).
19
+ * The item IS visible in the human's inbox (greyed "with parent"), and the human may still answer it.
20
+ * - The parent answers → item answered exactly as a human answer (same delivery: live waiter or
21
+ * mail from "You"; for parent answers the mail sender is the parent session), plus answeredBy.
22
+ * - The parent escalates (or the parent exits/stops/errors while the item is open and route==='parent')
23
+ * → route flips to 'human', escalatedAt set, escalationNote set (parent note, or PARENT_UNAVAILABLE_NOTE),
24
+ * and the normal dashboard notice + push flow fires NOW, as if the item had just been created.
25
+ * - No timers. Only the parent (or parent unavailability) moves an item to the human.
26
+ * - Repeating an identical open request keeps its current route (occurrence count only).
27
+ */
28
+ export type AttentionRoute = 'parent' | 'human';
29
+ export interface AttentionAnsweredBy {
30
+ kind: 'human' | 'session';
31
+ sessionId?: string;
32
+ name?: string;
33
+ }
34
+ /** Fields added to AttentionItem (declared optional on AttentionItem in attention-events.ts). */
35
+ export interface AttentionRoutingFields {
36
+ route?: AttentionRoute;
37
+ routedTo?: string;
38
+ escalatedAt?: string;
39
+ escalationNote?: string;
40
+ answeredBy?: AttentionAnsweredBy;
41
+ }
42
+ export declare const ESCALATION_NOTE_MAX = 500;
43
+ export declare const PARENT_UNAVAILABLE_NOTE = "Parent session is not running; routed to you.";
44
+ /**
45
+ * Exact mail body to the parent (type 'escalation', threadId = item id):
46
+ * `[ftown question id=<eventId> from=<childName> (<childId>)] <title>\n<message>\n` +
47
+ * (options ? `Options: 1) <label> 2) <label> ...\n` : '') +
48
+ * `Answer: ftown attention answer <childId> <eventId> --option <n> | --text "<reply>"\n` +
49
+ * `Escalate to the human: ftown attention escalate <childId> <eventId> --note "<context>"`
50
+ * For multi-question prompts list each question as `Q<i>: <question>` with its options.
51
+ */
52
+ export declare const PARENT_QUESTION_PREFIX = "[ftown question";
53
+ /** AttentionStore additions (bridge/src/attention-store.ts). */
54
+ export interface AttentionRoutingStoreApi {
55
+ /** set route fields on an existing open item; returns updated item or undefined if missing/resolved */
56
+ setRoute(sessionId: string, eventId: string, fields: AttentionRoutingFields): Promise<AttentionItem | undefined>;
57
+ /** open items (not resolved) of childId with route==='parent' */
58
+ listRouted(childId: string): Promise<AttentionItem[]>;
59
+ }
60
+ /** Local API (via dispatchSecure). Caller identity = x-ftown-session-id (local-cli principal). */
61
+ export declare const ATTENTION_ROUTES: {
62
+ /** POST body { answers: AttentionAnswerEntry[] } — caller must be the child's parent (403 otherwise); 409 if resolved; 400 invalid answers. → { event: AttentionItem } */
63
+ readonly answer: "/api/sessions/:id/attention/:eventId/answer";
64
+ /** POST body { note?: string } — caller must be the parent; 409 if route already 'human' or resolved. → { event: AttentionItem } */
65
+ readonly escalate: "/api/sessions/:id/attention/:eventId/escalate";
66
+ /** GET → { items: Array<{ sessionId: string; sessionName: string; event: AttentionItem }> } — open parent-routed items of all children of :id */
67
+ readonly routed: "/api/sessions/:id/attention/routed";
68
+ };
69
+ /** Existing POST /api/sessions/:id/attention and /attention/ask accept a new optional body key `to: 'human'`. */
70
+ /**
71
+ * CLI (new group `ftown attention`, bridge/src/attention-cli.ts):
72
+ * ftown attention list [--json] → routed items for my children (plain: `<child> <eventId> <title>` + options)
73
+ * ftown attention answer <child-id> <event-id> (--option <n|label>... | --text <t> | --answers-json <json>) [--json]
74
+ * ftown attention escalate <child-id> <event-id> [--note <t>] [--json]
75
+ * `ftown sessions attention ... --to human` bypasses the parent.
76
+ * Exit codes: 0 ok, 1 error, 2 usage, 4 forbidden (not the parent), 5 conflict (already resolved/escalated).
77
+ */
78
+ /** Dashboard wire: AttentionItem fields above flow unchanged through existing attention reads. */
79
+ export type AttentionPromptRef = AttentionPrompt;
80
+ export type AttentionAnswerRef = AttentionAnswer;
81
+ export declare const BRIEF_LIMITS: {
82
+ readonly text: 16384;
83
+ readonly note: 280;
84
+ readonly notes: 50;
85
+ };
86
+ export interface BriefNote {
87
+ ts: string;
88
+ text: string;
89
+ }
90
+ /** Stored on Session.brief (session.json) → flows to dashboard via toWireSession. */
91
+ export interface SessionBrief {
92
+ text?: string;
93
+ notes: BriefNote[];
94
+ updatedAt: string;
95
+ }
96
+ export declare const BRIEF_ROUTES: {
97
+ /** GET → { brief: SessionBrief | null }; PUT body { text: string | null } → { brief }; DELETE → { ok: true } */
98
+ readonly brief: "/api/sessions/:id/brief";
99
+ /** POST body { text: string } → { brief } (appends one note) */
100
+ readonly notes: "/api/sessions/:id/brief/notes";
101
+ };
102
+ /**
103
+ * CLI (bridge/src/brief-cli.ts, group `ftown brief`):
104
+ * ftown brief show [--json] [--session-id <id>]
105
+ * ftown brief set (--file <path.md> | --text <t> | --clear) [--session-id <id>]
106
+ * ftown brief note <text...> [--session-id <id>]
107
+ * Validation: control chars except \n\t stripped; text over limit → error exit 1 (no truncation).
108
+ * Every successful write publishes the session to the dashboard.
109
+ *
110
+ * Dashboard overview (UI): for a selected session that has ≥1 child, an "Overview" view showing:
111
+ * the brief (markdown, rendered like existing markdown previews, sanitized), the last 10 notes,
112
+ * and a table of children: name, card chip (state/headline/progress/blocked), activity + idle,
113
+ * PR link + check state, open attention count incl. "with parent" items, worktree branch.
114
+ * Rendered client-side from data the dashboard already has (sessions with card/brief/worktree,
115
+ * PR statuses hook, attention). Each card's age is shown ("updated 4m ago").
116
+ */
117
+ /** Stored on Session.worktree. Only createdByBridge worktrees are ever removed by the bridge. */
118
+ export interface SessionWorktree {
119
+ repoRoot: string;
120
+ path: string;
121
+ branch: string;
122
+ base: string;
123
+ createdByBridge: true;
124
+ retained?: {
125
+ reason: string;
126
+ at: string;
127
+ };
128
+ }
129
+ export interface WorktreeCreateRequest {
130
+ /** directory inside a git repo (the requested workdir, or the parent's workingDir when omitted) */
131
+ fromDir: string;
132
+ /** session name → default branch `ftown/<slug(name)>` and path `<repoRoot>/.claude/worktrees/<slug(name)>` */
133
+ name: string;
134
+ branch?: string;
135
+ base?: string;
136
+ }
137
+ export interface WorktreeStatus {
138
+ path: string;
139
+ branch: string;
140
+ base: string;
141
+ exists: boolean;
142
+ dirty: boolean;
143
+ ahead: number;
144
+ unpushed: number | null;
145
+ mergedIntoBase: boolean;
146
+ }
147
+ export type WorktreeRetireDecision = {
148
+ action: 'remove';
149
+ deleteBranch: boolean;
150
+ } | {
151
+ action: 'retain';
152
+ reason: string;
153
+ };
154
+ /** Implementation: bridge/src/worktrees.ts — all git calls via execFile('git', [...]) with cwd, no shell, timeout 30s. */
155
+ export interface WorktreeApi {
156
+ create(req: WorktreeCreateRequest): Promise<SessionWorktree>;
157
+ status(wt: SessionWorktree): Promise<WorktreeStatus>;
158
+ /** pure: retire iff exists && !dirty && (mergedIntoBase || (unpushed === 0)); else retain with reason */
159
+ decide(status: WorktreeStatus): WorktreeRetireDecision;
160
+ /** git worktree remove (no --force unless force) + optional `git branch -d` (never -D) */
161
+ remove(wt: SessionWorktree, opts: {
162
+ force: boolean;
163
+ deleteBranch: boolean;
164
+ }): Promise<void>;
165
+ }
166
+ export declare class WorktreeError extends Error {
167
+ readonly code: 'not_a_repo' | 'detached_head' | 'bad_branch' | 'exists' | 'git_failed';
168
+ constructor(code: 'not_a_repo' | 'detached_head' | 'bad_branch' | 'exists' | 'git_failed', message: string);
169
+ }
170
+ /**
171
+ * Create: POST /api/sessions body gains `worktree?: true | { branch?: string; base?: string }`.
172
+ * CLI: `ftown sessions create --worktree [--branch <b>] [--base <ref>]` (with --workdir optional).
173
+ * The worktree is created before the session launches; session.workingDir = worktree.path. On WorktreeError → 400 with message, no session created.
174
+ * Remove: DELETE /api/sessions/:id accepts query `worktree=keep|force` (default: auto-decide).
175
+ * CLI: `ftown sessions remove <id> [--keep-worktree | --force-worktree]`; output states removed/retained + reason.
176
+ * Auto: status → decide → remove or set worktree.retained. Errors never block archiving the session.
177
+ * List/prune:
178
+ * GET /api/worktrees → { worktrees: Array<{ sessionId: string; sessionName: string; archived: boolean; worktree: SessionWorktree; status: WorktreeStatus | null; error?: string }> } (status null + error when the git probe failed for that row)
179
+ * POST /api/worktrees/prune → { removed: string[]; retained: Array<{ path: string; reason: string }> } (archived sessions only, same decide rule)
180
+ * CLI (bridge/src/worktrees-cli.ts, group `ftown worktrees`): `list [--json]`, `prune [--json]`.
181
+ */
182
+ export declare const WORKTREE_ROUTES: {
183
+ readonly list: "/api/worktrees";
184
+ readonly prune: "/api/worktrees/prune";
185
+ };
186
+ export interface RouteContext {
187
+ req: IncomingMessage;
188
+ res: ServerResponse;
189
+ method: string;
190
+ /** pathname without query */
191
+ path: string;
192
+ query: URLSearchParams;
193
+ /** x-ftown-session-id of a local-cli caller, else undefined */
194
+ callerSessionId?: string;
195
+ parseBody(): Promise<Record<string, unknown>>;
196
+ json(status: number, body: unknown): void;
197
+ loadSession(id: string): Promise<Session | null>;
198
+ saveSession(session: Session): Promise<void>;
199
+ publishSession(session: Session): void;
200
+ }
201
+ /** Each route module exports `handle(ctx): Promise<boolean>` (true = handled). Throw BadRequestError-like errors as `{ status, message }`. */
202
+ export type RouteModule = (ctx: RouteContext) => Promise<boolean>;
203
+ export declare class RouteError extends Error {
204
+ readonly status: number;
205
+ constructor(status: number, message: string);
206
+ }
@@ -0,0 +1,58 @@
1
+ export const ESCALATION_NOTE_MAX = 500;
2
+ export const PARENT_UNAVAILABLE_NOTE = 'Parent session is not running; routed to you.';
3
+ /**
4
+ * Exact mail body to the parent (type 'escalation', threadId = item id):
5
+ * `[ftown question id=<eventId> from=<childName> (<childId>)] <title>\n<message>\n` +
6
+ * (options ? `Options: 1) <label> 2) <label> ...\n` : '') +
7
+ * `Answer: ftown attention answer <childId> <eventId> --option <n> | --text "<reply>"\n` +
8
+ * `Escalate to the human: ftown attention escalate <childId> <eventId> --note "<context>"`
9
+ * For multi-question prompts list each question as `Q<i>: <question>` with its options.
10
+ */
11
+ export const PARENT_QUESTION_PREFIX = '[ftown question';
12
+ /** Local API (via dispatchSecure). Caller identity = x-ftown-session-id (local-cli principal). */
13
+ export const ATTENTION_ROUTES = {
14
+ /** POST body { answers: AttentionAnswerEntry[] } — caller must be the child's parent (403 otherwise); 409 if resolved; 400 invalid answers. → { event: AttentionItem } */
15
+ answer: '/api/sessions/:id/attention/:eventId/answer',
16
+ /** POST body { note?: string } — caller must be the parent; 409 if route already 'human' or resolved. → { event: AttentionItem } */
17
+ escalate: '/api/sessions/:id/attention/:eventId/escalate',
18
+ /** GET → { items: Array<{ sessionId: string; sessionName: string; event: AttentionItem }> } — open parent-routed items of all children of :id */
19
+ routed: '/api/sessions/:id/attention/routed',
20
+ };
21
+ // ───────────────────────── C: manager brief + dashboard overview ─────────────────────────
22
+ export const BRIEF_LIMITS = { text: 16_384, note: 280, notes: 50 };
23
+ export const BRIEF_ROUTES = {
24
+ /** GET → { brief: SessionBrief | null }; PUT body { text: string | null } → { brief }; DELETE → { ok: true } */
25
+ brief: '/api/sessions/:id/brief',
26
+ /** POST body { text: string } → { brief } (appends one note) */
27
+ notes: '/api/sessions/:id/brief/notes',
28
+ };
29
+ export class WorktreeError extends Error {
30
+ code;
31
+ constructor(code, message) {
32
+ super(message);
33
+ this.code = code;
34
+ this.name = 'WorktreeError';
35
+ }
36
+ }
37
+ /**
38
+ * Create: POST /api/sessions body gains `worktree?: true | { branch?: string; base?: string }`.
39
+ * CLI: `ftown sessions create --worktree [--branch <b>] [--base <ref>]` (with --workdir optional).
40
+ * The worktree is created before the session launches; session.workingDir = worktree.path. On WorktreeError → 400 with message, no session created.
41
+ * Remove: DELETE /api/sessions/:id accepts query `worktree=keep|force` (default: auto-decide).
42
+ * CLI: `ftown sessions remove <id> [--keep-worktree | --force-worktree]`; output states removed/retained + reason.
43
+ * Auto: status → decide → remove or set worktree.retained. Errors never block archiving the session.
44
+ * List/prune:
45
+ * GET /api/worktrees → { worktrees: Array<{ sessionId: string; sessionName: string; archived: boolean; worktree: SessionWorktree; status: WorktreeStatus | null; error?: string }> } (status null + error when the git probe failed for that row)
46
+ * POST /api/worktrees/prune → { removed: string[]; retained: Array<{ path: string; reason: string }> } (archived sessions only, same decide rule)
47
+ * CLI (bridge/src/worktrees-cli.ts, group `ftown worktrees`): `list [--json]`, `prune [--json]`.
48
+ */
49
+ export const WORKTREE_ROUTES = { list: '/api/worktrees', prune: '/api/worktrees/prune' };
50
+ export class RouteError extends Error {
51
+ status;
52
+ constructor(status, message) {
53
+ super(message);
54
+ this.status = status;
55
+ this.name = 'RouteError';
56
+ }
57
+ }
58
+ //# sourceMappingURL=contract-b.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract-b.js","sourceRoot":"","sources":["../../src/coordination/contract-b.ts"],"names":[],"mappings":"AA2CA,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAG,CAAC;AACvC,MAAM,CAAC,MAAM,uBAAuB,GAAG,+CAA+C,CAAC;AAEvF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,iBAAiB,CAAC;AAUxD,kGAAkG;AAClG,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,0KAA0K;IAC1K,MAAM,EAAE,6CAA6C;IACrD,oIAAoI;IACpI,QAAQ,EAAE,+CAA+C;IACzD,iJAAiJ;IACjJ,MAAM,EAAE,oCAAoC;CACpC,CAAC;AAgBX,4FAA4F;AAE5F,MAAM,CAAC,MAAM,YAAY,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,EAAW,CAAC;AAU5E,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,gHAAgH;IAChH,KAAK,EAAE,yBAAyB;IAChC,gEAAgE;IAChE,KAAK,EAAE,+BAA+B;CAC9B,CAAC;AA8DX,MAAM,OAAO,aAAc,SAAQ,KAAK;IACV;IAA5B,YAA4B,IAA6E,EAAE,OAAe;QACxH,KAAK,CAAC,OAAO,CAAC,CAAC;QADW,SAAI,GAAJ,IAAI,CAAyE;QACvF,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;IAC9C,CAAC;CACF;AAED;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,EAAE,IAAI,EAAE,gBAAgB,EAAE,KAAK,EAAE,sBAAsB,EAAW,CAAC;AAsBlG,MAAM,OAAO,UAAW,SAAQ,KAAK;IACP;IAA5B,YAA4B,MAAc,EAAE,OAAe;QAAI,KAAK,CAAC,OAAO,CAAC,CAAC;QAAlD,WAAM,GAAN,MAAM,CAAQ;QAAqC,IAAI,CAAC,IAAI,GAAG,YAAY,CAAC;IAAC,CAAC;CAC3G"}
@@ -0,0 +1,117 @@
1
+ /**
2
+ * FROZEN CONTRACT — coordination phase C: plan list (todo with precedence), retired
3
+ * history on the board, removed/plan tree events, dashboard board snapshot RPC, board v3
4
+ * Overview. Types/constants/interfaces only. Implementers must not modify this file.
5
+ */
6
+ import type { Board, SessionCard, TreeEvent } from './contract.js';
7
+ import type { AttentionItem } from '../attention-events.js';
8
+ export declare const PLAN_LIMITS: {
9
+ readonly items: 200;
10
+ readonly title: 140;
11
+ readonly note: 500;
12
+ readonly after: 20;
13
+ };
14
+ export declare const PLAN_STATES: readonly ["todo", "started", "done", "dropped"];
15
+ export type PlanState = typeof PLAN_STATES[number];
16
+ export type PlanPriority = 1 | 2 | 3;
17
+ export interface PlanItem {
18
+ id: string;
19
+ title: string;
20
+ note?: string;
21
+ after: string[];
22
+ priority: PlanPriority;
23
+ state: PlanState;
24
+ sessionId?: string;
25
+ createdAt: string;
26
+ updatedAt: string;
27
+ }
28
+ /** Stored on the PARENT session: Session.plan (session.json) → flows to dashboard via toWireSession. */
29
+ export interface SessionPlan {
30
+ items: PlanItem[];
31
+ seq: number;
32
+ }
33
+ /**
34
+ * Derived ordering (pure helper `orderPlan(items)` in bridge/src/coordination/plan.ts, also re-implemented
35
+ * identically in the UI — see PLAN_ORDER_RULE):
36
+ * ready = state 'todo' && every `after` item is 'done' or 'dropped'
37
+ * waiting = state 'todo' && not ready
38
+ * Order: ready (priority asc, createdAt asc), then waiting (same), then started (updatedAt asc),
39
+ * then done (updatedAt desc), then dropped (updatedAt desc).
40
+ */
41
+ export declare const PLAN_ORDER_RULE: "ready>waiting>started>done>dropped; within ready/waiting: priority asc, createdAt asc";
42
+ export interface PlanView {
43
+ items: PlanItem[];
44
+ ready: string[];
45
+ }
46
+ /** Routes (via dispatchSecure; :id = parent session). Caller: local-cli principal; any session may edit the plan of :id (owner-only enforcement is out of scope). */
47
+ export declare const PLAN_ROUTES: {
48
+ /** GET → PlanView; POST body { title, note?, after?, priority? } → { item } */
49
+ readonly plan: "/api/sessions/:id/plan";
50
+ /** PATCH body { title?, note?: string|null, after?, priority?, state?, sessionId?: string|null } → { item }; DELETE → { ok: true } (removes id from others' `after`) */
51
+ readonly item: "/api/sessions/:id/plan/:itemId";
52
+ };
53
+ export declare class PlanValidationError extends Error {
54
+ constructor(message: string);
55
+ }
56
+ /**
57
+ * Automatic transitions (bridge wiring):
58
+ * - `ftown sessions create --parent --plan-item <id>`: after the child exists, item.state = 'started', item.sessionId = child.id.
59
+ * Unknown/non-todo item → create fails 400 BEFORE the session is created.
60
+ * - When a child's declared card becomes 'done' and an item of its parent has sessionId === child.id and state 'started' → 'done'.
61
+ * 'failed' card → item back to 'todo' with sessionId cleared? NO: leave 'started' (manager decides).
62
+ * - Every plan change emits ONE tree event kind 'plan' on the parent's log with summary
63
+ * `<id> <state>: <title>` ; when items become ready because of it, append ` → ready: <id>, <id>`.
64
+ *
65
+ * CLI (bridge/src/plan-cli.ts, group `ftown plan`, default session = FTOWN_SESSION_ID = the manager):
66
+ * ftown plan [--json] [--session-id <id>] list in order: `<mark> <id> [P<prio>] <title> (after p1,p2 | → <child name>)`
67
+ * marks: `→` ready, `·` waiting, `▸` started, `✓` done, `✗` dropped
68
+ * ftown plan add <title...> [--note <t>] [--after <id,id>] [--priority 1|2|3]
69
+ * ftown plan start <id> --session <child-id>
70
+ * ftown plan done <id> | drop <id> | todo <id>
71
+ * ftown plan edit <id> [--title <t>] [--note <t>|--no-note] [--after <ids>|--no-after] [--priority n]
72
+ * ftown plan rm <id>
73
+ * ftown sessions create --parent --plan-item <id> ... (in sessions-commands.ts)
74
+ * Exit codes: 0 ok, 1 error, 2 usage.
75
+ */
76
+ /** Added to TREE_EVENT_KINDS in contract.ts (phase C edit): 'plan' and 'removed'. */
77
+ export declare const PHASE_C_EVENT_KINDS: readonly ["plan", "removed"];
78
+ /** 'removed': emitted when a child session is removed/archived while not already exited; summary `removed`. */
79
+ /** SessionCard gains (in contract.ts, phase C edit): `archived?: boolean`. Board gains `plan?: PlanView`. */
80
+ export interface BoardPhaseCFields {
81
+ archived?: boolean;
82
+ }
83
+ /**
84
+ * BoardService (phase C):
85
+ * - BoardDeps gains `listArchived(): Promise<Session[]>` (archive store records).
86
+ * - board({ all: true }) includes archived children (archived: true, derived.status as recorded, activity 'unknown'),
87
+ * ordered after live children by archived time desc, capped at 50 most recent.
88
+ * - Board.plan = the parent's PlanView when the root has a plan (always included).
89
+ * - Plain `ftown board`: after child rows, a `planned` section: up to 5 ready items `→ <id> <title>` then `+N more`.
90
+ *
91
+ * `ftown board --wait --until any-done|any-blocked|any-ready` (phase C fix): the condition must hold for a child
92
+ * whose tree events arrived AFTER the starting cursor (any-done: a changed child is now done, etc.).
93
+ * `all-done` stays global. `any-change` unchanged.
94
+ */
95
+ /** command-rpc command `get_session_board`, payload { bridgeId, sessionId } → BoardSnapshot. Read-only. */
96
+ export interface BoardSnapshot {
97
+ board: Board;
98
+ events: TreeEvent[];
99
+ attention: Array<{
100
+ sessionId: string;
101
+ event: AttentionItem;
102
+ }>;
103
+ }
104
+ export declare const BOARD_SNAPSHOT_EVENTS = 50;
105
+ /**
106
+ * UI (board v3 Overview, see .ftown-previews/board-v3-notes.md in the phase B worktree — copied to
107
+ * docs/design/board-v3.md in this phase):
108
+ * - hook `useSessionBoard(bridgeId, sessionId, enabled)` calls get_session_board on mount, on any session update
109
+ * for that tree, and every 15 s while the Overview is visible; never while hidden.
110
+ * - SessionOverview renders: manager line; "needs attention · n" (failed | blocked | escalated question; owner
111
+ * `you` or manager name); "active · n"; folds: planned (PlanView order), done (incl. archived/retired),
112
+ * events, brief. Row expand shows details (state/progress/age/stale, blocked, question, next, activity +
113
+ * idle duration, pr, worktree, mail, preview, agent, that session's last 3 events).
114
+ * - Idle duration: the UI records the timestamp when a session's activity becomes 'idle' (from hook events)
115
+ * and shows `idle <n>m`.
116
+ */
117
+ export type OverviewChild = SessionCard;
@@ -0,0 +1,47 @@
1
+ // ───────────────────────── Plan (todo list with precedence) ─────────────────────────
2
+ export const PLAN_LIMITS = { items: 200, title: 140, note: 500, after: 20 };
3
+ export const PLAN_STATES = ['todo', 'started', 'done', 'dropped'];
4
+ /**
5
+ * Derived ordering (pure helper `orderPlan(items)` in bridge/src/coordination/plan.ts, also re-implemented
6
+ * identically in the UI — see PLAN_ORDER_RULE):
7
+ * ready = state 'todo' && every `after` item is 'done' or 'dropped'
8
+ * waiting = state 'todo' && not ready
9
+ * Order: ready (priority asc, createdAt asc), then waiting (same), then started (updatedAt asc),
10
+ * then done (updatedAt desc), then dropped (updatedAt desc).
11
+ */
12
+ export const PLAN_ORDER_RULE = 'ready>waiting>started>done>dropped; within ready/waiting: priority asc, createdAt asc';
13
+ /** Routes (via dispatchSecure; :id = parent session). Caller: local-cli principal; any session may edit the plan of :id (owner-only enforcement is out of scope). */
14
+ export const PLAN_ROUTES = {
15
+ /** GET → PlanView; POST body { title, note?, after?, priority? } → { item } */
16
+ plan: '/api/sessions/:id/plan',
17
+ /** PATCH body { title?, note?: string|null, after?, priority?, state?, sessionId?: string|null } → { item }; DELETE → { ok: true } (removes id from others' `after`) */
18
+ item: '/api/sessions/:id/plan/:itemId',
19
+ };
20
+ export class PlanValidationError extends Error {
21
+ constructor(message) { super(message); this.name = 'PlanValidationError'; }
22
+ }
23
+ /**
24
+ * Automatic transitions (bridge wiring):
25
+ * - `ftown sessions create --parent --plan-item <id>`: after the child exists, item.state = 'started', item.sessionId = child.id.
26
+ * Unknown/non-todo item → create fails 400 BEFORE the session is created.
27
+ * - When a child's declared card becomes 'done' and an item of its parent has sessionId === child.id and state 'started' → 'done'.
28
+ * 'failed' card → item back to 'todo' with sessionId cleared? NO: leave 'started' (manager decides).
29
+ * - Every plan change emits ONE tree event kind 'plan' on the parent's log with summary
30
+ * `<id> <state>: <title>` ; when items become ready because of it, append ` → ready: <id>, <id>`.
31
+ *
32
+ * CLI (bridge/src/plan-cli.ts, group `ftown plan`, default session = FTOWN_SESSION_ID = the manager):
33
+ * ftown plan [--json] [--session-id <id>] list in order: `<mark> <id> [P<prio>] <title> (after p1,p2 | → <child name>)`
34
+ * marks: `→` ready, `·` waiting, `▸` started, `✓` done, `✗` dropped
35
+ * ftown plan add <title...> [--note <t>] [--after <id,id>] [--priority 1|2|3]
36
+ * ftown plan start <id> --session <child-id>
37
+ * ftown plan done <id> | drop <id> | todo <id>
38
+ * ftown plan edit <id> [--title <t>] [--note <t>|--no-note] [--after <ids>|--no-after] [--priority n]
39
+ * ftown plan rm <id>
40
+ * ftown sessions create --parent --plan-item <id> ... (in sessions-commands.ts)
41
+ * Exit codes: 0 ok, 1 error, 2 usage.
42
+ */
43
+ // ───────────────────────── Tree events: new kinds ─────────────────────────
44
+ /** Added to TREE_EVENT_KINDS in contract.ts (phase C edit): 'plan' and 'removed'. */
45
+ export const PHASE_C_EVENT_KINDS = ['plan', 'removed'];
46
+ export const BOARD_SNAPSHOT_EVENTS = 50;
47
+ //# sourceMappingURL=contract-c.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract-c.js","sourceRoot":"","sources":["../../src/coordination/contract-c.ts"],"names":[],"mappings":"AAQA,uFAAuF;AAEvF,MAAM,CAAC,MAAM,WAAW,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,EAAW,CAAC;AACrF,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,CAAU,CAAC;AAmB3E;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,uFAAgG,CAAC;AAIhI,qKAAqK;AACrK,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,+EAA+E;IAC/E,IAAI,EAAE,wBAAwB;IAC9B,wKAAwK;IACxK,IAAI,EAAE,gCAAgC;CAC9B,CAAC;AAEX,MAAM,OAAO,mBAAoB,SAAQ,KAAK;IAC5C,YAAY,OAAe,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC,CAAC,CAAC;CACpF;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,6EAA6E;AAE7E,qFAAqF;AACrF,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,MAAM,EAAE,SAAS,CAAU,CAAC;AA6BhE,MAAM,CAAC,MAAM,qBAAqB,GAAG,EAAE,CAAC"}
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import type { Activity } from '../attention-events.js';
7
7
  import type { PullRequestState } from '../pull-request-status-protocol.js';
8
+ import type { PlanView } from './contract-c.js';
8
9
  import type { MailMessage, Session, SessionStatus } from '../types.js';
9
10
  export declare const CARD_STATES: readonly ["working", "blocked", "waiting_review", "done", "failed"];
10
11
  export type CardState = typeof CARD_STATES[number];
@@ -70,6 +71,8 @@ export interface CardDerived {
70
71
  model?: string;
71
72
  }
72
73
  export interface SessionCard {
74
+ /** phase C: true for retired (archived) children included by board(all) */
75
+ archived?: boolean;
73
76
  sessionId: string;
74
77
  name: string;
75
78
  parentSessionId?: string;
@@ -99,7 +102,7 @@ export declare const TREE_EVENTS_RETAIN = 5000;
99
102
  export declare const TREE_EVENTS_COMPACT_AT = 6000;
100
103
  export declare const TREE_EVENTS_MAX_WAIT_MS = 300000;
101
104
  export declare const TREE_EVENT_SUMMARY_MAX = 300;
102
- export declare const TREE_EVENT_KINDS: readonly ["status", "attention", "pr", "preview", "exit", "idle", "result", "mail"];
105
+ export declare const TREE_EVENT_KINDS: readonly ["status", "attention", "pr", "preview", "exit", "idle", "result", "mail", "plan", "removed"];
103
106
  export type TreeEventKind = typeof TREE_EVENT_KINDS[number];
104
107
  export interface TreeEvent {
105
108
  seq: number;
@@ -135,6 +138,8 @@ export interface TreeEventLogApi {
135
138
  }
136
139
  export type BoardRollupKey = CardState | 'unset';
137
140
  export interface Board {
141
+ /** phase C: the root's plan, ordered */
142
+ plan?: PlanView;
138
143
  rootId: string;
139
144
  /** tree event head at render time; pass back as `since` */
140
145
  cursor: number;
@@ -153,6 +158,8 @@ export interface BoardQuery {
153
158
  all?: boolean;
154
159
  }
155
160
  export interface BoardDeps {
161
+ /** phase C: archived session records */
162
+ listArchived?(): Promise<Session[]>;
156
163
  loadSession(id: string): Promise<Session | null>;
157
164
  listSessions(): Promise<Session[]>;
158
165
  attention(id: string): Promise<{
@@ -19,7 +19,7 @@ export const TREE_EVENTS_RETAIN = 5_000; // keep newest N on compaction
19
19
  export const TREE_EVENTS_COMPACT_AT = 6_000;
20
20
  export const TREE_EVENTS_MAX_WAIT_MS = 300_000;
21
21
  export const TREE_EVENT_SUMMARY_MAX = 300;
22
- export const TREE_EVENT_KINDS = ['status', 'attention', 'pr', 'preview', 'exit', 'idle', 'result', 'mail'];
22
+ export const TREE_EVENT_KINDS = ['status', 'attention', 'pr', 'preview', 'exit', 'idle', 'result', 'mail', 'plan', 'removed'];
23
23
  /**
24
24
  * Digest line format (exact):
25
25
  * `[ftown] since HH:MM: <part> | <part> | ...`
@@ -1 +1 @@
1
- {"version":3,"file":"contract.js","sourceRoot":"","sources":["../../src/coordination/contract.ts"],"names":[],"mappings":"AASA,2FAA2F;AAE3F,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,SAAS,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,EAAE,QAAQ,CAAU,CAAC;AAG/F,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,UAAU,CAAU,CAAC;AAGpF,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,QAAQ,EAAE,GAAG;IACb,IAAI,EAAE,GAAG;IACT,WAAW,EAAE,GAAG;IAChB,yDAAyD;IACzD,aAAa,EAAE,MAAM;CACb,CAAC;AA4BX,MAAM,OAAO,mBAAoB,SAAQ,KAAK;IAC5C,YAAY,OAAe,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC,CAAC,CAAC;CACpF;AA8BD,MAAM,CAAC,MAAM,aAAa,GAAG,EAAE,GAAG,MAAM,CAAC;AAoBzC,qEAAqE;AAErE,uFAAuF;AACvF,MAAM,CAAC,MAAM,gBAAgB,GAAG,mBAAmB,CAAC,CAAC,wBAAwB;AAC7E,MAAM,CAAC,MAAM,kBAAkB,GAAG,KAAK,CAAC,CAAG,8BAA8B;AACzE,MAAM,CAAC,MAAM,sBAAsB,GAAG,KAAK,CAAC;AAC5C,MAAM,CAAC,MAAM,uBAAuB,GAAG,OAAO,CAAC;AAC/C,MAAM,CAAC,MAAM,sBAAsB,GAAG,GAAG,CAAC;AAE1C,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,QAAQ,EAAE,WAAW,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAU,CAAC;AAwEpH;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,GAAG,CAAC;AAE9B;;GAEG;AAEH,6EAA6E;AAE7E,iEAAiE;AACjE,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,EAAE,KAAK,CAAU,CAAC;AAEzF,0FAA0F;AAC1F,MAAM,CAAC,MAAM,qBAAqB,GAA4B,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;AAC/E,gGAAgG;AAChG,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC;AAsBnC,+GAA+G;AAE/G,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,8GAA8G;IAC9G,IAAI,EAAE,wBAAwB;IAC9B,2CAA2C;IAC3C,KAAK,EAAE,yBAAyB;IAChC,kFAAkF;IAClF,MAAM,EAAE,0BAA0B;CAC1B,CAAC;AAEX,iGAAiG;AACjG;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,UAAU,EAAE,WAAW,CAAU,CAAC"}
1
+ {"version":3,"file":"contract.js","sourceRoot":"","sources":["../../src/coordination/contract.ts"],"names":[],"mappings":"AAUA,2FAA2F;AAE3F,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,SAAS,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,EAAE,QAAQ,CAAU,CAAC;AAG/F,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,UAAU,CAAU,CAAC;AAGpF,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,QAAQ,EAAE,GAAG;IACb,IAAI,EAAE,GAAG;IACT,WAAW,EAAE,GAAG;IAChB,yDAAyD;IACzD,aAAa,EAAE,MAAM;CACb,CAAC;AA4BX,MAAM,OAAO,mBAAoB,SAAQ,KAAK;IAC5C,YAAY,OAAe,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC,CAAC,CAAC;CACpF;AAgCD,MAAM,CAAC,MAAM,aAAa,GAAG,EAAE,GAAG,MAAM,CAAC;AAoBzC,qEAAqE;AAErE,uFAAuF;AACvF,MAAM,CAAC,MAAM,gBAAgB,GAAG,mBAAmB,CAAC,CAAC,wBAAwB;AAC7E,MAAM,CAAC,MAAM,kBAAkB,GAAG,KAAK,CAAC,CAAG,8BAA8B;AACzE,MAAM,CAAC,MAAM,sBAAsB,GAAG,KAAK,CAAC;AAC5C,MAAM,CAAC,MAAM,uBAAuB,GAAG,OAAO,CAAC;AAC/C,MAAM,CAAC,MAAM,sBAAsB,GAAG,GAAG,CAAC;AAE1C,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,QAAQ,EAAE,WAAW,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,CAAU,CAAC;AA4EvI;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,GAAG,CAAC;AAE9B;;GAEG;AAEH,6EAA6E;AAE7E,iEAAiE;AACjE,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,EAAE,KAAK,CAAU,CAAC;AAEzF,0FAA0F;AAC1F,MAAM,CAAC,MAAM,qBAAqB,GAA4B,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;AAC/E,gGAAgG;AAChG,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC;AAsBnC,+GAA+G;AAE/G,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,8GAA8G;IAC9G,IAAI,EAAE,wBAAwB;IAC9B,2CAA2C;IAC3C,KAAK,EAAE,yBAAyB;IAChC,kFAAkF;IAClF,MAAM,EAAE,0BAA0B;CAC1B,CAAC;AAEX,iGAAiG;AACjG;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,UAAU,EAAE,WAAW,CAAU,CAAC"}
@@ -1,8 +1,12 @@
1
1
  import { CARD_STATES, } from './contract.js';
2
2
  const ROLLUP_ORDER = [...CARD_STATES, 'unset'];
3
+ const BLOCKED_TEXT_MAX = 60;
3
4
  function pad2(n) {
4
5
  return String(n).padStart(2, '0');
5
6
  }
7
+ function truncate(text, max) {
8
+ return [...text].slice(0, max).join('');
9
+ }
6
10
  export function formatLocalHHMM(iso) {
7
11
  const d = new Date(iso);
8
12
  return `${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
@@ -22,6 +26,15 @@ export function formatBoardRow(card, nowMs) {
22
26
  }
23
27
  const headline = card.declared?.headline || '-';
24
28
  let suffix = '';
29
+ const progress = card.declared?.progress;
30
+ if (progress && state !== 'done') {
31
+ suffix += ` ${progress.done}/${progress.total}`;
32
+ }
33
+ const blockedOn = card.declared?.blockedOn;
34
+ if (blockedOn) {
35
+ const ref = blockedOn.ref ? `:${blockedOn.ref}` : '';
36
+ suffix += ` blocked on ${blockedOn.kind}${ref}: ${truncate(blockedOn.text, BLOCKED_TEXT_MAX)}`;
37
+ }
25
38
  if (card.derived.pr) {
26
39
  suffix += ` PR:${card.derived.pr.state}`;
27
40
  }
@@ -34,24 +47,50 @@ export function formatBoardRow(card, nowMs) {
34
47
  if (card.stale) {
35
48
  suffix += ' (stale)';
36
49
  }
50
+ if (card.archived) {
51
+ suffix += ' (retired)';
52
+ }
37
53
  return `${card.name} ${state} ${activity}${idlePart} ${headline}${suffix}`;
38
54
  }
55
+ const PLANNED_READY_LIMIT = 5;
56
+ function formatPlannedLines(board) {
57
+ const ready = board.plan?.ready ?? [];
58
+ if (ready.length === 0)
59
+ return [];
60
+ const titleById = new Map((board.plan?.items ?? []).map((item) => [item.id, item.title]));
61
+ const shown = ready.slice(0, PLANNED_READY_LIMIT);
62
+ const lines = ['planned'];
63
+ for (const id of shown) {
64
+ const title = titleById.get(id) ?? '';
65
+ lines.push(title === '' ? `→ ${id}` : `→ ${id} ${title}`);
66
+ }
67
+ const more = ready.length - shown.length;
68
+ if (more > 0)
69
+ lines.push(`+${more} more`);
70
+ return lines;
71
+ }
39
72
  export function formatBoard(board, nowMs) {
73
+ const lines = [];
40
74
  if (board.cards.length === 0) {
41
- return 'no child sessions';
42
- }
43
- const rows = board.cards.map((card) => formatBoardRow(card, nowMs));
44
- const rollupParts = [];
45
- for (const key of ROLLUP_ORDER) {
46
- const count = board.rollup[key] ?? 0;
47
- if (count > 0) {
48
- rollupParts.push(`${count} ${key}`);
49
- }
75
+ lines.push('no child sessions');
50
76
  }
51
- if (rollupParts.length > 0) {
52
- rows.push(rollupParts.join(', '));
77
+ else {
78
+ for (const card of board.cards) {
79
+ lines.push(formatBoardRow(card, nowMs));
80
+ }
81
+ const rollupParts = [];
82
+ for (const key of ROLLUP_ORDER) {
83
+ const count = board.rollup[key] ?? 0;
84
+ if (count > 0) {
85
+ rollupParts.push(`${count} ${key}`);
86
+ }
87
+ }
88
+ if (rollupParts.length > 0) {
89
+ lines.push(rollupParts.join(', '));
90
+ }
53
91
  }
54
- return rows.join('\n');
92
+ lines.push(...formatPlannedLines(board));
93
+ return lines.join('\n');
55
94
  }
56
95
  export function formatEvent(e) {
57
96
  const hhmm = formatLocalHHMM(e.ts);
@@ -1 +1 @@
1
- {"version":3,"file":"format.js","sourceRoot":"","sources":["../../src/coordination/format.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"format.js","sourceRoot":"","sources":["../../src/coordination/format.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,GAKZ,MAAM,eAAe,CAAC;AAEvB,MAAM,YAAY,GAA8B,CAAC,GAAG,WAAW,EAAE,OAAO,CAAC,CAAC;AAE1E,MAAM,gBAAgB,GAAG,EAAE,CAAC;AAE5B,SAAS,IAAI,CAAC,CAAS;IACrB,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AACpC,CAAC;AAED,SAAS,QAAQ,CAAC,IAAY,EAAE,GAAW;IACzC,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AAC1C,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,GAAW;IACzC,MAAM,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC;IACxB,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC,EAAE,CAAC;AACzD,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,IAAiB,EAAE,KAAa;IAC7D,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,EAAE,KAAK,IAAI,GAAG,CAAC;IAC1C,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC;IAEvC,IAAI,QAAQ,GAAG,EAAE,CAAC;IAClB,IAAI,QAAQ,KAAK,MAAM,EAAE,CAAC;QACxB,MAAM,WAAW,GACf,IAAI,CAAC,OAAO,CAAC,SAAS,KAAK,SAAS;YAClC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC;YACpC,CAAC,CAAC,GAAG,CAAC;QACV,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC;YACvC,CAAC,CAAC,CAAC;YACH,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,GAAG,WAAW,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC;QAC5D,QAAQ,GAAG,SAAS,OAAO,GAAG,CAAC;IACjC,CAAC;IAED,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,EAAE,QAAQ,IAAI,GAAG,CAAC;IAEhD,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC;IACzC,IAAI,QAAQ,IAAI,KAAK,KAAK,MAAM,EAAE,CAAC;QACjC,MAAM,IAAI,KAAK,QAAQ,CAAC,IAAI,IAAI,QAAQ,CAAC,KAAK,EAAE,CAAC;IACnD,CAAC;IACD,MAAM,SAAS,GAAG,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAC;IAC3C,IAAI,SAAS,EAAE,CAAC;QACd,MAAM,GAAG,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,SAAS,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACrD,MAAM,IAAI,gBAAgB,SAAS,CAAC,IAAI,GAAG,GAAG,KAAK,QAAQ,CAAC,SAAS,CAAC,IAAI,EAAE,gBAAgB,CAAC,EAAE,CAAC;IAClG,CAAC;IACD,IAAI,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC;QACpB,MAAM,IAAI,QAAQ,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC;IAC5C,CAAC;IACD,IAAI,IAAI,CAAC,OAAO,CAAC,aAAa,GAAG,CAAC,EAAE,CAAC;QACnC,MAAM,IAAI,MAAM,IAAI,CAAC,OAAO,CAAC,aAAa,EAAE,CAAC;IAC/C,CAAC;IACD,IAAI,IAAI,CAAC,OAAO,CAAC,UAAU,GAAG,CAAC,EAAE,CAAC;QAChC,MAAM,IAAI,MAAM,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC;IAC5C,CAAC;IACD,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,WAAW,CAAC;IACxB,CAAC;IACD,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;QAClB,MAAM,IAAI,aAAa,CAAC;IAC1B,CAAC;IAED,OAAO,GAAG,IAAI,CAAC,IAAI,IAAI,KAAK,IAAI,QAAQ,GAAG,QAAQ,KAAK,QAAQ,GAAG,MAAM,EAAE,CAAC;AAC9E,CAAC;AAED,MAAM,mBAAmB,GAAG,CAAC,CAAC;AAE9B,SAAS,kBAAkB,CAAC,KAAY;IACtC,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC;IACtC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAClC,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC1F,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,mBAAmB,CAAC,CAAC;IAClD,MAAM,KAAK,GAAG,CAAC,SAAS,CAAC,CAAC;IAC1B,KAAK,MAAM,EAAE,IAAI,KAAK,EAAE,CAAC;QACvB,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC;QACtC,KAAK,CAAC,IAAI,CAAC,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,EAAE,IAAI,KAAK,EAAE,CAAC,CAAC;IAC5D,CAAC;IACD,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;IACzC,IAAI,IAAI,GAAG,CAAC;QAAE,KAAK,CAAC,IAAI,CAAC,IAAI,IAAI,OAAO,CAAC,CAAC;IAC1C,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,KAAY,EAAE,KAAa;IACrD,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC7B,KAAK,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;IAClC,CAAC;SAAM,CAAC;QACN,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;YAC/B,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;QAC1C,CAAC;QACD,MAAM,WAAW,GAAa,EAAE,CAAC;QACjC,KAAK,MAAM,GAAG,IAAI,YAAY,EAAE,CAAC;YAC/B,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACrC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;gBACd,WAAW,CAAC,IAAI,CAAC,GAAG,KAAK,IAAI,GAAG,EAAE,CAAC,CAAC;YACtC,CAAC;QACH,CAAC;QACD,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC3B,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QACrC,CAAC;IACH,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAC,CAAC;IACzC,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,CAAY;IACtC,MAAM,IAAI,GAAG,eAAe,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACnC,OAAO,IAAI,CAAC,CAAC,GAAG,IAAI,IAAI,IAAI,CAAC,CAAC,WAAW,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC;AACtE,CAAC"}