ftown 0.25.2 → 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.
Files changed (108) hide show
  1. package/dist/command-rpc.d.ts +3 -0
  2. package/dist/command-rpc.js +41 -0
  3. package/dist/command-rpc.js.map +1 -1
  4. package/dist/coordination/board.d.ts +12 -0
  5. package/dist/coordination/board.js +240 -0
  6. package/dist/coordination/board.js.map +1 -0
  7. package/dist/coordination/card-store.d.ts +9 -0
  8. package/dist/coordination/card-store.js +188 -0
  9. package/dist/coordination/card-store.js.map +1 -0
  10. package/dist/coordination/contract.d.ts +236 -0
  11. package/dist/coordination/contract.js +63 -0
  12. package/dist/coordination/contract.js.map +1 -0
  13. package/dist/coordination/format.d.ts +5 -0
  14. package/dist/coordination/format.js +60 -0
  15. package/dist/coordination/format.js.map +1 -0
  16. package/dist/coordination/tree-events.d.ts +31 -0
  17. package/dist/coordination/tree-events.js +226 -0
  18. package/dist/coordination/tree-events.js.map +1 -0
  19. package/dist/coordination/wiring.d.ts +48 -0
  20. package/dist/coordination/wiring.js +143 -0
  21. package/dist/coordination/wiring.js.map +1 -0
  22. package/dist/coordination-cli.d.ts +34 -0
  23. package/dist/coordination-cli.js +460 -0
  24. package/dist/coordination-cli.js.map +1 -0
  25. package/dist/ftown-cli.js +12 -2
  26. package/dist/ftown-cli.js.map +1 -1
  27. package/dist/index.js +33 -7
  28. package/dist/index.js.map +1 -1
  29. package/dist/local-api-server.d.ts +6 -1
  30. package/dist/local-api-server.js +282 -5
  31. package/dist/local-api-server.js.map +1 -1
  32. package/dist/mail-delivery.d.ts +16 -2
  33. package/dist/mail-delivery.js +331 -77
  34. package/dist/mail-delivery.js.map +1 -1
  35. package/dist/mail-store.d.ts +7 -0
  36. package/dist/mail-store.js +71 -1
  37. package/dist/mail-store.js.map +1 -1
  38. package/dist/pull-request-events.d.ts +5 -0
  39. package/dist/pull-request-events.js +21 -1
  40. package/dist/pull-request-events.js.map +1 -1
  41. package/dist/quota/agy.d.ts +16 -0
  42. package/dist/quota/agy.js +287 -0
  43. package/dist/quota/agy.js.map +1 -0
  44. package/dist/quota/annotate.d.ts +5 -0
  45. package/dist/quota/annotate.js +56 -0
  46. package/dist/quota/annotate.js.map +1 -0
  47. package/dist/quota/claude.d.ts +27 -0
  48. package/dist/quota/claude.js +265 -0
  49. package/dist/quota/claude.js.map +1 -0
  50. package/dist/quota/codex.d.ts +25 -0
  51. package/dist/quota/codex.js +378 -0
  52. package/dist/quota/codex.js.map +1 -0
  53. package/dist/quota/format.d.ts +6 -0
  54. package/dist/quota/format.js +113 -0
  55. package/dist/quota/format.js.map +1 -0
  56. package/dist/quota/grok.d.ts +97 -0
  57. package/dist/quota/grok.js +563 -0
  58. package/dist/quota/grok.js.map +1 -0
  59. package/dist/quota/models-agy.d.ts +10 -0
  60. package/dist/quota/models-agy.js +144 -0
  61. package/dist/quota/models-agy.js.map +1 -0
  62. package/dist/quota/models-claude.d.ts +10 -0
  63. package/dist/quota/models-claude.js +227 -0
  64. package/dist/quota/models-claude.js.map +1 -0
  65. package/dist/quota/models-codex.d.ts +18 -0
  66. package/dist/quota/models-codex.js +305 -0
  67. package/dist/quota/models-codex.js.map +1 -0
  68. package/dist/quota/models-grok.d.ts +19 -0
  69. package/dist/quota/models-grok.js +272 -0
  70. package/dist/quota/models-grok.js.map +1 -0
  71. package/dist/quota/models-muse.d.ts +13 -0
  72. package/dist/quota/models-muse.js +200 -0
  73. package/dist/quota/models-muse.js.map +1 -0
  74. package/dist/quota/models-opencode.d.ts +51 -0
  75. package/dist/quota/models-opencode.js +400 -0
  76. package/dist/quota/models-opencode.js.map +1 -0
  77. package/dist/quota/muse.d.ts +38 -0
  78. package/dist/quota/muse.js +225 -0
  79. package/dist/quota/muse.js.map +1 -0
  80. package/dist/quota/opencode.d.ts +7 -0
  81. package/dist/quota/opencode.js +47 -0
  82. package/dist/quota/opencode.js.map +1 -0
  83. package/dist/quota/routing-config.d.ts +18 -0
  84. package/dist/quota/routing-config.js +438 -0
  85. package/dist/quota/routing-config.js.map +1 -0
  86. package/dist/quota/routing-service.d.ts +24 -0
  87. package/dist/quota/routing-service.js +118 -0
  88. package/dist/quota/routing-service.js.map +1 -0
  89. package/dist/quota/routing.d.ts +10 -0
  90. package/dist/quota/routing.js +180 -0
  91. package/dist/quota/routing.js.map +1 -0
  92. package/dist/quota/types.d.ts +304 -0
  93. package/dist/quota/types.js +45 -0
  94. package/dist/quota/types.js.map +1 -0
  95. package/dist/quota-cli.d.ts +80 -0
  96. package/dist/quota-cli.js +462 -0
  97. package/dist/quota-cli.js.map +1 -0
  98. package/dist/security/bridge/permissions.js +24 -2
  99. package/dist/security/bridge/permissions.js.map +1 -1
  100. package/dist/types.d.ts +7 -3
  101. package/dist/wire-types.d.ts +17 -1
  102. package/dist/wire-types.js +1 -1
  103. package/dist/wire-types.js.map +1 -1
  104. package/package.json +3 -2
  105. package/skills/ftown/SKILL.md +31 -5
  106. package/skills/ftown/references/orchestrator.md +17 -7
  107. package/skills/ftown/references/quota.md +177 -0
  108. package/skills/ftown/references/sessions.md +45 -3
