@undefineds.co/linx 0.3.8 → 0.3.15

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 (39) hide show
  1. package/README.md +17 -0
  2. package/dist/generated/version.js +2 -2
  3. package/dist/generated/version.js.map +1 -1
  4. package/dist/index.js +13 -3
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib/auto-mode/pod-approval.js +216 -2
  7. package/dist/lib/auto-mode/pod-approval.js.map +1 -1
  8. package/dist/lib/linx-status-line.js +335 -0
  9. package/dist/lib/linx-status-line.js.map +1 -0
  10. package/dist/lib/linx-tui-contract.js +3 -3
  11. package/dist/lib/linx-tui-contract.js.map +1 -1
  12. package/dist/lib/models.js +2 -2
  13. package/dist/lib/models.js.map +1 -1
  14. package/dist/lib/pi-adapter/interactive.js +326 -231
  15. package/dist/lib/pi-adapter/interactive.js.map +1 -1
  16. package/dist/lib/pi-adapter/pod-mirror.js +52 -2
  17. package/dist/lib/pi-adapter/pod-mirror.js.map +1 -1
  18. package/dist/lib/pi-adapter/runtime.js +14 -4
  19. package/dist/lib/pi-adapter/runtime.js.map +1 -1
  20. package/dist/lib/pi-adapter/stream.js +24 -1
  21. package/dist/lib/pi-adapter/stream.js.map +1 -1
  22. package/dist/lib/status-line-command.js +108 -0
  23. package/dist/lib/status-line-command.js.map +1 -0
  24. package/dist/lib/symphony/pod-projection.js +357 -17
  25. package/dist/lib/symphony/pod-projection.js.map +1 -1
  26. package/dist/lib/symphony-command.js +20 -21
  27. package/dist/lib/symphony-command.js.map +1 -1
  28. package/dist/skills/symphony/SKILL.md +119 -10
  29. package/dist/skills/xpod-cli/SKILL.md +70 -0
  30. package/package.json +9 -3
  31. package/vendor/agent-runtime/dist/client-inbox-subscription.d.ts +56 -0
  32. package/vendor/agent-runtime/dist/client-inbox-subscription.js +93 -0
  33. package/vendor/agent-runtime/dist/index.d.ts +1 -0
  34. package/vendor/agent-runtime/dist/index.js +1 -0
  35. package/vendor/agent-runtime/dist/reconciler.d.ts +60 -1
  36. package/vendor/agent-runtime/dist/reconciler.js +150 -6
  37. package/vendor/agent-runtime/dist/symphony.js +6 -5
  38. package/vendor/agent-runtime/dist/thread-reconciler-controller.d.ts +2 -1
  39. package/vendor/agent-runtime/dist/thread-reconciler-controller.js +4 -0
@@ -1,11 +1,43 @@
1
1
  import type { AgentParticipantRole } from './turn-controller.js';
2
2
  export type ThreadPolicyKind = 'direct' | 'auto' | 'symphony' | 'open_group' | 'review';
3
3
  export type ThreadKind = 'main' | 'control' | 'worker' | 'review' | 'schedule' | 'schedule_run';
4
- export type ReconcilerEventType = 'message.appended' | 'input.required' | 'approval.required' | 'delivery.submitted' | 'delivery.completed' | 'delivery.failed' | 'schedule.tick' | 'worker.blocked' | 'change.requested' | 'issue.updated' | 'task.updated' | 'run.updated' | (string & {});
4
+ export type ReconcilerEventType = 'message.appended' | 'input.required' | 'approval.required' | 'inbox.notification.created' | 'inbox.notification.updated' | 'delivery.submitted' | 'delivery.completed' | 'delivery.failed' | 'schedule.tick' | 'worker.blocked' | 'change.requested' | 'issue.updated' | 'task.updated' | 'run.updated' | (string & {});
5
5
  export type ReconcilerActorRole = AgentParticipantRole | 'worker' | 'reviewer' | 'runtime' | 'scheduler' | 'tool' | 'assistant';
6
6
  export type WakeJobTargetRole = AgentParticipantRole | 'worker' | 'reviewer';
7
7
  export type WakeJobPriority = 'low' | 'normal' | 'high';
8
8
  export type WakeJobStatus = 'queued';
