ftown 0.25.3 → 0.26.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.
- package/dist/coordination/board.d.ts +12 -0
- package/dist/coordination/board.js +240 -0
- package/dist/coordination/board.js.map +1 -0
- package/dist/coordination/card-store.d.ts +9 -0
- package/dist/coordination/card-store.js +188 -0
- package/dist/coordination/card-store.js.map +1 -0
- package/dist/coordination/contract.d.ts +236 -0
- package/dist/coordination/contract.js +63 -0
- package/dist/coordination/contract.js.map +1 -0
- package/dist/coordination/format.d.ts +5 -0
- package/dist/coordination/format.js +60 -0
- package/dist/coordination/format.js.map +1 -0
- package/dist/coordination/tree-events.d.ts +31 -0
- package/dist/coordination/tree-events.js +226 -0
- package/dist/coordination/tree-events.js.map +1 -0
- package/dist/coordination/wiring.d.ts +48 -0
- package/dist/coordination/wiring.js +143 -0
- package/dist/coordination/wiring.js.map +1 -0
- package/dist/coordination-cli.d.ts +34 -0
- package/dist/coordination-cli.js +460 -0
- package/dist/coordination-cli.js.map +1 -0
- package/dist/ftown-cli.js +9 -2
- package/dist/ftown-cli.js.map +1 -1
- package/dist/index.js +30 -7
- package/dist/index.js.map +1 -1
- package/dist/local-api-server.d.ts +6 -1
- package/dist/local-api-server.js +282 -5
- package/dist/local-api-server.js.map +1 -1
- package/dist/mail-delivery.d.ts +16 -2
- package/dist/mail-delivery.js +331 -77
- package/dist/mail-delivery.js.map +1 -1
- package/dist/mail-store.d.ts +7 -0
- package/dist/mail-store.js +71 -1
- package/dist/mail-store.js.map +1 -1
- package/dist/pull-request-events.d.ts +5 -0
- package/dist/pull-request-events.js +21 -1
- package/dist/pull-request-events.js.map +1 -1
- package/dist/security/bridge/permissions.js +20 -0
- package/dist/security/bridge/permissions.js.map +1 -1
- package/dist/types.d.ts +5 -1
- package/dist/wire-types.d.ts +17 -1
- package/dist/wire-types.js +1 -1
- package/dist/wire-types.js.map +1 -1
- package/package.json +2 -2
- package/skills/ftown/SKILL.md +29 -4
- package/skills/ftown/references/orchestrator.md +12 -7
- package/skills/ftown/references/sessions.md +45 -3
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FROZEN CONTRACT — coordination phase A (session cards, board, tree events,
|
|
3
|
+
* nudge-once mail, digest). Types, constants and interfaces only; no logic.
|
|
4
|
+
* Implementers must not modify this file. Changes go through the orchestrator.
|
|
5
|
+
*/
|
|
6
|
+
import type { Activity } from '../attention-events.js';
|
|
7
|
+
import type { PullRequestState } from '../pull-request-status-protocol.js';
|
|
8
|
+
import type { MailMessage, Session, SessionStatus } from '../types.js';
|
|
9
|
+
export declare const CARD_STATES: readonly ["working", "blocked", "waiting_review", "done", "failed"];
|
|
10
|
+
export type CardState = typeof CARD_STATES[number];
|
|
11
|
+
export declare const BLOCKED_ON_KINDS: readonly ["human", "parent", "session", "external"];
|
|
12
|
+
export type BlockedOnKind = typeof BLOCKED_ON_KINDS[number];
|
|
13
|
+
export declare const CARD_LIMITS: {
|
|
14
|
+
readonly headline: 140;
|
|
15
|
+
readonly next: 280;
|
|
16
|
+
readonly blockedText: 280;
|
|
17
|
+
/** progress.total upper bound; done must be 0..total. */
|
|
18
|
+
readonly progressTotal: 10000;
|
|
19
|
+
};
|
|
20
|
+
export interface CardProgress {
|
|
21
|
+
done: number;
|
|
22
|
+
total: number;
|
|
23
|
+
}
|
|
24
|
+
export interface CardBlockedOn {
|
|
25
|
+
kind: BlockedOnKind; /** session id when kind==='session' */
|
|
26
|
+
ref?: string;
|
|
27
|
+
text: string;
|
|
28
|
+
}
|
|
29
|
+
/** Stored on Session.card (session.json) so it flows to the dashboard via toWireSession. */
|
|
30
|
+
export interface SessionCardDeclared {
|
|
31
|
+
state: CardState;
|
|
32
|
+
headline: string;
|
|
33
|
+
progress?: CardProgress;
|
|
34
|
+
blockedOn?: CardBlockedOn;
|
|
35
|
+
next?: string;
|
|
36
|
+
updatedAt: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Partial update. Omitted field = unchanged; null = remove.
|
|
40
|
+
* The first write to a session with no card must include state and headline.
|
|
41
|
+
* Setting state to anything other than 'blocked' clears blockedOn unless blockedOn is also given.
|
|
42
|
+
*/
|
|
43
|
+
export interface CardPatch {
|
|
44
|
+
state?: CardState;
|
|
45
|
+
headline?: string;
|
|
46
|
+
progress?: CardProgress | null;
|
|
47
|
+
blockedOn?: CardBlockedOn | null;
|
|
48
|
+
next?: string | null;
|
|
49
|
+
}
|
|
50
|
+
export declare class CardValidationError extends Error {
|
|
51
|
+
constructor(message: string);
|
|
52
|
+
}
|
|
53
|
+
export interface CardPr {
|
|
54
|
+
url: string;
|
|
55
|
+
state: PullRequestState;
|
|
56
|
+
stale: boolean;
|
|
57
|
+
checkedAt: string | null;
|
|
58
|
+
}
|
|
59
|
+
export interface CardDerived {
|
|
60
|
+
status: SessionStatus;
|
|
61
|
+
activity: Activity['state'];
|
|
62
|
+
/** activity.lastActivityAt when activity === 'idle'. */
|
|
63
|
+
idleSince?: string;
|
|
64
|
+
openAttention: number;
|
|
65
|
+
/** inbox messages without deliveredAt */
|
|
66
|
+
unreadMail: number;
|
|
67
|
+
pr?: CardPr;
|
|
68
|
+
previews: string[];
|
|
69
|
+
shellType?: string;
|
|
70
|
+
model?: string;
|
|
71
|
+
}
|
|
72
|
+
export interface SessionCard {
|
|
73
|
+
sessionId: string;
|
|
74
|
+
name: string;
|
|
75
|
+
parentSessionId?: string;
|
|
76
|
+
declared: SessionCardDeclared | null;
|
|
77
|
+
derived: CardDerived;
|
|
78
|
+
/** declared exists and activity.lastActivityAt is more than CARD_STALE_MS after declared.updatedAt */
|
|
79
|
+
stale: boolean;
|
|
80
|
+
}
|
|
81
|
+
export declare const CARD_STALE_MS: number;
|
|
82
|
+
export interface CardStoreDeps {
|
|
83
|
+
loadSession(id: string): Promise<Session | null>;
|
|
84
|
+
saveSession(session: Session): Promise<void>;
|
|
85
|
+
/** called after a successful set/clear with the saved session (publish to dashboard + emit tree event) */
|
|
86
|
+
onChange?(session: Session, previous: SessionCardDeclared | null): void | Promise<void>;
|
|
87
|
+
now(): number;
|
|
88
|
+
}
|
|
89
|
+
/** Implementation: bridge/src/coordination/card-store.ts → `export class CardStore implements CardStoreApi { constructor(deps: CardStoreDeps) }` */
|
|
90
|
+
export interface CardStoreApi {
|
|
91
|
+
get(sessionId: string): Promise<SessionCardDeclared | null>;
|
|
92
|
+
/** validates (CardValidationError), merges, stamps updatedAt, saves. Throws Error('Session not found') if missing. */
|
|
93
|
+
set(sessionId: string, patch: CardPatch): Promise<SessionCardDeclared>;
|
|
94
|
+
clear(sessionId: string): Promise<void>;
|
|
95
|
+
}
|
|
96
|
+
/** Root of a tree = session.parentSessionId ?? session.id (tree is one level deep). */
|
|
97
|
+
export declare const TREE_EVENTS_FILE = "tree-events.jsonl";
|
|
98
|
+
export declare const TREE_EVENTS_RETAIN = 5000;
|
|
99
|
+
export declare const TREE_EVENTS_COMPACT_AT = 6000;
|
|
100
|
+
export declare const TREE_EVENTS_MAX_WAIT_MS = 300000;
|
|
101
|
+
export declare const TREE_EVENT_SUMMARY_MAX = 300;
|
|
102
|
+
export declare const TREE_EVENT_KINDS: readonly ["status", "attention", "pr", "preview", "exit", "idle", "result", "mail"];
|
|
103
|
+
export type TreeEventKind = typeof TREE_EVENT_KINDS[number];
|
|
104
|
+
export interface TreeEvent {
|
|
105
|
+
seq: number;
|
|
106
|
+
ts: string;
|
|
107
|
+
sessionId: string;
|
|
108
|
+
sessionName: string;
|
|
109
|
+
kind: TreeEventKind;
|
|
110
|
+
summary: string;
|
|
111
|
+
ref?: string;
|
|
112
|
+
}
|
|
113
|
+
export type TreeEventInput = Omit<TreeEvent, 'seq' | 'ts'>;
|
|
114
|
+
export interface TreeEventPage {
|
|
115
|
+
events: TreeEvent[]; /** highest seq returned, or `since` if none */
|
|
116
|
+
cursor: number;
|
|
117
|
+
timedOut?: boolean;
|
|
118
|
+
}
|
|
119
|
+
export interface TreeEventLogDeps {
|
|
120
|
+
sessionDir(id: string): string;
|
|
121
|
+
now(): number;
|
|
122
|
+
}
|
|
123
|
+
/** Implementation: bridge/src/coordination/tree-events.ts → `export class TreeEventLog implements TreeEventLogApi { constructor(deps: TreeEventLogDeps) }` */
|
|
124
|
+
export interface TreeEventLogApi {
|
|
125
|
+
append(rootId: string, event: TreeEventInput): Promise<TreeEvent>;
|
|
126
|
+
/** events with seq > since (default 0), oldest first, at most limit (default 100, max 500) */
|
|
127
|
+
read(rootId: string, opts?: {
|
|
128
|
+
since?: number;
|
|
129
|
+
limit?: number;
|
|
130
|
+
}): Promise<TreeEventPage>;
|
|
131
|
+
/** resolves immediately if events exist after since; otherwise on next append or timeout (clamped to TREE_EVENTS_MAX_WAIT_MS) with timedOut: true */
|
|
132
|
+
wait(rootId: string, since: number, timeoutMs: number, signal?: AbortSignal): Promise<TreeEventPage>;
|
|
133
|
+
/** highest seq for root (0 if none) */
|
|
134
|
+
head(rootId: string): Promise<number>;
|
|
135
|
+
}
|
|
136
|
+
export type BoardRollupKey = CardState | 'unset';
|
|
137
|
+
export interface Board {
|
|
138
|
+
rootId: string;
|
|
139
|
+
/** tree event head at render time; pass back as `since` */
|
|
140
|
+
cursor: number;
|
|
141
|
+
/** children of rootId (not the root itself), ordered by createdAt ascending; filtered when ready/since */
|
|
142
|
+
cards: SessionCard[];
|
|
143
|
+
rollup: Record<BoardRollupKey, number>;
|
|
144
|
+
}
|
|
145
|
+
export interface BoardQuery {
|
|
146
|
+
/** any session id; root is resolved */
|
|
147
|
+
sessionId: string;
|
|
148
|
+
/** only children with tree events of seq > since */
|
|
149
|
+
since?: number;
|
|
150
|
+
/** only children whose derived.pr.state === 'passed' and declared.state !== 'done' (approval/mergeable not tracked in phase A) */
|
|
151
|
+
ready?: boolean;
|
|
152
|
+
/** include archived/completed children; default false = status running|pending only plus any with events after since */
|
|
153
|
+
all?: boolean;
|
|
154
|
+
}
|
|
155
|
+
export interface BoardDeps {
|
|
156
|
+
loadSession(id: string): Promise<Session | null>;
|
|
157
|
+
listSessions(): Promise<Session[]>;
|
|
158
|
+
attention(id: string): Promise<{
|
|
159
|
+
activity: Activity;
|
|
160
|
+
openCount: number;
|
|
161
|
+
}>;
|
|
162
|
+
prStatus(url: string): Promise<{
|
|
163
|
+
state: PullRequestState;
|
|
164
|
+
stale: boolean;
|
|
165
|
+
checkedAt: string | null;
|
|
166
|
+
} | null>;
|
|
167
|
+
unreadMail(id: string): Promise<number>;
|
|
168
|
+
events: TreeEventLogApi;
|
|
169
|
+
now(): number;
|
|
170
|
+
}
|
|
171
|
+
/** Implementation: bridge/src/coordination/board.ts → `export class BoardService implements BoardServiceApi { constructor(deps: BoardDeps) }` */
|
|
172
|
+
export interface BoardServiceApi {
|
|
173
|
+
card(sessionId: string): Promise<SessionCard | null>;
|
|
174
|
+
board(query: BoardQuery): Promise<Board>;
|
|
175
|
+
/** deterministic one-line digest of tree events after sinceSeq for the root of sessionId; null if nothing new */
|
|
176
|
+
digest(sessionId: string, sinceSeq: number): Promise<{
|
|
177
|
+
line: string | null;
|
|
178
|
+
cursor: number;
|
|
179
|
+
}>;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Digest line format (exact):
|
|
183
|
+
* `[ftown] since HH:MM: <part> | <part> | ...`
|
|
184
|
+
* Parts, in order, one per child with events (latest event per child wins, max 6 children then `+N more`):
|
|
185
|
+
* `<name> <state>` + (headline ? ` "<headline ≤60>"` : '') + (pr ? ` (PR <state>)` : '')
|
|
186
|
+
* then, if any: `<n> question(s) waiting`. Total line ≤ 500 chars. HH:MM is local time of the first event.
|
|
187
|
+
*/
|
|
188
|
+
export declare const DIGEST_MAX = 500;
|
|
189
|
+
/** Plain `ftown board` row (exact column order, single spaces):
|
|
190
|
+
* `<name> <state|-> <activity>[ idle <Nm>] <headline>[ PR:<state>][ !<openAttention>][ ✉<unreadMail>][ (stale)]`
|
|
191
|
+
*/
|
|
192
|
+
/** Replaces MAIL_TYPES in mail-delivery.ts and wire-types.ts. */
|
|
193
|
+
export declare const MAIL_TYPES_V2: readonly ["message", "task", "result", "escalation", "fyi"];
|
|
194
|
+
export type MailTypeV2 = typeof MAIL_TYPES_V2[number];
|
|
195
|
+
/** fyi never schedules a nudge or wake; collected only at the next natural drain/read. */
|
|
196
|
+
export declare const NON_WAKING_MAIL_TYPES: ReadonlySet<MailTypeV2>;
|
|
197
|
+
/** Max native-wake attempts (codex/muse) for one batch before the batch is stamped and left. */
|
|
198
|
+
export declare const MAX_WAKE_ATTEMPTS = 3;
|
|
199
|
+
/**
|
|
200
|
+
* Nudge-once rules (MailMessage.nudgedAt):
|
|
201
|
+
* 1. A nudge/wake fires only if ≥1 undelivered, waking message has no nudgedAt.
|
|
202
|
+
* 2. On firing (any path: PTY, codex, muse, plugin), stamp nudgedAt on every undelivered waking message → that is the batch.
|
|
203
|
+
* 3. A stamped message never causes another nudge on any path (new mail, recovery, retry).
|
|
204
|
+
* 4. Recovery (recoverPendingMail) skips stamped mail.
|
|
205
|
+
* 5. Nudge text: NUDGE_TEXT(counts, digest).
|
|
206
|
+
*/
|
|
207
|
+
export type MailCounts = Partial<Record<MailTypeV2, number>>;
|
|
208
|
+
export interface DigestProvider {
|
|
209
|
+
/** one-line digest for the recipient (as tree root), or null. Must not throw; return null on error. */
|
|
210
|
+
digestFor(recipientSessionId: string): Promise<string | null>;
|
|
211
|
+
}
|
|
212
|
+
/** Event sink so mail-delivery can record child→parent `result` mail on the tree log without importing it. */
|
|
213
|
+
export interface MailEventSink {
|
|
214
|
+
onAccepted(message: MailMessage, recipient: Session): void | Promise<void>;
|
|
215
|
+
}
|
|
216
|
+
export declare const COORDINATION_ROUTES: {
|
|
217
|
+
/** GET → SessionCard; PUT body CardPatch → SessionCard (400 on CardValidationError); DELETE → { ok: true } */
|
|
218
|
+
readonly card: "/api/sessions/:id/card";
|
|
219
|
+
/** GET ?since=<n>&ready=1&all=1 → Board */
|
|
220
|
+
readonly board: "/api/sessions/:id/board";
|
|
221
|
+
/** GET ?since=<n>&limit=<n>&waitSeconds=<0..300> → TreeEventPage (root of :id) */
|
|
222
|
+
readonly events: "/api/sessions/:id/events";
|
|
223
|
+
};
|
|
224
|
+
/**
|
|
225
|
+
* ftown status [--json] [--session-id <id>] → print own card (SessionCard)
|
|
226
|
+
* ftown status set [--state <s>] [--headline <t>] [--progress <d>/<t>|--no-progress]
|
|
227
|
+
* [--on human|parent|external|session:<id>] [--text <t>] [--next <t>|--no-next] [--session-id <id>]
|
|
228
|
+
* ftown status clear [--session-id <id>]
|
|
229
|
+
* ftown board [--json] [--since <n>] [--ready] [--all] [--session-id <id>]
|
|
230
|
+
* [--wait --until any-change|any-done|any-blocked|all-done|any-ready [--timeout <s≤300>]]
|
|
231
|
+
* ftown events [--json] [--since <n>] [--limit <n>] [--wait [--timeout <s≤300>]] [--session-id <id>]
|
|
232
|
+
* Exit codes: 0 ok, 1 error, 2 usage, 3 wait timed out with condition unmet.
|
|
233
|
+
* Default session id: FTOWN_SESSION_ID.
|
|
234
|
+
*/
|
|
235
|
+
export declare const BOARD_WAIT_CONDITIONS: readonly ["any-change", "any-done", "any-blocked", "all-done", "any-ready"];
|
|
236
|
+
export type BoardWaitCondition = typeof BOARD_WAIT_CONDITIONS[number];
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// ───────────────────────── Session card (declared by the agent) ─────────────────────────
|
|
2
|
+
export const CARD_STATES = ['working', 'blocked', 'waiting_review', 'done', 'failed'];
|
|
3
|
+
export const BLOCKED_ON_KINDS = ['human', 'parent', 'session', 'external'];
|
|
4
|
+
export const CARD_LIMITS = {
|
|
5
|
+
headline: 140,
|
|
6
|
+
next: 280,
|
|
7
|
+
blockedText: 280,
|
|
8
|
+
/** progress.total upper bound; done must be 0..total. */
|
|
9
|
+
progressTotal: 10_000,
|
|
10
|
+
};
|
|
11
|
+
export class CardValidationError extends Error {
|
|
12
|
+
constructor(message) { super(message); this.name = 'CardValidationError'; }
|
|
13
|
+
}
|
|
14
|
+
export const CARD_STALE_MS = 10 * 60_000;
|
|
15
|
+
// ───────────────────────── Tree event log ─────────────────────────
|
|
16
|
+
/** Root of a tree = session.parentSessionId ?? session.id (tree is one level deep). */
|
|
17
|
+
export const TREE_EVENTS_FILE = 'tree-events.jsonl'; // in sessionDir(rootId)
|
|
18
|
+
export const TREE_EVENTS_RETAIN = 5_000; // keep newest N on compaction
|
|
19
|
+
export const TREE_EVENTS_COMPACT_AT = 6_000;
|
|
20
|
+
export const TREE_EVENTS_MAX_WAIT_MS = 300_000;
|
|
21
|
+
export const TREE_EVENT_SUMMARY_MAX = 300;
|
|
22
|
+
export const TREE_EVENT_KINDS = ['status', 'attention', 'pr', 'preview', 'exit', 'idle', 'result', 'mail'];
|
|
23
|
+
/**
|
|
24
|
+
* Digest line format (exact):
|
|
25
|
+
* `[ftown] since HH:MM: <part> | <part> | ...`
|
|
26
|
+
* Parts, in order, one per child with events (latest event per child wins, max 6 children then `+N more`):
|
|
27
|
+
* `<name> <state>` + (headline ? ` "<headline ≤60>"` : '') + (pr ? ` (PR <state>)` : '')
|
|
28
|
+
* then, if any: `<n> question(s) waiting`. Total line ≤ 500 chars. HH:MM is local time of the first event.
|
|
29
|
+
*/
|
|
30
|
+
export const DIGEST_MAX = 500;
|
|
31
|
+
/** Plain `ftown board` row (exact column order, single spaces):
|
|
32
|
+
* `<name> <state|-> <activity>[ idle <Nm>] <headline>[ PR:<state>][ !<openAttention>][ ✉<unreadMail>][ (stale)]`
|
|
33
|
+
*/
|
|
34
|
+
// ───────────────────────── Mail: nudge once + fyi ─────────────────────────
|
|
35
|
+
/** Replaces MAIL_TYPES in mail-delivery.ts and wire-types.ts. */
|
|
36
|
+
export const MAIL_TYPES_V2 = ['message', 'task', 'result', 'escalation', 'fyi'];
|
|
37
|
+
/** fyi never schedules a nudge or wake; collected only at the next natural drain/read. */
|
|
38
|
+
export const NON_WAKING_MAIL_TYPES = new Set(['fyi']);
|
|
39
|
+
/** Max native-wake attempts (codex/muse) for one batch before the batch is stamped and left. */
|
|
40
|
+
export const MAX_WAKE_ATTEMPTS = 3;
|
|
41
|
+
// ───────────────────────── Local API (all via dispatchSecure, :id = target session) ─────────────────────────
|
|
42
|
+
export const COORDINATION_ROUTES = {
|
|
43
|
+
/** GET → SessionCard; PUT body CardPatch → SessionCard (400 on CardValidationError); DELETE → { ok: true } */
|
|
44
|
+
card: '/api/sessions/:id/card',
|
|
45
|
+
/** GET ?since=<n>&ready=1&all=1 → Board */
|
|
46
|
+
board: '/api/sessions/:id/board',
|
|
47
|
+
/** GET ?since=<n>&limit=<n>&waitSeconds=<0..300> → TreeEventPage (root of :id) */
|
|
48
|
+
events: '/api/sessions/:id/events',
|
|
49
|
+
};
|
|
50
|
+
// ───────────────────────── CLI (new top-level groups in ftown-cli.ts) ─────────────────────────
|
|
51
|
+
/**
|
|
52
|
+
* ftown status [--json] [--session-id <id>] → print own card (SessionCard)
|
|
53
|
+
* ftown status set [--state <s>] [--headline <t>] [--progress <d>/<t>|--no-progress]
|
|
54
|
+
* [--on human|parent|external|session:<id>] [--text <t>] [--next <t>|--no-next] [--session-id <id>]
|
|
55
|
+
* ftown status clear [--session-id <id>]
|
|
56
|
+
* ftown board [--json] [--since <n>] [--ready] [--all] [--session-id <id>]
|
|
57
|
+
* [--wait --until any-change|any-done|any-blocked|all-done|any-ready [--timeout <s≤300>]]
|
|
58
|
+
* ftown events [--json] [--since <n>] [--limit <n>] [--wait [--timeout <s≤300>]] [--session-id <id>]
|
|
59
|
+
* Exit codes: 0 ok, 1 error, 2 usage, 3 wait timed out with condition unmet.
|
|
60
|
+
* Default session id: FTOWN_SESSION_ID.
|
|
61
|
+
*/
|
|
62
|
+
export const BOARD_WAIT_CONDITIONS = ['any-change', 'any-done', 'any-blocked', 'all-done', 'any-ready'];
|
|
63
|
+
//# sourceMappingURL=contract.js.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type Board, type SessionCard, type TreeEvent } from './contract.js';
|
|
2
|
+
export declare function formatLocalHHMM(iso: string): string;
|
|
3
|
+
export declare function formatBoardRow(card: SessionCard, nowMs: number): string;
|
|
4
|
+
export declare function formatBoard(board: Board, nowMs: number): string;
|
|
5
|
+
export declare function formatEvent(e: TreeEvent): string;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { CARD_STATES, } from './contract.js';
|
|
2
|
+
const ROLLUP_ORDER = [...CARD_STATES, 'unset'];
|
|
3
|
+
function pad2(n) {
|
|
4
|
+
return String(n).padStart(2, '0');
|
|
5
|
+
}
|
|
6
|
+
export function formatLocalHHMM(iso) {
|
|
7
|
+
const d = new Date(iso);
|
|
8
|
+
return `${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
|
|
9
|
+
}
|
|
10
|
+
export function formatBoardRow(card, nowMs) {
|
|
11
|
+
const state = card.declared?.state ?? '-';
|
|
12
|
+
const activity = card.derived.activity;
|
|
13
|
+
let idlePart = '';
|
|
14
|
+
if (activity === 'idle') {
|
|
15
|
+
const idleSinceMs = card.derived.idleSince !== undefined
|
|
16
|
+
? Date.parse(card.derived.idleSince)
|
|
17
|
+
: NaN;
|
|
18
|
+
const idleMin = Number.isNaN(idleSinceMs)
|
|
19
|
+
? 0
|
|
20
|
+
: Math.max(0, Math.floor((nowMs - idleSinceMs) / 60_000));
|
|
21
|
+
idlePart = ` idle ${idleMin}m`;
|
|
22
|
+
}
|
|
23
|
+
const headline = card.declared?.headline || '-';
|
|
24
|
+
let suffix = '';
|
|
25
|
+
if (card.derived.pr) {
|
|
26
|
+
suffix += ` PR:${card.derived.pr.state}`;
|
|
27
|
+
}
|
|
28
|
+
if (card.derived.openAttention > 0) {
|
|
29
|
+
suffix += ` !${card.derived.openAttention}`;
|
|
30
|
+
}
|
|
31
|
+
if (card.derived.unreadMail > 0) {
|
|
32
|
+
suffix += ` ✉${card.derived.unreadMail}`;
|
|
33
|
+
}
|
|
34
|
+
if (card.stale) {
|
|
35
|
+
suffix += ' (stale)';
|
|
36
|
+
}
|
|
37
|
+
return `${card.name} ${state} ${activity}${idlePart} ${headline}${suffix}`;
|
|
38
|
+
}
|
|
39
|
+
export function formatBoard(board, nowMs) {
|
|
40
|
+
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
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (rollupParts.length > 0) {
|
|
52
|
+
rows.push(rollupParts.join(', '));
|
|
53
|
+
}
|
|
54
|
+
return rows.join('\n');
|
|
55
|
+
}
|
|
56
|
+
export function formatEvent(e) {
|
|
57
|
+
const hhmm = formatLocalHHMM(e.ts);
|
|
58
|
+
return `#${e.seq} ${hhmm} ${e.sessionName} ${e.kind}: ${e.summary}`;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=format.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format.js","sourceRoot":"","sources":["../../src/coordination/format.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type TreeEvent, type TreeEventInput, type TreeEventLogApi, type TreeEventLogDeps, type TreeEventPage } from './contract.js';
|
|
2
|
+
/** Per-root JSONL log of tree events (one JSON object per line, seq survives compaction). */
|
|
3
|
+
export declare class TreeEventLog implements TreeEventLogApi {
|
|
4
|
+
private deps;
|
|
5
|
+
private locks;
|
|
6
|
+
/** Highest seq seen per root, cached after the first file read. */
|
|
7
|
+
private heads;
|
|
8
|
+
/** Valid event lines per root, cached alongside the head to trigger compaction without recounting. */
|
|
9
|
+
private counts;
|
|
10
|
+
/** Wake callbacks of long-poll waiters, keyed by root. */
|
|
11
|
+
private waiters;
|
|
12
|
+
constructor(deps: TreeEventLogDeps);
|
|
13
|
+
private file;
|
|
14
|
+
private requireValidId;
|
|
15
|
+
private withLock;
|
|
16
|
+
/** Parses the log, keeping only well-formed events; a truncated/corrupt tail is tolerated. */
|
|
17
|
+
private events;
|
|
18
|
+
/** Cold-fills the head cache. Must run while holding the per-root lock, so it can never
|
|
19
|
+
* overwrite a newer head written by a concurrent append. */
|
|
20
|
+
private stateLocked;
|
|
21
|
+
private cleaned;
|
|
22
|
+
append(rootId: string, input: TreeEventInput): Promise<TreeEvent>;
|
|
23
|
+
/** Rewrites the log down to the newest TREE_EVENTS_RETAIN events via tmp file + rename. Seqs are preserved. */
|
|
24
|
+
private compact;
|
|
25
|
+
read(rootId: string, opts?: {
|
|
26
|
+
since?: number;
|
|
27
|
+
limit?: number;
|
|
28
|
+
}): Promise<TreeEventPage>;
|
|
29
|
+
wait(rootId: string, since: number, timeoutMs: number, signal?: AbortSignal): Promise<TreeEventPage>;
|
|
30
|
+
head(rootId: string): Promise<number>;
|
|
31
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { appendFile, mkdir, open, readFile, rename, rm } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { TREE_EVENTS_COMPACT_AT, TREE_EVENTS_FILE, TREE_EVENTS_MAX_WAIT_MS, TREE_EVENTS_RETAIN, TREE_EVENT_KINDS, TREE_EVENT_SUMMARY_MAX, } from './contract.js';
|
|
5
|
+
const ID_PATTERN = /^[A-Za-z0-9_-]{1,200}$/;
|
|
6
|
+
const SESSION_NAME_MAX = 200;
|
|
7
|
+
const REF_MAX = 500;
|
|
8
|
+
const READ_LIMIT_DEFAULT = 100;
|
|
9
|
+
const READ_LIMIT_MAX = 500;
|
|
10
|
+
function clampInt(value, fallback, min, max) {
|
|
11
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
12
|
+
return fallback;
|
|
13
|
+
return Math.min(max, Math.max(min, Math.trunc(value)));
|
|
14
|
+
}
|
|
15
|
+
/** Replaces control characters and whitespace with spaces, then collapses runs of spaces. */
|
|
16
|
+
function cleanLine(text) {
|
|
17
|
+
return (typeof text === 'string' ? text : '')
|
|
18
|
+
.replace(/[\u0000-\u001f\u007f\s]/g, ' ')
|
|
19
|
+
.replace(/\s+/g, ' ')
|
|
20
|
+
.trim();
|
|
21
|
+
}
|
|
22
|
+
/** Hard-truncates to `max` characters total, marking the cut with an ellipsis. */
|
|
23
|
+
function truncate(text, max) {
|
|
24
|
+
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
25
|
+
}
|
|
26
|
+
/** Accepts only lines that parse to a well-formed tree event; anything else (truncated tail included) is skipped. */
|
|
27
|
+
function parseEvent(line) {
|
|
28
|
+
if (!line.trim())
|
|
29
|
+
return null;
|
|
30
|
+
let value;
|
|
31
|
+
try {
|
|
32
|
+
value = JSON.parse(line);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
38
|
+
return null;
|
|
39
|
+
const record = value;
|
|
40
|
+
const { seq, ts, sessionId, sessionName, kind, summary, ref } = record;
|
|
41
|
+
if (typeof seq !== 'number' || !Number.isInteger(seq) || seq < 1)
|
|
42
|
+
return null;
|
|
43
|
+
if (typeof ts !== 'string' || typeof sessionId !== 'string' || typeof sessionName !== 'string' || typeof summary !== 'string')
|
|
44
|
+
return null;
|
|
45
|
+
if (typeof kind !== 'string' || !TREE_EVENT_KINDS.includes(kind))
|
|
46
|
+
return null;
|
|
47
|
+
if (ref !== undefined && typeof ref !== 'string')
|
|
48
|
+
return null;
|
|
49
|
+
return { seq, ts, sessionId, sessionName, kind: kind, summary, ...(ref !== undefined ? { ref } : {}) };
|
|
50
|
+
}
|
|
51
|
+
/** Per-root JSONL log of tree events (one JSON object per line, seq survives compaction). */
|
|
52
|
+
export class TreeEventLog {
|
|
53
|
+
deps;
|
|
54
|
+
locks = new Map();
|
|
55
|
+
/** Highest seq seen per root, cached after the first file read. */
|
|
56
|
+
heads = new Map();
|
|
57
|
+
/** Valid event lines per root, cached alongside the head to trigger compaction without recounting. */
|
|
58
|
+
counts = new Map();
|
|
59
|
+
/** Wake callbacks of long-poll waiters, keyed by root. */
|
|
60
|
+
waiters = new Map();
|
|
61
|
+
constructor(deps) {
|
|
62
|
+
this.deps = deps;
|
|
63
|
+
}
|
|
64
|
+
file(rootId) {
|
|
65
|
+
return join(this.deps.sessionDir(rootId), TREE_EVENTS_FILE);
|
|
66
|
+
}
|
|
67
|
+
requireValidId(rootId) {
|
|
68
|
+
if (!ID_PATTERN.test(rootId))
|
|
69
|
+
throw new Error('Invalid session id');
|
|
70
|
+
}
|
|
71
|
+
withLock(rootId, fn) {
|
|
72
|
+
const previous = this.locks.get(rootId) ?? Promise.resolve();
|
|
73
|
+
const next = previous.then(fn, fn);
|
|
74
|
+
const settled = next.catch(() => undefined);
|
|
75
|
+
this.locks.set(rootId, settled);
|
|
76
|
+
void settled.then(() => { if (this.locks.get(rootId) === settled)
|
|
77
|
+
this.locks.delete(rootId); });
|
|
78
|
+
return next;
|
|
79
|
+
}
|
|
80
|
+
/** Parses the log, keeping only well-formed events; a truncated/corrupt tail is tolerated. */
|
|
81
|
+
async events(rootId) {
|
|
82
|
+
let content;
|
|
83
|
+
try {
|
|
84
|
+
content = await readFile(this.file(rootId), 'utf8');
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (error.code === 'ENOENT')
|
|
88
|
+
return [];
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
const events = [];
|
|
92
|
+
for (const line of content.split('\n')) {
|
|
93
|
+
const event = parseEvent(line);
|
|
94
|
+
if (event)
|
|
95
|
+
events.push(event);
|
|
96
|
+
}
|
|
97
|
+
return events;
|
|
98
|
+
}
|
|
99
|
+
/** Cold-fills the head cache. Must run while holding the per-root lock, so it can never
|
|
100
|
+
* overwrite a newer head written by a concurrent append. */
|
|
101
|
+
async stateLocked(rootId) {
|
|
102
|
+
const cachedHead = this.heads.get(rootId);
|
|
103
|
+
const cachedCount = this.counts.get(rootId);
|
|
104
|
+
if (cachedHead !== undefined && cachedCount !== undefined)
|
|
105
|
+
return { head: cachedHead, count: cachedCount };
|
|
106
|
+
const events = await this.events(rootId);
|
|
107
|
+
const head = events.reduce((max, event) => Math.max(max, event.seq), 0);
|
|
108
|
+
this.heads.set(rootId, head);
|
|
109
|
+
this.counts.set(rootId, events.length);
|
|
110
|
+
return { head, count: events.length };
|
|
111
|
+
}
|
|
112
|
+
cleaned(input) {
|
|
113
|
+
if (!ID_PATTERN.test(input.sessionId))
|
|
114
|
+
throw new Error('Invalid session id');
|
|
115
|
+
if (typeof input.kind !== 'string' || !TREE_EVENT_KINDS.includes(input.kind)) {
|
|
116
|
+
throw new Error('Invalid event kind');
|
|
117
|
+
}
|
|
118
|
+
const summary = truncate(cleanLine(input.summary), TREE_EVENT_SUMMARY_MAX);
|
|
119
|
+
const sessionName = cleanLine(input.sessionName).slice(0, SESSION_NAME_MAX);
|
|
120
|
+
const ref = input.ref === undefined ? undefined : cleanLine(input.ref);
|
|
121
|
+
return { sessionId: input.sessionId, sessionName, kind: input.kind, summary, ...(ref ? { ref: ref.slice(0, REF_MAX) } : {}) };
|
|
122
|
+
}
|
|
123
|
+
async append(rootId, input) {
|
|
124
|
+
this.requireValidId(rootId);
|
|
125
|
+
const fields = this.cleaned(input);
|
|
126
|
+
const event = { seq: 0, ts: new Date(this.deps.now()).toISOString(), ...fields };
|
|
127
|
+
await this.withLock(rootId, async () => {
|
|
128
|
+
const { head, count } = await this.stateLocked(rootId);
|
|
129
|
+
event.seq = head + 1;
|
|
130
|
+
const dir = this.deps.sessionDir(rootId);
|
|
131
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
132
|
+
await appendFile(this.file(rootId), `${JSON.stringify(event)}\n`, { mode: 0o600 });
|
|
133
|
+
this.heads.set(rootId, event.seq);
|
|
134
|
+
this.counts.set(rootId, count + 1);
|
|
135
|
+
if (count + 1 > TREE_EVENTS_COMPACT_AT) {
|
|
136
|
+
// The event is already durable: a failed compaction must not fail the append.
|
|
137
|
+
// The count stays above the threshold, so the next append retries compaction.
|
|
138
|
+
await this.compact(rootId).catch((error) => {
|
|
139
|
+
console.warn(`[Bridge] Tree event compaction failed for session ${rootId}:`, error);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
for (const wake of Array.from(this.waiters.get(rootId) ?? []))
|
|
144
|
+
wake();
|
|
145
|
+
return event;
|
|
146
|
+
}
|
|
147
|
+
/** Rewrites the log down to the newest TREE_EVENTS_RETAIN events via tmp file + rename. Seqs are preserved. */
|
|
148
|
+
async compact(rootId) {
|
|
149
|
+
const retained = (await this.events(rootId)).sort((a, b) => a.seq - b.seq).slice(-TREE_EVENTS_RETAIN);
|
|
150
|
+
const file = this.file(rootId);
|
|
151
|
+
const tmp = `${file}.${randomUUID()}.tmp`;
|
|
152
|
+
try {
|
|
153
|
+
const handle = await open(tmp, 'wx', 0o600);
|
|
154
|
+
try {
|
|
155
|
+
await handle.writeFile(retained.map((event) => JSON.stringify(event)).join('\n') + (retained.length ? '\n' : ''), 'utf8');
|
|
156
|
+
await handle.sync();
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
await handle.close();
|
|
160
|
+
}
|
|
161
|
+
await rename(tmp, file);
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
await rm(tmp, { force: true }).catch(() => undefined);
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
this.counts.set(rootId, retained.length);
|
|
168
|
+
}
|
|
169
|
+
async read(rootId, opts = {}) {
|
|
170
|
+
this.requireValidId(rootId);
|
|
171
|
+
const since = clampInt(opts.since, 0, 0, Number.MAX_SAFE_INTEGER);
|
|
172
|
+
const limit = clampInt(opts.limit, READ_LIMIT_DEFAULT, 1, READ_LIMIT_MAX);
|
|
173
|
+
const matching = (await this.events(rootId)).filter((event) => event.seq > since).sort((a, b) => a.seq - b.seq);
|
|
174
|
+
const events = matching.slice(0, limit);
|
|
175
|
+
return { events, cursor: events.length ? events[events.length - 1].seq : since };
|
|
176
|
+
}
|
|
177
|
+
async wait(rootId, since, timeoutMs, signal) {
|
|
178
|
+
this.requireValidId(rootId);
|
|
179
|
+
const start = clampInt(since, 0, 0, Number.MAX_SAFE_INTEGER);
|
|
180
|
+
const ms = clampInt(timeoutMs, 0, 0, TREE_EVENTS_MAX_WAIT_MS);
|
|
181
|
+
// The wake callback is registered and armed before the first read, so an append
|
|
182
|
+
// committing while that read is in flight cannot be lost.
|
|
183
|
+
let wake;
|
|
184
|
+
const waiters = this.waiters.get(rootId) ?? new Set();
|
|
185
|
+
const waiter = () => wake?.();
|
|
186
|
+
waiters.add(waiter);
|
|
187
|
+
this.waiters.set(rootId, waiters);
|
|
188
|
+
let timer;
|
|
189
|
+
let removeAbort;
|
|
190
|
+
try {
|
|
191
|
+
if (signal?.aborted)
|
|
192
|
+
return { events: [], cursor: start, timedOut: true };
|
|
193
|
+
const settled = new Promise((resolve) => {
|
|
194
|
+
wake = () => resolve(false);
|
|
195
|
+
timer = setTimeout(() => resolve(true), ms);
|
|
196
|
+
timer.unref?.();
|
|
197
|
+
if (signal) {
|
|
198
|
+
const onAbort = () => resolve(true);
|
|
199
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
200
|
+
removeAbort = () => signal.removeEventListener('abort', onAbort);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
const immediate = await this.read(rootId, { since: start });
|
|
204
|
+
if (immediate.events.length)
|
|
205
|
+
return immediate;
|
|
206
|
+
const timedOut = await settled;
|
|
207
|
+
if (timedOut)
|
|
208
|
+
return { events: [], cursor: start, timedOut: true };
|
|
209
|
+
// Woken by an append: return the page as of now (empty if the append was for an already-seen seq).
|
|
210
|
+
return await this.read(rootId, { since: start });
|
|
211
|
+
}
|
|
212
|
+
finally {
|
|
213
|
+
if (timer)
|
|
214
|
+
clearTimeout(timer);
|
|
215
|
+
removeAbort?.();
|
|
216
|
+
waiters.delete(waiter);
|
|
217
|
+
if (!waiters.size)
|
|
218
|
+
this.waiters.delete(rootId);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async head(rootId) {
|
|
222
|
+
this.requireValidId(rootId);
|
|
223
|
+
return this.withLock(rootId, async () => (await this.stateLocked(rootId)).head);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
//# sourceMappingURL=tree-events.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tree-events.js","sourceRoot":"","sources":["../../src/coordination/tree-events.ts"],"names":[],"mappings":"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"}
|