@@ -0,0 +1,188 @@
1
+ import { BLOCKED_ON_KINDS, CARD_LIMITS, CARD_STATES, CardValidationError, } from './contract.js';
2
+ const ALLOWED_PATCH_KEYS = new Set(['state', 'headline', 'progress', 'blockedOn', 'next']);
3
+ const SESSION_REF_PATTERN = /^[A-Za-z0-9_-]{1,200}$/;
4
+ function hasOwn(record, key) {
5
+ return Object.prototype.hasOwnProperty.call(record, key);
6
+ }
7
+ function cleanText(value, field, minLength, maxLength) {
8
+ if (typeof value !== 'string')
9
+ throw new CardValidationError(`${field} must be a string`);
10
+ const withoutControls = [...value].map((char) => {
11
+ const code = char.codePointAt(0) ?? 0;
12
+ return code <= 31 || code === 127 ? ' ' : char;
13
+ }).join('');
14
+ const cleaned = withoutControls.replace(/\s+/g, ' ').trim();
15
+ const cleanedLength = [...cleaned].length;
16
+ if (cleanedLength < minLength)
17
+ throw new CardValidationError(`${field} must not be empty`);
18
+ if (cleanedLength > maxLength)
19
+ throw new CardValidationError(`${field} must be at most ${maxLength} characters`);
20
+ return cleaned;
21
+ }
22
+ function parseProgress(value) {
23
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
24
+ throw new CardValidationError('Progress must be an object');
25
+ }
26
+ const record = value;
27
+ const done = record.done;
28
+ const total = record.total;
29
+ if (typeof done !== 'number' || typeof total !== 'number' || !Number.isInteger(done) || !Number.isInteger(total)) {
30
+ throw new CardValidationError('Progress done and total must be integers');
31
+ }
32
+ if (total < 1 || total > CARD_LIMITS.progressTotal) {
33
+ throw new CardValidationError(`Progress total must be between 1 and ${CARD_LIMITS.progressTotal}`);
34
+ }
35
+ if (done < 0 || done > total) {
36
+ throw new CardValidationError('Progress done must be between 0 and total');
37
+ }
38
+ return { done, total };
39
+ }
40
+ function parseBlockedOn(value) {
41
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
42
+ throw new CardValidationError('Blocked-on must be an object');
43
+ }
44
+ const record = value;
45
+ const kind = BLOCKED_ON_KINDS.find((candidate) => candidate === record.kind);
46
+ if (!kind)
47
+ throw new CardValidationError('Blocked-on kind is invalid');
48
+ const text = cleanText(record.text, 'Blocked-on text', 1, CARD_LIMITS.blockedText);
49
+ if (kind === 'session') {
50
+ const ref = record.ref;
51
+ if (typeof ref !== 'string' || !SESSION_REF_PATTERN.test(ref)) {
52
+ throw new CardValidationError('Blocked-on session ref is invalid');
53
+ }
54
+ return { kind, ref, text };
55
+ }
56
+ if (hasOwn(record, 'ref') && record.ref !== undefined && record.ref !== null) {
57
+ throw new CardValidationError('Blocked-on ref is only allowed when kind is session');
58
+ }
59
+ return { kind, text };
60
+ }
61
+ function parsePatch(patch) {
62
+ if (typeof patch !== 'object' || patch === null || Array.isArray(patch)) {
63
+ throw new CardValidationError('Card update must be an object');
64
+ }
65
+ const record = patch;
66
+ const keys = Object.keys(record).filter((key) => record[key] !== undefined);
67
+ if (keys.length === 0)
68
+ throw new CardValidationError('Empty card update');
69
+ for (const key of keys) {
70
+ if (!ALLOWED_PATCH_KEYS.has(key))
71
+ throw new CardValidationError(`Unknown card field: ${key}`);
72
+ }
73
+ const parsed = {
74
+ hasState: keys.includes('state'),
75
+ hasHeadline: keys.includes('headline'),
76
+ hasProgress: keys.includes('progress'),
77
+ hasBlockedOn: keys.includes('blockedOn'),
78
+ hasNext: keys.includes('next'),
79
+ };
80
+ if (parsed.hasState) {
81
+ const state = CARD_STATES.find((candidate) => candidate === record.state);
82
+ if (!state)
83
+ throw new CardValidationError('Card state is invalid');
84
+ parsed.state = state;
85
+ }
86
+ if (parsed.hasHeadline) {
87
+ parsed.headline = cleanText(record.headline, 'Headline', 1, CARD_LIMITS.headline);
88
+ }
89
+ if (parsed.hasProgress) {
90
+ parsed.progress = record.progress === null ? null : parseProgress(record.progress);
91
+ }
92
+ if (parsed.hasBlockedOn) {
93
+ parsed.blockedOn = record.blockedOn === null ? null : parseBlockedOn(record.blockedOn);
94
+ }
95
+ if (parsed.hasNext) {
96
+ if (record.next === null) {
97
+ parsed.next = null;
98
+ }
99
+ else {
100
+ const cleanedNext = cleanText(record.next, 'Next', 0, CARD_LIMITS.next);
101
+ parsed.next = cleanedNext === '' ? null : cleanedNext;
102
+ }
103
+ }
104
+ return parsed;
105
+ }
106
+ function applyParsed(existing, parsed, updatedAt) {
107
+ if (!existing && (!parsed.hasState || !parsed.hasHeadline)) {
108
+ throw new CardValidationError('First card update must include state and headline');
109
+ }
110
+ let state = existing?.state;
111
+ let headline = existing?.headline;
112
+ let progress = existing?.progress;
113
+ let blockedOn = existing?.blockedOn;
114
+ let next = existing?.next;
115
+ if (parsed.hasState)
116
+ state = parsed.state;
117
+ if (parsed.hasHeadline)
118
+ headline = parsed.headline;
119
+ if (parsed.hasProgress)
120
+ progress = parsed.progress === null ? undefined : parsed.progress;
121
+ if (parsed.hasBlockedOn)
122
+ blockedOn = parsed.blockedOn === null ? undefined : parsed.blockedOn;
123
+ if (parsed.hasNext)
124
+ next = parsed.next === null ? undefined : parsed.next;
125
+ if (state !== 'blocked' && !parsed.hasBlockedOn)
126
+ blockedOn = undefined;
127
+ if (state === undefined || headline === undefined) {
128
+ throw new CardValidationError('First card update must include state and headline');
129
+ }
130
+ const card = { state, headline, updatedAt };
131
+ if (progress !== undefined)
132
+ card.progress = progress;
133
+ if (blockedOn !== undefined)
134
+ card.blockedOn = blockedOn;
135
+ if (next !== undefined)
136
+ card.next = next;
137
+ return card;
138
+ }
139
+ export class CardStore {
140
+ deps;
141
+ constructor(deps) {
142
+ this.deps = deps;
143
+ }
144
+ async get(sessionId) {
145
+ const session = await this.deps.loadSession(sessionId);
146
+ return session?.card ?? null;
147
+ }
148
+ async set(sessionId, patch) {
149
+ const session = await this.deps.loadSession(sessionId);
150
+ if (!session)
151
+ throw new Error('Session not found');
152
+ const parsed = parsePatch(patch);
153
+ const updatedAt = new Date(this.deps.now()).toISOString();
154
+ const fresh = await this.deps.loadSession(sessionId);
155
+ if (!fresh)
156
+ throw new Error('Session not found');
157
+ const previousCard = fresh.card ?? null;
158
+ const card = applyParsed(previousCard, parsed, updatedAt);
159
+ const savedSession = { ...fresh, card };
160
+ await this.deps.saveSession(savedSession);
161
+ await this.notify(savedSession, previousCard);
162
+ return card;
163
+ }
164
+ async clear(sessionId) {
165
+ const session = await this.deps.loadSession(sessionId);
166
+ if (!session || !session.card)
167
+ return;
168
+ const fresh = await this.deps.loadSession(sessionId);
169
+ if (!fresh || !fresh.card)
170
+ return;
171
+ const previousCard = fresh.card;
172
+ const savedSession = { ...fresh };
173
+ delete savedSession.card;
174
+ await this.deps.saveSession(savedSession);
175
+ await this.notify(savedSession, previousCard);
176
+ }
177
+ async notify(session, previous) {
178
+ if (!this.deps.onChange)
179
+ return;
180
+ try {
181
+ await this.deps.onChange(session, previous);
182
+ }
183
+ catch (error) {
184
+ console.warn('CardStore onChange failed', error);
185
+ }
186
+ }
187
+ }
188
+ //# sourceMappingURL=card-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"card-store.js","sourceRoot":"","sources":["../../src/coordination/card-store.ts"],"names":[],"mappings":"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"}
@@ -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
+ }