9
+ export type ReconcilerNotificationAudience = 'user';
10
+ export type ReconcilerNotificationChannel = 'inbox';
11
+ export type ReconcilerClientFocusState = 'focused' | 'background' | 'closed';
12
+ export type ReconcilerControlResourceClaimStatus = 'claimed' | 'lost' | 'display_only' | 'none';
13
+ export interface ReconcilerControlResourceClaimState {
14
+ status: ReconcilerControlResourceClaimStatus;
15
+ controlResource?: string;
16
+ leaseOwner?: string;
17
+ leaseExpiresAt?: string;
18
+ reason?: string;
19
+ }
20
+ export interface ReconcilerClientContext {
21
+ id: string;
22
+ agentCapable?: boolean;
23
+ secretaryRuntimeAvailable?: boolean;
24
+ focusState?: ReconcilerClientFocusState;
25
+ activeThread?: string;
26
+ activeChat?: string;
27
+ generationLocked?: boolean;
28
+ controlResourceClaim?: ReconcilerControlResourceClaimState;
29
+ }
30
+ export interface SecretaryInboxWakeContext {
31
+ eventId: string;
32
+ eventType: ReconcilerEventType;
33
+ controlResource?: string;
34
+ sourceThread?: string;
35
+ sourceRun?: string;
36
+ sourceTask?: string;
37
+ requestKind?: string;
38
+ priority: WakeJobPriority;
39
+ shortSummary?: string;
40
+ }
9
41
  export interface ReconcilerActorRef {
10
42
  id?: string;
11
43
  role?: ReconcilerActorRole;
@@ -60,12 +92,32 @@ export interface WakeJob {
60
92
  sourceEventType: ReconcilerEventType;
61
93
  createdAt: string;
62
94
  }
95
+ export interface ReconcilerNotificationEvent {
96
+ id: string;
97
+ thread: string;
98
+ chat?: string;
99
+ audience: ReconcilerNotificationAudience;
100
+ channel: ReconcilerNotificationChannel;
101
+ priority: WakeJobPriority;
102
+ reason: string;
103
+ sourceEventId?: string;
104
+ sourceEventType: ReconcilerEventType;
105
+ sourceResource?: string;
106
+ inboxNotification?: string;
107
+ sourceThread?: string;
108
+ sourceRun?: string;
109
+ sourceTask?: string;
110
+ requestKind?: string;
111
+ shortSummary?: string;
112
+ createdAt: string;
113
+ }
63
114
  export interface ReconcileDecision {
64
115
  id: string;
65
116
  policyKind: ThreadPolicyKind;
66
117
  event: ThreadControlEvent;
67
118
  placement: ThreadPlacement;
68
119
  wakeJobs: WakeJob[];
120
+ notificationEvents?: ReconcilerNotificationEvent[];
69
121
  skippedReason?: string;
70
122
  createdAt: string;
71
123
  }
@@ -82,6 +134,7 @@ export interface WakeJobSummary {
82
134
  sourceEventId?: string;
83
135
  sourceEventType: ReconcilerEventType;
84
136
  sourceResource?: string;
137
+ inboxNotification?: string;
85
138
  controlGate?: string;
86
139
  }
87
140
  export interface ReconcileDecisionSummary {
@@ -92,6 +145,7 @@ export interface ReconcileDecisionSummary {
92
145
  chat?: string;
93
146
  skippedReason?: string;
94
147
  wakeJobs: WakeJobSummary[];
148
+ notificationEvents?: ReconcilerNotificationEvent[];
95
149
  createdAt: string;
96
150
  }
97
151
  export interface ReconcileThreadEventInput {
@@ -101,6 +155,7 @@ export interface ReconcileThreadEventInput {
101
155
  thread?: string;
102
156
  now?: Date;
103
157
  randomId?: string;
158
+ client?: ReconcilerClientContext;
104
159
  }
105
160
  export interface ThreadReconciler {
106
161
  readonly policy: ThreadPolicy;
@@ -115,3 +170,7 @@ export declare function resolveThreadPlacement(input: {
115
170
  thread?: string;
116
171
  randomId?: string;
117
172
  }): ThreadPlacement;
173
+ export declare function buildSecretaryInboxWakeContext(event: ThreadControlEvent, options?: {
174
+ priority?: WakeJobPriority;
175
+ }): SecretaryInboxWakeContext;
176
+ export declare function canClientScheduleInboxWake(client: ReconcilerClientContext | undefined, event: ThreadControlEvent, now?: Date | string | number): boolean;
@@ -24,14 +24,16 @@ export function reconcileThreadEvent(input) {
24
24
  thread: input.thread,
25
25
  randomId: input.randomId,
26
26
  });
27
- const jobs = selectWakeJobs(policy, event, placement, createdAt, input.randomId);
28
- const skippedReason = jobs.length === 0 ? skipReasonFor(policy, event) : undefined;
27
+ const jobs = selectWakeJobs(policy, event, placement, createdAt, input.randomId, input.client);
28
+ const notificationEvents = selectNotificationEvents(event, placement, createdAt, input.randomId);
29
+ const skippedReason = jobs.length === 0 ? skipReasonFor(policy, event, input.client) : undefined;
29
30
  return {
30
31
  id: createReconcilerId('decision', input.randomId),
31
32
  policyKind: policy.kind,
32
33
  event,
33
34
  placement,
34
35
  wakeJobs: jobs,
36
+ ...(notificationEvents.length > 0 ? { notificationEvents } : {}),
35
37
  ...(skippedReason ? { skippedReason } : {}),
36
38
  createdAt,
37
39
  };
@@ -57,8 +59,12 @@ export function summarizeReconcileDecision(decision) {
57
59
  ...(job.sourceEventId ? { sourceEventId: job.sourceEventId } : {}),
58
60
  sourceEventType: job.sourceEventType,
59
61
  ...(decision.event.resource ? { sourceResource: decision.event.resource } : {}),
62
+ ...(normalizeText(decision.event.data?.inboxNotification) ? { inboxNotification: normalizeText(decision.event.data?.inboxNotification) } : {}),
60
63
  ...(resolveControlGate(decision.event) ? { controlGate: resolveControlGate(decision.event) } : {}),
61
64
  })),
65
+ ...(decision.notificationEvents && decision.notificationEvents.length > 0
66
+ ? { notificationEvents: decision.notificationEvents.map((event) => ({ ...event })) }
67
+ : {}),
62
68
  createdAt: decision.createdAt,
63
69
  };
64
70
  }
@@ -114,7 +120,7 @@ export function resolveThreadPlacement(input) {
114
120
  kind: 'control',
115
121
  };
116
122
  }
117
- function selectWakeJobs(policy, event, placement, createdAt, randomId) {
123
+ function selectWakeJobs(policy, event, placement, createdAt, randomId, client) {
118
124
  if (policy.kind === 'direct') {
119
125
  return isUserMessage(event)
120
126
  ? [createWakeJob(policy, event, placement, {
@@ -128,6 +134,11 @@ function selectWakeJobs(policy, event, placement, createdAt, randomId) {
128
134
  : [];
129
135
  }
130
136
  if (policy.kind === 'auto') {
137
+ if (isActionableInboxEvent(event)) {
138
+ return canClientScheduleInboxWake(client, event, createdAt)
139
+ ? [createSecretaryWakeJob(policy, event, placement, 'Auto mode claimed an actionable control resource on this client and schedules the same-Thread Secretary to check Inbox.', 'high', createdAt, randomId)]
140
+ : [];
141
+ }
131
142
  if (isInputApprovalOrBlocker(event)) {
132
143
  return [createSecretaryWakeJob(policy, event, placement, 'Auto mode routes input, approval, and blocker events to the same-Thread Secretary.', 'high', createdAt, randomId)];
133
144
  }
@@ -140,6 +151,11 @@ function selectWakeJobs(policy, event, placement, createdAt, randomId) {
140
151
  return [];
141
152
  }
142
153
  if (policy.kind === 'symphony') {
154
+ if (isActionableInboxEvent(event)) {
155
+ return canClientScheduleInboxWake(client, event, createdAt)
156
+ ? [createSecretaryWakeJob(policy, event, placement, 'Symphony claimed an actionable control resource on this client and schedules Secretary to check Inbox; the payload is runtime control context, not a human user message.', 'high', createdAt, randomId)]
157
+ : [];
158
+ }
143
159
  if (isInputApprovalOrBlocker(event) || event.type === 'change.requested') {
144
160
  return [createSecretaryWakeJob(policy, event, placement, 'Symphony routes blocker, input, approval, and change requests to Secretary for semantic judgment.', 'high', createdAt, randomId)];
145
161
  }
@@ -157,10 +173,10 @@ function selectWakeJobs(policy, event, placement, createdAt, randomId) {
157
173
  return [createSecretaryWakeJob(policy, event, placement, 'Symphony Delivery submissions wake Secretary for review or routing.', 'normal', createdAt, randomId)];
158
174
  }
159
175
  if (event.type === 'delivery.completed') {
160
- return [createSecretaryWakeJob(policy, event, placement, 'Symphony completion Delivery wakes Secretary for quality and acceptance reconciliation.', 'high', createdAt, randomId)];
176
+ return [createSecretaryWakeJob(policy, event, placement, 'Symphony completion Delivery wakes Secretary for quality, acceptance, and follow-up extraction reconciliation.', 'high', createdAt, randomId)];
161
177
  }
162
178
  if (event.type === 'delivery.failed') {
163
- return [createSecretaryWakeJob(policy, event, placement, 'Symphony failed Delivery wakes Secretary for feasibility, retry, or scope change reconciliation.', 'high', createdAt, randomId)];
179
+ return [createSecretaryWakeJob(policy, event, placement, 'Symphony failed Delivery wakes Secretary for feasibility, retry, scope change, or follow-up extraction reconciliation.', 'high', createdAt, randomId)];
164
180
  }
165
181
  if (event.type === 'issue.updated' || event.type === 'task.updated' || event.type === 'run.updated') {
166
182
  return [createSecretaryWakeJob(policy, event, placement, 'Symphony state changes wake Secretary to reconcile system state.', 'normal', createdAt, randomId)];
@@ -197,6 +213,92 @@ function selectWakeJobs(policy, event, placement, createdAt, randomId) {
197
213
  }
198
214
  return [];
199
215
  }
216
+ function selectNotificationEvents(event, placement, createdAt, randomId) {
217
+ if (!isActionableInboxEvent(event)) {
218
+ return [];
219
+ }
220
+ const wakeContext = buildSecretaryInboxWakeContext(event, { priority: 'high' });
221
+ return [{
222
+ id: createReconcilerId('notice', randomId ?? event.id),
223
+ thread: placement.thread,
224
+ ...(placement.chat ? { chat: placement.chat } : {}),
225
+ audience: 'user',
226
+ channel: 'inbox',
227
+ priority: 'high',
228
+ reason: 'Inbox envelope changed; notify subscribed clients to refresh Inbox and read the linked control resource without converting it into chat.',
229
+ ...(event.id ? { sourceEventId: event.id } : {}),
230
+ sourceEventType: event.type,
231
+ ...(event.resource ? { sourceResource: event.resource } : {}),
232
+ ...(normalizeText(event.data?.inboxNotification) ? { inboxNotification: normalizeText(event.data?.inboxNotification) } : {}),
233
+ ...(wakeContext.sourceThread ? { sourceThread: wakeContext.sourceThread } : {}),
234
+ ...(wakeContext.sourceRun ? { sourceRun: wakeContext.sourceRun } : {}),
235
+ ...(wakeContext.sourceTask ? { sourceTask: wakeContext.sourceTask } : {}),
236
+ ...(wakeContext.requestKind ? { requestKind: wakeContext.requestKind } : {}),
237
+ ...(wakeContext.shortSummary ? { shortSummary: wakeContext.shortSummary } : {}),
238
+ createdAt,
239
+ }];
240
+ }
241
+ export function buildSecretaryInboxWakeContext(event, options = {}) {
242
+ const controlResource = normalizeText(event.resource)
243
+ ?? normalizeText(event.data?.controlResource)
244
+ ?? normalizeText(event.data?.controlResourceId)
245
+ ?? normalizeText(event.data?.approval)
246
+ ?? normalizeText(event.data?.inputRequest);
247
+ const requestKind = normalizeText(event.data?.requestKind)
248
+ ?? normalizeText(event.data?.kind)
249
+ ?? normalizeText(event.data?.type);
250
+ const sourceThread = normalizeText(event.data?.sourceThread);
251
+ const sourceRun = normalizeText(event.data?.sourceRun);
252
+ const sourceTask = normalizeText(event.data?.sourceTask);
253
+ const shortSummary = normalizeText(event.data?.shortSummary)
254
+ ?? normalizeText(event.data?.summary)
255
+ ?? normalizeText(event.content);
256
+ return {
257
+ eventId: event.id ?? createReconcilerId('event', controlResource ?? requestKind),
258
+ eventType: event.type,
259
+ ...(controlResource ? { controlResource } : {}),
260
+ ...(sourceThread ? { sourceThread } : {}),
261
+ ...(sourceRun ? { sourceRun } : {}),
262
+ ...(sourceTask ? { sourceTask } : {}),
263
+ ...(requestKind ? { requestKind } : {}),
264
+ priority: options.priority ?? 'normal',
265
+ ...(shortSummary ? { shortSummary } : {}),
266
+ };
267
+ }
268
+ export function canClientScheduleInboxWake(client, event, now = Date.now()) {
269
+ if (!client || !isActionableInboxEvent(event)) {
270
+ return false;
271
+ }
272
+ if (client.agentCapable !== true) {
273
+ return false;
274
+ }
275
+ if (client.secretaryRuntimeAvailable === false) {
276
+ return false;
277
+ }
278
+ const claim = client.controlResourceClaim;
279
+ if (!claim || claim.status !== 'claimed') {
280
+ return false;
281
+ }
282
+ const eventControlResource = normalizeText(event.resource)
283
+ ?? normalizeText(event.data?.controlResource)
284
+ ?? normalizeText(event.data?.controlResourceId);
285
+ const claimControlResource = normalizeText(claim.controlResource);
286
+ if (eventControlResource && claimControlResource && eventControlResource !== claimControlResource) {
287
+ return false;
288
+ }
289
+ const leaseOwner = normalizeText(claim.leaseOwner);
290
+ if (leaseOwner && leaseOwner !== client.id) {
291
+ return false;
292
+ }
293
+ if (claim.leaseExpiresAt) {
294
+ const expiresAt = Date.parse(claim.leaseExpiresAt);
295
+ const nowMs = typeof now === 'number' ? now : Date.parse(now instanceof Date ? now.toISOString() : now);
296
+ if (!Number.isNaN(expiresAt) && !Number.isNaN(nowMs) && expiresAt <= nowMs) {
297
+ return false;
298
+ }
299
+ }
300
+ return true;
301
+ }
200
302
  function createSecretaryWakeJob(policy, event, placement, reason, priority, createdAt, randomId) {
201
303
  return createWakeJob(policy, event, placement, {
202
304
  targetAgent: policy.secretaryAgent ?? DEFAULT_SECRETARY_AGENT,
@@ -245,12 +347,34 @@ function threadKindFromEvent(event) {
245
347
  if (event.type === 'issue.updated' || event.type === 'task.updated' || event.type === 'run.updated') {
246
348
  return 'control';
247
349
  }
350
+ if (isInboxEvent(event)) {
351
+ return 'control';
352
+ }
248
353
  return 'main';
249
354
  }
250
355
  function isInputApprovalOrBlocker(event) {
251
356
  return event.type === 'input.required' || event.type === 'approval.required' || event.type === 'worker.blocked';
252
357
  }
253
358
  function resolveControlGate(event) {
359
+ if (isInboxEvent(event)) {
360
+ const explicitGate = normalizeText(event.data?.controlGate);
361
+ if (explicitGate) {
362
+ return explicitGate;
363
+ }
364
+ const requestKind = normalizeText(event.data?.requestKind)
365
+ ?? normalizeText(event.data?.kind)
366
+ ?? normalizeText(event.data?.type);
367
+ if (requestKind && /approval|auth|credential|grant|permission|destructive/iu.test(requestKind)) {
368
+ return 'authority';
369
+ }
370
+ if (requestKind && /block|fail|feasibil/iu.test(requestKind)) {
371
+ return 'feasibility';
372
+ }
373
+ if (requestKind && /change|scope|rebase|steer/iu.test(requestKind)) {
374
+ return 'change';
375
+ }
376
+ return 'binding';
377
+ }
254
378
  if (event.type === 'input.required' || event.type === 'approval.required') {
255
379
  return 'authority';
256
380
  }
@@ -290,6 +414,19 @@ function isPrimaryAgentMessage(event) {
290
414
  || event.data?.role === 'assistant'
291
415
  || event.data?.source === 'primary-agent');
292
416
  }
417
+ function isInboxEvent(event) {
418
+ return event.type === 'inbox.notification.created' || event.type === 'inbox.notification.updated';
419
+ }
420
+ function isActionableInboxEvent(event) {
421
+ if (!isInboxEvent(event)) {
422
+ return false;
423
+ }
424
+ const status = normalizeText(event.data?.status);
425
+ if (!status) {
426
+ return event.type === 'inbox.notification.created';
427
+ }
428
+ return status === 'pending' || status === 'handling';
429
+ }
293
430
  function isTaskDispatchDelivery(event) {
294
431
  return event.data?.deliveryType === 'task_dispatch'
295
432
  || event.data?.type === 'task_dispatch'
@@ -328,10 +465,17 @@ function isMentioned(content, agent) {
328
465
  .filter(Boolean)
329
466
  .some((name) => normalized.includes(`@${name.toLowerCase()}`));
330
467
  }
331
- function skipReasonFor(policy, event) {
468
+ function skipReasonFor(policy, event, client) {
332
469
  if (policy.kind === 'open_group') {
333
470
  return 'No mentioned or subscribed agents matched the event.';
334
471
  }
472
+ if (isActionableInboxEvent(event) && (policy.kind === 'auto' || policy.kind === 'symphony')) {
473
+ if (!client) {
474
+ return `Policy ${policy.kind} keeps ${event.type} display-only until a subscribed client claims the control resource.`;
475
+ }
476
+ const claimStatus = client.controlResourceClaim?.status ?? 'none';
477
+ return `Policy ${policy.kind} keeps ${event.type} display-only for client ${client.id} because control resource claim status is ${claimStatus}.`;
478
+ }
335
479
  return `Policy ${policy.kind} does not wake an agent for ${event.type}.`;
336
480
  }
337
481
  function createSyntheticThreadUri(kind, value) {
@@ -245,10 +245,10 @@ export function renderSymphonyRuntimePrompt(input) {
245
245
  ...(input.workspace.environment ? [`Workspace environment: ${formatSymphonyWorkerEnvironment(input.workspace.environment)}`] : []),
246
246
  '',
247
247
  '## Runtime Space Contract',
248
- '- Shared control space: Issue, Task, Delivery, Session, Run, and Evidence URIs are the common coordination surface with AI Secretary and product UI.',
248
+ '- Shared control space: Idea/Issue/Report/Evidence are file-primary Pod resources with structured meta; Task, Delivery, Session, Run, and RunStep are TTL control resources. Use the provided URIs as the common coordination surface with AI Secretary and product UI.',
249
249
  '- Explicit session topology: you may be collaborating in the same room as Secretary or running in a runtime-projected worker session reached through control events. Follow the provided chat/thread/session targets; do not infer topology from workspace sharing.',
250
250
  '- Thread reconciliation: messages, input/approval requests, blockers, schedule ticks, and Delivery submissions enter the Thread first; the Reconciler/Scheduler wakes Secretary or workers.',
251
- '- Report through Delivery/Evidence: return progress, blockers, implementation change requests, and verification for AI Secretary to persist or route.',
251
+ '- Report through Delivery plus file-primary Report/Evidence: return progress, blockers, implementation change requests, and verification so AI Secretary can persist structured control facts and Pod files without inlining long logs into TTL.',
252
252
  '- Thread workspace: workers assigned to the same Thread in the same environment should normally share this workspace; independent Threads may use separate worktrees.',
253
253
  '- Environment-scoped identity: cross-environment file identity requires revision, artifact, patch, checksum, or evidence references.',
254
254
  '',
@@ -266,20 +266,21 @@ export function renderSymphonyRuntimePrompt(input) {
266
266
  '- Report blockers to AI Secretary instead of asking the user directly.',
267
267
  '- Do not read sibling worker transcripts unless Secretary explicitly includes them in a Delivery.',
268
268
  '- Preserve a concise report with changed files, commands run, and remaining risks.',
269
+ '- In the final report, explicitly list follow-up candidates separately from assigned-work evidence: new defects, missing shared abstractions, app-local glue to move into shared models, storage, or adapter packages, live verification gaps, or deferred cleanup. Secretary classifies these; do not create or close Issues yourself.',
269
270
  '- If blocked by missing credentials, destructive actions, or unclear scope, report the blocker instead of guessing.',
270
271
  '- Your workspace path is local to this worker environment. Same-Thread workers in this environment may share it, but do not assume Secretary, the user, or workers in other environments can access the same absolute path.',
271
272
  '- When reporting file work across environments, include repo-relative paths plus base revision, checksums/etags, patch or artifact references, and verification evidence.',
272
273
  '',
273
274
  '## Pod And Control Record Boundary',
274
275
  '- In LinX runtime, Pod control records are authoritative. Local files are mirrors, logs, or portable-runtime fallbacks.',
275
- '- If Pod/model tools are available, read only the assigned Issue, Task, Delivery, Run, source context, and existing evidence needed for this task.',
276
- '- Write only execution facts for the assigned work: Run/RunStep progress, blockers, Evidence, Delivery report, or Implementation Change Request.',
276
+ '- If Pod/model tools are available, read only the assigned Issue document/meta, Task, Delivery, Run, source context, and existing Report/Evidence files needed for this task.',
277
+ '- Write only execution facts for the assigned work: Run/RunStep progress, blockers, file-primary Evidence/Report, Delivery report metadata, or Implementation Change Request.',
277
278
  '- Do not close Issues, rewrite Spec/current truth, change acceptance criteria, change work split, alter release or roadmap state, create grants, or mutate sibling worker state.',
278
279
  '- Use shared model/ORM surfaces when writing structured Pod data. Do not hand-patch business TTL or invent Pod paths.',
279
280
  '- If Pod access is unavailable, return the same facts as a structured report so AI Secretary can persist them.',
280
281
  '',
281
282
  '## Documentation Authority',
282
- '- Pod Issue/Spec/Task records are the control authority for status, scope, acceptance, split, ownership, closure, and cross-client coordination.',
283
+ '- Pod Issue files plus meta, Spec files, and Task control records are the authority for status, scope, acceptance, split, ownership, closure, and cross-client coordination.',
283
284
  '- Repository docs are the implementation authority for code-adjacent design, behavior notes, tests, examples, migration details, and file-level evidence.',
284
285
  '- When you edit repository docs, reference the Pod Issue/Spec/Task URI instead of creating a second Issue truth.',
285
286
  '- If repository findings contradict the Pod control record, write an Implementation Change Request instead of silently changing acceptance or scope.',
@@ -1,4 +1,4 @@
1
- import { type ReconcileDecision, type ReconcileDecisionSummary, type ReconcileThreadEventInput, type ThreadControlEvent, type ThreadPolicy, type ThreadPolicyKind, type WakeJob } from './reconciler.js';
1
+ import { type ReconcileDecision, type ReconcileDecisionSummary, type ReconcileThreadEventInput, type ReconcilerNotificationEvent, type ThreadControlEvent, type ThreadPolicy, type ThreadPolicyKind, type WakeJob } from './reconciler.js';
2
2
  import { type WakeJobExecutionRecord, type WakeJobExecutionRecordSummary, type WakeJobSchedulerSnapshot, type WakeJobSchedulerSnapshotSummary } from './wake-scheduler.js';
3
3
  type MaybePromise<T> = T | Promise<T>;
4
4
  export interface ThreadWakeJobContext {
@@ -19,6 +19,7 @@ export interface ThreadReconcilerControllerOptions {
19
19
  onWakeJobStarted?: (record: WakeJobExecutionRecord, decision: ReconcileDecisionSummary) => void;
20
20
  onWakeJobCompleted?: (record: WakeJobExecutionRecord, decision: ReconcileDecisionSummary) => void;
21
21
  onWakeJobFailed?: (record: WakeJobExecutionRecord, decision: ReconcileDecisionSummary) => void;
22
+ onNotificationEvent?: (event: ReconcilerNotificationEvent, decision: ReconcileDecisionSummary) => void;
22
23
  }
23
24
  export interface ThreadReconcilerDispatchOptions extends Omit<ReconcileThreadEventInput, 'policy' | 'event'> {
24
25
  }
@@ -58,10 +58,14 @@ class ConfiguredThreadReconcilerController {
58
58
  dispatch(event, options = {}) {
59
59
  const { decision, summary } = decideThreadControlEvent({
60
60
  ...options,
61
+ now: options.now ?? this.options.now?.(),
61
62
  policy: this.options.policy,
62
63
  event,
63
64
  });
64
65
  this.options.onDecision?.(summary);
66
+ for (const notificationEvent of decision.notificationEvents ?? []) {
67
+ this.options.onNotificationEvent?.(notificationEvent, summary);
68
+ }
65
69
  for (const job of decision.wakeJobs) {
66
70
  const key = defaultWakeJobDedupeKey(job);
67
71
  if (!this.contexts.has(key)) {