@undefineds.co/linx 0.3.20 → 0.3.23

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 (99) hide show
  1. package/dist/generated/version.js +1 -1
  2. package/dist/index.js +6 -1
  3. package/dist/index.js.map +1 -1
  4. package/dist/lib/auto-mode/pod-persistence.js +53 -3
  5. package/dist/lib/auto-mode/pod-persistence.js.map +1 -1
  6. package/dist/lib/auto-mode/secretary.js +2 -2
  7. package/dist/lib/auto-mode/secretary.js.map +1 -1
  8. package/dist/lib/chat-api.js +23 -61
  9. package/dist/lib/chat-api.js.map +1 -1
  10. package/dist/lib/codex-plugin/index.js +1 -0
  11. package/dist/lib/codex-plugin/index.js.map +1 -1
  12. package/dist/lib/codex-plugin/symphony-mcp.js +335 -0
  13. package/dist/lib/codex-plugin/symphony-mcp.js.map +1 -0
  14. package/dist/lib/linx-cloud-errors.js +0 -5
  15. package/dist/lib/linx-cloud-errors.js.map +1 -1
  16. package/dist/lib/linx-status-line.js +1 -8
  17. package/dist/lib/linx-status-line.js.map +1 -1
  18. package/dist/lib/linx-tui-contract.js +2 -1
  19. package/dist/lib/linx-tui-contract.js.map +1 -1
  20. package/dist/lib/models.js +3 -2
  21. package/dist/lib/models.js.map +1 -1
  22. package/dist/lib/pi-adapter/auth.js +68 -0
  23. package/dist/lib/pi-adapter/auth.js.map +1 -0
  24. package/dist/lib/pi-adapter/branding.js +67 -110
  25. package/dist/lib/pi-adapter/branding.js.map +1 -1
  26. package/dist/lib/pi-adapter/interactive.js +341 -101
  27. package/dist/lib/pi-adapter/interactive.js.map +1 -1
  28. package/dist/lib/pi-adapter/pod-mirror.js +38 -107
  29. package/dist/lib/pi-adapter/pod-mirror.js.map +1 -1
  30. package/dist/lib/pi-adapter/pod-native.js +2 -0
  31. package/dist/lib/pi-adapter/pod-native.js.map +1 -1
  32. package/dist/lib/pi-adapter/pod-tools.js +140 -0
  33. package/dist/lib/pi-adapter/pod-tools.js.map +1 -0
  34. package/dist/lib/pi-adapter/runtime.js +2 -12
  35. package/dist/lib/pi-adapter/runtime.js.map +1 -1
  36. package/dist/lib/pi-adapter/session.js +13 -17
  37. package/dist/lib/pi-adapter/session.js.map +1 -1
  38. package/dist/lib/pi-adapter/stream.js +2 -20
  39. package/dist/lib/pi-adapter/stream.js.map +1 -1
  40. package/dist/lib/pod-chat-store.js +53 -4
  41. package/dist/lib/pod-chat-store.js.map +1 -1
  42. package/dist/lib/resource-identity.js +2 -0
  43. package/dist/lib/resource-identity.js.map +1 -0
  44. package/dist/lib/status-line-command.js +2 -2
  45. package/dist/lib/status-line-command.js.map +1 -1
  46. package/dist/lib/symphony/archive.js +15 -37
  47. package/dist/lib/symphony/archive.js.map +1 -1
  48. package/dist/lib/symphony/pod-projection.js +189 -1346
  49. package/dist/lib/symphony/pod-projection.js.map +1 -1
  50. package/dist/lib/symphony-command.js +209 -109
  51. package/dist/lib/symphony-command.js.map +1 -1
  52. package/dist/plugins/linx-symphony-codex/.codex-plugin/plugin.json +38 -0
  53. package/dist/plugins/linx-symphony-codex/.mcp.json +10 -0
  54. package/dist/plugins/linx-symphony-codex/README.md +9 -0
  55. package/dist/plugins/linx-symphony-codex/hooks.json +60 -0
  56. package/dist/plugins/linx-symphony-codex/scripts/symphony-hook-events.mjs +119 -0
  57. package/dist/plugins/linx-symphony-codex/scripts/symphony-mcp.mjs +335 -0
  58. package/dist/plugins/linx-symphony-codex/skills/symphony/SKILL.md +791 -0
  59. package/dist/skills/symphony/SKILL.md +7 -0
  60. package/dist/skills/xpod-cli/SKILL.md +2 -13
  61. package/package.json +4 -4
  62. package/vendor/agent-runtime/dist/chat-reconciler.d.ts +33 -0
  63. package/vendor/agent-runtime/dist/chat-reconciler.js +108 -0
  64. package/vendor/agent-runtime/dist/index.d.ts +4 -1
  65. package/vendor/agent-runtime/dist/index.js +4 -1
  66. package/vendor/agent-runtime/dist/matrix-client.d.ts +149 -0
  67. package/vendor/agent-runtime/dist/matrix-client.js +220 -0
  68. package/vendor/agent-runtime/dist/pod-resource-identity.d.ts +17 -0
  69. package/vendor/agent-runtime/dist/pod-resource-identity.js +54 -0
  70. package/vendor/agent-runtime/dist/reconciler.d.ts +0 -11
  71. package/vendor/agent-runtime/dist/reconciler.js +5 -43
  72. package/vendor/agent-runtime/dist/symphony.d.ts +272 -27
  73. package/vendor/agent-runtime/dist/symphony.js +1268 -21
  74. package/vendor/agent-runtime/dist/workspace.d.ts +61 -0
  75. package/vendor/agent-runtime/dist/workspace.js +81 -0
  76. package/vendor/agent-runtime/package.json +5 -1
  77. package/vendor/stores/dist/current-pod-base.d.ts +2 -0
  78. package/vendor/stores/dist/current-pod-base.js +14 -0
  79. package/vendor/stores/dist/exact-records.d.ts +7 -0
  80. package/vendor/stores/dist/exact-records.js +87 -0
  81. package/vendor/stores/dist/index.d.ts +1 -0
  82. package/vendor/stores/dist/index.js +1 -0
  83. package/vendor/stores/dist/login.d.ts +51 -0
  84. package/vendor/stores/dist/login.js +195 -0
  85. package/vendor/stores/dist/pod-collection.d.ts +28 -0
  86. package/vendor/stores/dist/pod-collection.js +194 -0
  87. package/vendor/stores/dist/pod-write-guard.d.ts +5 -0
  88. package/vendor/stores/dist/pod-write-guard.js +133 -0
  89. package/vendor/stores/dist/symphony-control.d.ts +245 -0
  90. package/vendor/stores/dist/symphony-control.js +2175 -0
  91. package/vendor/stores/package.json +14 -0
  92. package/dist/lib/capture/persistence.js +0 -377
  93. package/dist/lib/capture/persistence.js.map +0 -1
  94. package/dist/lib/capture/tool.js +0 -242
  95. package/dist/lib/capture/tool.js.map +0 -1
  96. package/dist/skills/basic/SKILL.md +0 -46
  97. package/dist/skills/capture/SKILL.md +0 -165
  98. package/vendor/agent-runtime/dist/coordination.d.ts +0 -93
  99. package/vendor/agent-runtime/dist/coordination.js +0 -145
@@ -1,4 +1,4 @@
1
- import { summarizeReconcileDecision } from './reconciler.js';
1
+ import { summarizeReconcileDecision, } from './reconciler.js';
2
2
  import { decideThreadControlEvent } from './thread-reconciler-controller.js';
3
3
  export const SYMPHONY_HOME_DIRNAME = 'symphony';
4
4
  export const SYMPHONY_IDEAS_DIRNAME = 'ideas';
@@ -6,11 +6,13 @@ export const SYMPHONY_ISSUES_DIRNAME = 'issues';
6
6
  export const SYMPHONY_TASKS_DIRNAME = 'tasks';
7
7
  export const SYMPHONY_DELIVERIES_DIRNAME = 'deliveries';
8
8
  export const SYMPHONY_SESSIONS_DIRNAME = 'sessions';
9
+ export const SYMPHONY_RUN_STEPS_DIRNAME = 'run-steps';
9
10
  export const SYMPHONY_IDEA_FILE_NAME = 'idea.json';
10
11
  export const SYMPHONY_ISSUE_FILE_NAME = 'issue.json';
11
12
  export const SYMPHONY_TASK_FILE_NAME = 'task.json';
12
13
  export const SYMPHONY_DELIVERY_FILE_NAME = 'delivery.json';
13
14
  export const SYMPHONY_SESSION_FILE_NAME = 'session.json';
15
+ export const SYMPHONY_RUN_STEP_FILE_NAME = 'run-step.json';
14
16
  const SYMPHONY_URI_PREFIX = 'urn:undefineds:linx';
15
17
  export function createSymphonyIdeaUri(options = {}) {
16
18
  return createSymphonyResourceUri('idea', options);
@@ -27,6 +29,9 @@ export function createSymphonyDeliveryUri(options = {}) {
27
29
  export function createSymphonySessionUri(options = {}) {
28
30
  return createSymphonyResourceUri('session', options);
29
31
  }
32
+ export function createSymphonyRunStepUri(options = {}) {
33
+ return createSymphonyResourceUri('runStep', options);
34
+ }
30
35
  export function getSymphonyArchiveRelativePaths(uri, kind) {
31
36
  const key = getSymphonyArchiveKey(uri);
32
37
  const dirNameByKind = {
@@ -35,6 +40,7 @@ export function getSymphonyArchiveRelativePaths(uri, kind) {
35
40
  task: SYMPHONY_TASKS_DIRNAME,
36
41
  delivery: SYMPHONY_DELIVERIES_DIRNAME,
37
42
  session: SYMPHONY_SESSIONS_DIRNAME,
43
+ runStep: SYMPHONY_RUN_STEPS_DIRNAME,
38
44
  };
39
45
  const fileNameByKind = {
40
46
  idea: SYMPHONY_IDEA_FILE_NAME,
@@ -42,6 +48,7 @@ export function getSymphonyArchiveRelativePaths(uri, kind) {
42
48
  task: SYMPHONY_TASK_FILE_NAME,
43
49
  delivery: SYMPHONY_DELIVERY_FILE_NAME,
44
50
  session: SYMPHONY_SESSION_FILE_NAME,
51
+ runStep: SYMPHONY_RUN_STEP_FILE_NAME,
45
52
  };
46
53
  const dirName = dirNameByKind[kind];
47
54
  const fileName = fileNameByKind[kind];
@@ -57,6 +64,7 @@ export function createRunPlan(input) {
57
64
  const uriOptions = { now, randomId };
58
65
  const objective = normalizeRequiredText(input.objective, 'objective');
59
66
  const title = normalizeOptionalText(input.title) ?? createSymphonyTitle(objective);
67
+ const source = input.source ?? 'cli';
60
68
  const acceptanceCriteria = normalizeSymphonyAcceptanceCriteria(input.acceptanceCriteria);
61
69
  const issuer = normalizeSymphonyIssuer(input);
62
70
  const workerSpecs = normalizeSymphonyWorkerSpecs(input);
@@ -72,7 +80,7 @@ export function createRunPlan(input) {
72
80
  ...(normalizeOptionalText(input.repository) ? { repository: normalizeOptionalText(input.repository) } : {}),
73
81
  ...(normalizeOptionalText(input.branch) ? { branch: normalizeOptionalText(input.branch) } : {}),
74
82
  ...(normalizeOptionalText(input.worktree) ? { worktree: normalizeOptionalText(input.worktree) } : {}),
75
- ...(normalizeOptionalText(input.workspace) ? { workspace: normalizeOptionalText(input.workspace) } : {}),
83
+ ...(normalizeOptionalText(input.container) ? { container: normalizeOptionalText(input.container) } : {}),
76
84
  ...(normalizeOptionalText(input.baseRevision) ? { baseRevision: normalizeOptionalText(input.baseRevision) } : {}),
77
85
  environment: normalizeSymphonyWorkerEnvironment(input.environment, input.backend),
78
86
  };
@@ -84,7 +92,7 @@ export function createRunPlan(input) {
84
92
  description: objective,
85
93
  status: 'open',
86
94
  priority: 'medium',
87
- source: 'cli',
95
+ source,
88
96
  issuer,
89
97
  tasks: workerUris.map((item) => item.task),
90
98
  deliveries: workerUris.map((item) => item.delivery),
@@ -107,7 +115,7 @@ export function createRunPlan(input) {
107
115
  thread: target.thread ?? chatThread.thread,
108
116
  messages: target.messages ?? chatThread.messages,
109
117
  });
110
- const targetAgent = target.agent ?? target.contact ?? target.backend;
118
+ const targetAgent = target.agent ?? `${target.backend}-worker`;
111
119
  const dispatchReconciler = createSymphonyDispatchReconcilerState({
112
120
  issue: issueUri,
113
121
  task: uris.task,
@@ -146,7 +154,7 @@ export function createRunPlan(input) {
146
154
  ...(input.secretaryAutoEnabled !== undefined ? { secretaryAutoEnabled: input.secretaryAutoEnabled } : {}),
147
155
  status: 'planned',
148
156
  cwd: workerWorkspace.path,
149
- workspaceRef: workerWorkspace,
157
+ workspace: workerWorkspace,
150
158
  target,
151
159
  ...(spec.model ? { model: spec.model } : {}),
152
160
  ...(spec.supervisor ? { supervisor: spec.supervisor } : {}),
@@ -208,6 +216,592 @@ export function appendSymphonyReconcilerDecision(record, decision) {
208
216
  },
209
217
  };
210
218
  }
219
+ export function reconcileSymphonyThreadEvents(input) {
220
+ const policy = input.policy ?? { kind: 'symphony', secretaryAgent: '__secretary__' };
221
+ const events = (input.events ?? []).map((event, index) => normalizeSymphonyThreadReconcileEvent(event, input, index));
222
+ const now = input.now instanceof Date
223
+ ? input.now
224
+ : normalizeOptionalText(input.now)
225
+ ? new Date(normalizeOptionalText(input.now))
226
+ : undefined;
227
+ const decisions = events.map((event, index) => decideThreadControlEvent({
228
+ policy,
229
+ event,
230
+ ...(input.chat ? { chat: input.chat } : {}),
231
+ ...(input.thread ? { thread: input.thread } : {}),
232
+ ...(now && Number.isFinite(now.getTime()) ? { now } : {}),
233
+ randomId: `${input.randomId ?? event.id ?? 'symphony-thread'}-${index + 1}`,
234
+ ...(input.client ? { client: input.client } : {}),
235
+ }).summary);
236
+ const wakeJobs = decisions.flatMap((decision) => decision.wakeJobs);
237
+ const notificationEvents = decisions.flatMap((decision) => decision.notificationEvents ?? []);
238
+ const latestDecision = decisions.length > 0 ? decisions[decisions.length - 1] : undefined;
239
+ const latestEvent = events.length > 0 ? events[events.length - 1] : undefined;
240
+ const thread = latestDecision?.thread
241
+ ?? normalizeOptionalText(input.thread)
242
+ ?? latestEvent?.thread
243
+ ?? 'urn:undefineds:linx:thread:system-control';
244
+ const chat = latestDecision?.chat
245
+ ?? normalizeOptionalText(input.chat)
246
+ ?? latestEvent?.chat;
247
+ const nextAction = resolveSymphonyThreadReconcilerNextAction(wakeJobs, notificationEvents);
248
+ return {
249
+ policyKind: typeof policy === 'string' ? policy : policy.kind,
250
+ ...(chat ? { chat } : {}),
251
+ thread,
252
+ eventCount: events.length,
253
+ decisions,
254
+ wakeJobs,
255
+ notificationEvents,
256
+ nextAction,
257
+ summary: summarizeSymphonyThreadReconcilerResult(events.length, nextAction, wakeJobs.length, notificationEvents.length),
258
+ };
259
+ }
260
+ export function withSymphonyIssueStatus(record, status, updates = {}) {
261
+ const timestamp = normalizeSymphonyStatusTimestamp(updates.now);
262
+ return {
263
+ ...record,
264
+ status,
265
+ updatedAt: timestamp,
266
+ ...(updates.error ? { error: updates.error } : {}),
267
+ ...((updates.closedAt || status === 'resolved' || status === 'closed') ? { closedAt: updates.closedAt ?? timestamp } : {}),
268
+ };
269
+ }
270
+ export function withSymphonyTaskStatus(record, status, updates = {}) {
271
+ const timestamp = normalizeSymphonyStatusTimestamp(updates.now);
272
+ return {
273
+ ...record,
274
+ status,
275
+ updatedAt: timestamp,
276
+ ...(updates.error ? { error: updates.error } : {}),
277
+ ...((updates.completedAt || status === 'completed' || status === 'failed') ? { completedAt: updates.completedAt ?? timestamp } : {}),
278
+ };
279
+ }
280
+ export function withSymphonyDeliveryStatus(record, status, updates = {}) {
281
+ const timestamp = normalizeSymphonyStatusTimestamp(updates.now);
282
+ return {
283
+ ...record,
284
+ status,
285
+ updatedAt: timestamp,
286
+ ...(updates.autoModeSessionId ? { autoModeSessionId: updates.autoModeSessionId } : {}),
287
+ ...(updates.error ? { error: updates.error } : {}),
288
+ ...((updates.completedAt || status === 'completed' || status === 'failed') ? { completedAt: updates.completedAt ?? timestamp } : {}),
289
+ };
290
+ }
291
+ export function withSymphonySessionStatus(record, status, updates = {}) {
292
+ const timestamp = normalizeSymphonyStatusTimestamp(updates.now);
293
+ return {
294
+ ...record,
295
+ status,
296
+ updatedAt: timestamp,
297
+ ...(updates.autoModeSessionId ? { autoModeSessionId: updates.autoModeSessionId } : {}),
298
+ ...(updates.exitCode !== undefined ? { exitCode: updates.exitCode } : {}),
299
+ ...(updates.dryRun !== undefined ? { dryRun: updates.dryRun } : {}),
300
+ ...(updates.error ? { error: updates.error } : {}),
301
+ ...((updates.completedAt || status === 'completed' || status === 'failed') ? { completedAt: updates.completedAt ?? timestamp } : {}),
302
+ };
303
+ }
304
+ export function withSymphonyRunPlanPrimaryWorker(plan) {
305
+ const primary = plan.workers[0];
306
+ if (!primary) {
307
+ return plan;
308
+ }
309
+ return {
310
+ ...plan,
311
+ task: primary.task,
312
+ taskRecord: primary.taskRecord,
313
+ delivery: primary.delivery,
314
+ session: primary.session,
315
+ };
316
+ }
317
+ export function withSymphonyRunPlanWorker(plan, worker) {
318
+ return withSymphonyRunPlanPrimaryWorker({
319
+ ...plan,
320
+ workers: plan.workers.map((candidate) => (candidate.session.uri === worker.session.uri ? worker : candidate)),
321
+ });
322
+ }
323
+ export function createSymphonyRunStepRecord(input) {
324
+ const now = input.now ?? new Date();
325
+ const payload = sanitizeSymphonyRunStepPayload(input.payload);
326
+ return {
327
+ uri: createSymphonyRunStepUri({
328
+ now,
329
+ randomId: input.randomId ?? `${getSymphonyArchiveKey(input.worker.session.uri)}-${input.stepType}`,
330
+ }),
331
+ issue: input.worker.session.issue,
332
+ task: input.worker.task,
333
+ delivery: input.worker.delivery.uri,
334
+ session: input.worker.session.uri,
335
+ stepType: input.stepType,
336
+ message: normalizeOptionalText(input.message) ?? defaultSymphonyRunStepMessage(input.stepType, input.worker),
337
+ ...(payload ? { payload } : {}),
338
+ createdAt: now.toISOString(),
339
+ };
340
+ }
341
+ export function withSymphonyWorkerRunStep(worker, step) {
342
+ const existing = worker.runSteps ?? [];
343
+ if (existing.some((candidate) => candidate.uri === step.uri)) {
344
+ return worker;
345
+ }
346
+ return {
347
+ ...worker,
348
+ runSteps: [...existing, step],
349
+ };
350
+ }
351
+ export function withSymphonyWorkerRuntimeStep(worker, input) {
352
+ return withSymphonyWorkerRunStep(worker, createSymphonyRunStepRecord({
353
+ worker,
354
+ ...input,
355
+ }));
356
+ }
357
+ export function startSymphonyWorkerRun(input) {
358
+ const taskRecord = applySymphonyDecision(withSymphonyTaskStatus(input.worker.taskRecord, 'running', { now: input.now }), input.decision);
359
+ const delivery = applySymphonyDecision(withSymphonyDeliveryStatus(input.worker.delivery, 'dispatched', { now: input.now }), input.decision);
360
+ const session = applySymphonyDecision(withSymphonySessionStatus(input.worker.session, 'running', { now: input.now }), input.decision);
361
+ return withSymphonyWorkerRuntimeStep({
362
+ task: input.worker.task,
363
+ taskRecord,
364
+ delivery,
365
+ session,
366
+ ...(input.worker.runSteps?.length ? { runSteps: input.worker.runSteps } : {}),
367
+ }, {
368
+ stepType: 'run.started',
369
+ message: input.message ?? `${input.worker.session.backend} worker run started.`,
370
+ payload: {
371
+ issue: input.worker.session.issue,
372
+ task: input.worker.task,
373
+ delivery: input.worker.delivery.uri,
374
+ session: input.worker.session.uri,
375
+ backend: input.worker.session.backend,
376
+ targetAgent: input.worker.delivery.targetAgent,
377
+ ...(input.payload ?? {}),
378
+ },
379
+ now: input.now,
380
+ randomId: input.randomId ?? `${input.worker.delivery.uri}-run-started`,
381
+ });
382
+ }
383
+ export function recordSymphonyWorkerRuntimeEvent(input) {
384
+ const session = input.stepType === 'run.step'
385
+ ? withSymphonySessionStatus(input.worker.session, 'running', { now: input.now })
386
+ : input.worker.session;
387
+ return withSymphonyWorkerRuntimeStep({
388
+ ...input.worker,
389
+ session,
390
+ }, {
391
+ stepType: input.stepType,
392
+ message: input.message,
393
+ payload: {
394
+ issue: input.worker.session.issue,
395
+ task: input.worker.task,
396
+ delivery: input.worker.delivery.uri,
397
+ session: input.worker.session.uri,
398
+ backend: input.worker.session.backend,
399
+ ...(input.payload ?? {}),
400
+ },
401
+ now: input.now,
402
+ randomId: input.randomId,
403
+ });
404
+ }
405
+ export function completeSymphonyWorkerRun(input) {
406
+ const error = input.status === 'failed'
407
+ ? `Backend exited with code ${input.exitCode}`
408
+ : undefined;
409
+ const taskRecord = applySymphonyDecision(withSymphonyTaskStatus(input.worker.taskRecord, input.status, {
410
+ now: input.now,
411
+ ...(error ? { error } : {}),
412
+ }), input.decision);
413
+ const delivery = applySymphonyDecision(withSymphonyDeliveryStatus(input.worker.delivery, input.status, {
414
+ now: input.now,
415
+ ...(input.autoModeSessionId ? { autoModeSessionId: input.autoModeSessionId } : {}),
416
+ ...(error ? { error } : {}),
417
+ }), input.decision);
418
+ const session = applySymphonyDecision(withSymphonySessionStatus(input.worker.session, input.status, {
419
+ now: input.now,
420
+ ...(input.autoModeSessionId ? { autoModeSessionId: input.autoModeSessionId } : {}),
421
+ exitCode: input.exitCode,
422
+ ...(error ? { error } : {}),
423
+ }), input.decision);
424
+ return reconcileSymphonyWorkerDelivery({
425
+ issue: input.issue,
426
+ worker: {
427
+ task: input.worker.task,
428
+ taskRecord,
429
+ delivery,
430
+ session,
431
+ ...(input.worker.runSteps?.length ? { runSteps: input.worker.runSteps } : {}),
432
+ },
433
+ status: input.status,
434
+ exitCode: input.exitCode,
435
+ autoModeSessionId: input.autoModeSessionId,
436
+ reportText: input.reportText,
437
+ now: input.now,
438
+ randomId: input.randomId,
439
+ });
440
+ }
441
+ export function finalizeSymphonyRunPlanAfterWorkers(input) {
442
+ const workers = input.workers ?? input.plan.workers;
443
+ const followUpIssues = input.followUpIssues ?? input.plan.followUpIssues;
444
+ const basePlan = withSymphonyRunPlanPrimaryWorker({
445
+ ...input.plan,
446
+ workers,
447
+ ...(followUpIssues?.length ? { followUpIssues } : {}),
448
+ });
449
+ const failure = workers.find((worker) => {
450
+ const exitCode = worker.session.exitCode;
451
+ return worker.taskRecord.status === 'failed'
452
+ || worker.delivery.status === 'failed'
453
+ || worker.session.status === 'failed'
454
+ || (typeof exitCode === 'number' && exitCode !== 0);
455
+ });
456
+ if (failure) {
457
+ const exitCode = typeof failure.session.exitCode === 'number' ? failure.session.exitCode : undefined;
458
+ const error = failure.session.error
459
+ ?? failure.delivery.error
460
+ ?? failure.taskRecord.error
461
+ ?? (exitCode !== undefined ? `Backend ${failure.session.backend} exited with code ${exitCode}` : `Worker ${failure.taskRecord.title} failed.`);
462
+ return {
463
+ status: 'failed',
464
+ issueStatus: 'blocked',
465
+ blocker: {
466
+ kind: 'worker_failure',
467
+ error,
468
+ ...(exitCode !== undefined ? { exitCode } : {}),
469
+ worker: failure,
470
+ },
471
+ plan: withSymphonyRunPlanPrimaryWorker({
472
+ ...basePlan,
473
+ issue: withSymphonyIssueStatus(basePlan.issue, 'blocked', { error, now: input.now }),
474
+ }),
475
+ };
476
+ }
477
+ const acceptanceBlockedWorker = workers.find((worker) => {
478
+ const review = worker.taskRecord.acceptanceReview
479
+ ?? worker.delivery.acceptanceReview
480
+ ?? worker.session.acceptanceReview;
481
+ return Boolean(review && !review.accepted);
482
+ });
483
+ if (acceptanceBlockedWorker) {
484
+ const review = acceptanceBlockedWorker.taskRecord.acceptanceReview
485
+ ?? acceptanceBlockedWorker.delivery.acceptanceReview
486
+ ?? acceptanceBlockedWorker.session.acceptanceReview;
487
+ const error = review ? summarizeSymphonyAcceptanceBlocker(review) : `Worker ${acceptanceBlockedWorker.taskRecord.title} was not accepted.`;
488
+ return {
489
+ status: 'completed',
490
+ issueStatus: 'blocked',
491
+ blocker: {
492
+ kind: 'acceptance',
493
+ error,
494
+ worker: acceptanceBlockedWorker,
495
+ },
496
+ plan: withSymphonyRunPlanPrimaryWorker({
497
+ ...basePlan,
498
+ issue: withSymphonyIssueStatus(basePlan.issue, 'blocked', { error, now: input.now }),
499
+ }),
500
+ };
501
+ }
502
+ return {
503
+ status: 'completed',
504
+ issueStatus: 'resolved',
505
+ plan: withSymphonyRunPlanPrimaryWorker({
506
+ ...basePlan,
507
+ issue: withSymphonyIssueStatus(basePlan.issue, 'resolved', { now: input.now }),
508
+ }),
509
+ };
510
+ }
511
+ export function summarizeSymphonyAcceptanceBlocker(review) {
512
+ const blocking = review.followUps.find((followUp) => followUp.disposition === 'same_issue_task' || followUp.disposition === 'ask_user');
513
+ return blocking
514
+ ? `${review.summary}: ${blocking.summary}`
515
+ : review.summary;
516
+ }
517
+ function applySymphonyDecision(record, decision) {
518
+ return decision ? appendSymphonyReconcilerDecision(record, decision) : record;
519
+ }
520
+ export function reconcileSymphonyWorkerDelivery(input) {
521
+ const review = createSymphonyAcceptanceReview({
522
+ issue: input.issue,
523
+ worker: input.worker,
524
+ status: input.status,
525
+ exitCode: input.exitCode,
526
+ reportText: input.reportText,
527
+ now: input.now,
528
+ });
529
+ const followUpIssues = createFollowUpIssuesFromAcceptanceReview({
530
+ issue: input.issue,
531
+ worker: input.worker,
532
+ review,
533
+ now: input.now,
534
+ randomId: input.randomId,
535
+ });
536
+ const followUps = review.followUps.map((followUp) => {
537
+ const createdIssue = followUpIssues.find((issue) => issue.description?.includes(followUp.summary));
538
+ return followUp.disposition === 'new_issue' && createdIssue
539
+ ? { ...followUp, issue: createdIssue.uri }
540
+ : followUp;
541
+ });
542
+ const acceptanceReview = {
543
+ ...review,
544
+ followUps,
545
+ reusableExtraction: {
546
+ ...review.reusableExtraction,
547
+ candidates: review.reusableExtraction.candidates.map((candidate) => {
548
+ const updated = followUps.find((followUp) => followUp.summary === candidate.summary && followUp.kind === candidate.kind);
549
+ return updated ?? candidate;
550
+ }),
551
+ },
552
+ };
553
+ const taskRecord = withSymphonyWorkerAcceptanceTaskStatus(input.worker.taskRecord, acceptanceReview, input.status);
554
+ const terminalStep = createSymphonyRunStepRecord({
555
+ worker: input.worker,
556
+ stepType: input.status === 'completed' ? 'run.completed' : 'run.failed',
557
+ message: input.status === 'completed'
558
+ ? `${input.worker.taskRecord.title} completed and entered Secretary reconciliation.`
559
+ : `${input.worker.taskRecord.title} failed before Secretary acceptance.`,
560
+ payload: {
561
+ status: input.status,
562
+ exitCode: input.exitCode,
563
+ autoModeSessionId: input.autoModeSessionId,
564
+ acceptanceOutcome: acceptanceReview.outcome,
565
+ accepted: acceptanceReview.accepted,
566
+ },
567
+ now: input.now,
568
+ randomId: `${normalizeSymphonyRandomId(input.randomId)}-terminal`,
569
+ });
570
+ return {
571
+ acceptanceReview,
572
+ followUpIssues,
573
+ worker: withSymphonyWorkerRunStep({
574
+ task: input.worker.task,
575
+ taskRecord: {
576
+ ...taskRecord,
577
+ acceptanceReview,
578
+ },
579
+ delivery: {
580
+ ...input.worker.delivery,
581
+ ...(input.autoModeSessionId ? { autoModeSessionId: input.autoModeSessionId } : {}),
582
+ acceptanceReview,
583
+ },
584
+ session: {
585
+ ...input.worker.session,
586
+ ...(input.autoModeSessionId ? { autoModeSessionId: input.autoModeSessionId } : {}),
587
+ acceptanceReview,
588
+ },
589
+ ...(input.worker.runSteps?.length ? { runSteps: input.worker.runSteps } : {}),
590
+ }, terminalStep),
591
+ };
592
+ }
593
+ export function createSymphonyAcceptanceReview(input) {
594
+ const now = input.now ?? new Date();
595
+ const envelope = parseSymphonyFinalReportEnvelope(input.reportText);
596
+ const reportSummary = normalizeOptionalText(envelope?.summary)
597
+ ?? summarizeWorkerReportText(input.reportText)
598
+ ?? defaultWorkerCompletionSummary(input.worker, input.status, input.exitCode);
599
+ const followUps = (envelope?.followUps ?? inferFollowUpCandidatesFromReport(input.reportText))
600
+ .map((candidate) => classifySymphonyFollowUpCandidate(candidate));
601
+ const reusableCandidates = followUps.filter(isReusableExtractionFollowUp);
602
+ const reusableExtraction = createReusableExtractionDecision(reusableCandidates);
603
+ const blockingFollowUp = followUps.find((followUp) => followUp.disposition === 'same_issue_task' || followUp.disposition === 'ask_user');
604
+ const accepted = input.status === 'completed' && !blockingFollowUp;
605
+ const outcome = input.status === 'failed' || blockingFollowUp
606
+ ? 'blocked'
607
+ : followUps.some((followUp) => followUp.disposition === 'new_issue' || followUp.disposition === 'idea')
608
+ ? 'follow_up'
609
+ : accepted
610
+ ? 'accepted'
611
+ : 'rejected';
612
+ return {
613
+ outcome,
614
+ accepted,
615
+ reviewedBy: '__secretary__',
616
+ reviewedAt: now.toISOString(),
617
+ summary: reportSummary,
618
+ evidence: normalizeStringList(envelope?.evidence),
619
+ risks: normalizeStringList(envelope?.risks),
620
+ changedFiles: normalizeStringList(envelope?.changedFiles),
621
+ commands: normalizeStringList(envelope?.commands),
622
+ followUps,
623
+ reusableExtraction,
624
+ ...buildSymphonyImplementationChangeRequest({
625
+ issue: input.issue,
626
+ worker: input.worker,
627
+ status: input.status,
628
+ exitCode: input.exitCode,
629
+ summary: reportSummary,
630
+ evidence: normalizeStringList(envelope?.evidence),
631
+ risks: normalizeStringList(envelope?.risks),
632
+ blockingFollowUp,
633
+ now,
634
+ }),
635
+ };
636
+ }
637
+ function buildSymphonyImplementationChangeRequest(input) {
638
+ const trigger = input.status === 'failed'
639
+ ? 'worker_failed'
640
+ : input.blockingFollowUp
641
+ ? 'acceptance_blocked'
642
+ : undefined;
643
+ if (!trigger) {
644
+ return {};
645
+ }
646
+ const failureEvidence = [
647
+ ...input.evidence,
648
+ ...(input.worker.runSteps?.map((step) => `${step.stepType}: ${step.message}`) ?? []),
649
+ ].slice(0, 12);
650
+ const blockingSummary = input.blockingFollowUp?.summary;
651
+ const summary = trigger === 'worker_failed'
652
+ ? input.summary
653
+ : blockingSummary ?? input.summary;
654
+ const recommendedNextShape = input.blockingFollowUp?.disposition === 'ask_user'
655
+ ? 'request_authority'
656
+ : input.blockingFollowUp?.disposition === 'same_issue_task'
657
+ ? 'split'
658
+ : input.status === 'failed'
659
+ ? 'redesign'
660
+ : 'retry';
661
+ return {
662
+ implementationChangeRequest: {
663
+ trigger,
664
+ task: input.worker.task,
665
+ delivery: input.worker.delivery.uri,
666
+ session: input.worker.session.uri,
667
+ summary,
668
+ failedAssumption: trigger === 'worker_failed'
669
+ ? `Worker could complete "${input.worker.taskRecord.title}" under the current task plan.`
670
+ : `Current acceptance can be satisfied without resolving "${summary}".`,
671
+ evidence: failureEvidence,
672
+ risks: input.risks,
673
+ recommendedNextShape,
674
+ basedOnRunSteps: input.worker.runSteps?.map((step) => step.uri) ?? [],
675
+ createdAt: input.now.toISOString(),
676
+ },
677
+ };
678
+ }
679
+ function withSymphonyWorkerAcceptanceTaskStatus(taskRecord, acceptanceReview, status) {
680
+ if (status === 'failed' || acceptanceReview.accepted) {
681
+ return taskRecord;
682
+ }
683
+ const { completedAt: _completedAt, ...rest } = taskRecord;
684
+ return {
685
+ ...rest,
686
+ status: 'blocked',
687
+ error: acceptanceReview.summary,
688
+ };
689
+ }
690
+ export function classifySymphonyFollowUpCandidate(candidate) {
691
+ const normalized = normalizeSymphonyFollowUpCandidate(candidate);
692
+ const suggested = normalizeDisposition(candidate.suggestedDisposition);
693
+ let disposition;
694
+ let reason;
695
+ if (normalized.userDecisionRequired) {
696
+ disposition = 'ask_user';
697
+ reason = normalized.reason ?? 'The follow-up requires user-owned intent, authority, priority, or acceptance.';
698
+ }
699
+ else if (normalized.requiredBeforeAcceptance) {
700
+ disposition = 'same_issue_task';
701
+ reason = normalized.reason ?? 'The follow-up must be resolved before the current delivery can be accepted.';
702
+ }
703
+ else if (suggested) {
704
+ disposition = suggested;
705
+ reason = normalized.reason ?? `Worker suggested ${suggested}.`;
706
+ }
707
+ else if (isReusableExtractionKind(normalized.kind)) {
708
+ disposition = 'new_issue';
709
+ reason = normalized.reason ?? 'The finding is reusable across surfaces or workers and should be tracked independently.';
710
+ }
711
+ else if (normalized.kind === 'live_verification_gap' || normalized.kind === 'new_defect') {
712
+ disposition = 'new_issue';
713
+ reason = normalized.reason ?? 'The finding is actionable follow-up work discovered during the run.';
714
+ }
715
+ else if (normalized.kind === 'documentation' || normalized.kind === 'cleanup') {
716
+ disposition = 'idea';
717
+ reason = normalized.reason ?? 'The finding is useful but needs scope or priority before it becomes work.';
718
+ }
719
+ else {
720
+ disposition = 'evidence_only';
721
+ reason = normalized.reason ?? 'The finding is recorded as evidence and does not currently create new work.';
722
+ }
723
+ return {
724
+ ...normalized,
725
+ disposition,
726
+ reason,
727
+ source: 'worker_report',
728
+ };
729
+ }
730
+ export function parseSymphonyFinalReportEnvelope(text) {
731
+ const normalized = normalizeOptionalText(text);
732
+ if (!normalized) {
733
+ return null;
734
+ }
735
+ const explicit = extractSymphonyFinalJsonBlocks(normalized);
736
+ for (const block of explicit) {
737
+ const parsed = parseJsonObject(block);
738
+ const envelope = normalizeFinalReportEnvelope(parsed);
739
+ if (envelope) {
740
+ return envelope;
741
+ }
742
+ }
743
+ for (const block of extractGenericJsonBlocks(normalized)) {
744
+ const parsed = parseJsonObject(block);
745
+ const envelope = normalizeFinalReportEnvelope(parsed);
746
+ if (envelope) {
747
+ return envelope;
748
+ }
749
+ }
750
+ const parsed = parseJsonObject(normalized);
751
+ return normalizeFinalReportEnvelope(parsed);
752
+ }
753
+ export function parseSymphonyRuntimeDeliveryResult(text) {
754
+ const normalized = normalizeOptionalText(text);
755
+ if (!normalized) {
756
+ return null;
757
+ }
758
+ const explicit = extractSymphonyDeliveryJsonBlocks(normalized);
759
+ for (const block of explicit) {
760
+ const result = normalizeSymphonyRuntimeDeliveryResult(parseJsonObject(block));
761
+ if (result) {
762
+ return result;
763
+ }
764
+ }
765
+ for (const block of extractGenericJsonBlocks(normalized)) {
766
+ const result = normalizeSymphonyRuntimeDeliveryResult(parseJsonObject(block));
767
+ if (result) {
768
+ return result;
769
+ }
770
+ }
771
+ return normalizeSymphonyRuntimeDeliveryResult(parseJsonObject(normalized));
772
+ }
773
+ export function normalizeSymphonyRuntimeDeliveryResult(value) {
774
+ if (!isRecord(value)) {
775
+ return null;
776
+ }
777
+ const finalReport = normalizeRuntimeDeliveryReportText(value);
778
+ const events = normalizeRuntimeDeliveryEvents(value.events);
779
+ const exitCode = normalizeRuntimeDeliveryExitCode(value.exitCode ?? value.exit_code);
780
+ const status = normalizeRuntimeDeliveryStatus(value.status)
781
+ ?? (exitCode !== undefined && exitCode !== 0 ? 'failed' : 'completed');
782
+ const normalizedExitCode = exitCode ?? (status === 'completed' ? 0 : 1);
783
+ const hasSignal = value.symphonyDelivery === true
784
+ || value.symphonyFinal === true
785
+ || normalizeOptionalText(value.kind)?.toLowerCase().includes('symphony') === true
786
+ || normalizeOptionalText(value.type)?.toLowerCase().includes('symphony') === true
787
+ || finalReport !== undefined
788
+ || events.length > 0
789
+ || value.status !== undefined
790
+ || value.exitCode !== undefined
791
+ || value.exit_code !== undefined;
792
+ if (!hasSignal) {
793
+ return null;
794
+ }
795
+ return {
796
+ status,
797
+ exitCode: normalizedExitCode,
798
+ ...(normalizeOptionalText(value.autoModeSessionId ?? value.auto_mode_session_id ?? value.sessionId ?? value.session_id ?? value.externalRunId ?? value.external_run_id) ? {
799
+ autoModeSessionId: normalizeOptionalText(value.autoModeSessionId ?? value.auto_mode_session_id ?? value.sessionId ?? value.session_id ?? value.externalRunId ?? value.external_run_id),
800
+ } : {}),
801
+ ...(finalReport ? { reportText: finalReport } : {}),
802
+ events,
803
+ };
804
+ }
211
805
  export function renderSymphonyRuntimePrompt(input) {
212
806
  const acceptanceCriteria = normalizeSymphonyAcceptanceCriteria(input.acceptanceCriteria);
213
807
  const criteria = acceptanceCriteria.length > 0
@@ -233,23 +827,22 @@ export function renderSymphonyRuntimePrompt(input) {
233
827
  ...(input.issuer?.thread ? [`Issuer thread: ${input.issuer.thread}`] : []),
234
828
  ...(input.target?.chat ? [`Target chat: ${input.target.chat}`] : []),
235
829
  ...(input.target?.thread ? [`Target thread: ${input.target.thread}`] : []),
236
- ...(input.target?.contact ? [`Target contact: ${input.target.contact}`] : []),
237
830
  ...(input.target?.agent ? [`Target agent: ${input.target.agent}`] : []),
238
831
  ...(input.workerIndex && input.workerCount ? [`Worker: ${input.workerIndex}/${input.workerCount}`] : []),
239
832
  ...(workThread ? [`Work thread: ${workThread}`] : []),
240
833
  `Workspace: ${input.workspace.path}`,
241
834
  `Workspace kind: ${input.workspace.kind}`,
242
- ...(input.workspace.workspace ? [`Workspace resource: ${input.workspace.workspace}`] : []),
835
+ ...(input.workspace.container ? [`Workspace container: ${input.workspace.container}`] : []),
243
836
  ...(input.workspace.repository ? [`Workspace repository: ${input.workspace.repository}`] : []),
244
837
  ...(input.workspace.branch ? [`Workspace branch: ${input.workspace.branch}`] : []),
245
838
  ...(input.workspace.baseRevision ? [`Workspace base revision: ${input.workspace.baseRevision}`] : []),
246
839
  ...(input.workspace.environment ? [`Workspace environment: ${formatSymphonyWorkerEnvironment(input.workspace.environment)}`] : []),
247
840
  '',
248
841
  '## Runtime Space Contract',
249
- '- 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.',
842
+ '- Shared control space: Issue, Task, Delivery, Session, Run, and Evidence URIs are the common coordination surface with AI Secretary and product UI.',
250
843
  '- 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.',
251
844
  '- Thread reconciliation: messages, input/approval requests, blockers, schedule ticks, and Delivery submissions enter the Thread first; the Reconciler/Scheduler wakes Secretary or workers.',
252
- '- 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.',
845
+ '- Report through Delivery/Evidence: return progress, blockers, implementation change requests, and verification for AI Secretary to persist or route.',
253
846
  '- Thread workspace: workers assigned to the same Thread in the same environment should normally share this workspace; independent Threads may use separate worktrees.',
254
847
  '- Environment-scoped identity: cross-environment file identity requires revision, artifact, patch, checksum, or evidence references.',
255
848
  '',
@@ -267,26 +860,658 @@ export function renderSymphonyRuntimePrompt(input) {
267
860
  '- Report blockers to AI Secretary instead of asking the user directly.',
268
861
  '- Do not read sibling worker transcripts unless Secretary explicitly includes them in a Delivery.',
269
862
  '- Preserve a concise report with changed files, commands run, and remaining risks.',
270
- '- 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.',
271
863
  '- If blocked by missing credentials, destructive actions, or unclear scope, report the blocker instead of guessing.',
272
864
  '- 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.',
273
865
  '- When reporting file work across environments, include repo-relative paths plus base revision, checksums/etags, patch or artifact references, and verification evidence.',
274
866
  '',
867
+ '## Final Report And Follow-Up Candidates',
868
+ '- Final output must separate assigned-work evidence from follow-up candidates.',
869
+ '- Include changed files, commands/tests run, verification evidence, remaining risks, and blockers.',
870
+ '- If you created app-local glue that another surface or worker will likely need, report it as a follow-up candidate instead of hiding it in the summary.',
871
+ '- Candidate signals include missing shared abstractions, repeated model/ORM helpers, duplicated CLI/Web/service lifecycle logic, runtime adapter normalization, reusable test harnesses, live-verification gaps, defects, cleanup, or documentation work.',
872
+ '- You may recommend a disposition, but AI Secretary owns the final classification: same_issue_task, new_issue, idea, evidence_only, or ask_user.',
873
+ '- Prefer this machine-readable envelope at the end of the report so LinX can archive it:',
874
+ '```json',
875
+ '{',
876
+ ' "symphonyFinal": true,',
877
+ ' "summary": "one sentence result",',
878
+ ' "changedFiles": ["repo-relative/path.ts"],',
879
+ ' "commands": ["test command actually run"],',
880
+ ' "evidence": ["what proves acceptance"],',
881
+ ' "risks": ["known risk or not-tested gap"],',
882
+ ' "followUps": [',
883
+ ' {',
884
+ ' "kind": "missing_shared_abstraction",',
885
+ ' "summary": "shared extraction or follow-up work",',
886
+ ' "evidence": ["file or observation"],',
887
+ ' "suggestedDisposition": "new_issue",',
888
+ ' "reason": "why it is separate from this delivery"',
889
+ ' }',
890
+ ' ]',
891
+ '}',
892
+ '```',
893
+ '',
275
894
  '## Pod And Control Record Boundary',
276
895
  '- In LinX runtime, Pod control records are authoritative. Local files are mirrors, logs, or portable-runtime fallbacks.',
277
- '- 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.',
278
- '- Write only execution facts for the assigned work: Run/RunStep progress, blockers, file-primary Evidence/Report, Delivery report metadata, or Implementation Change Request.',
896
+ '- If Pod/model tools are available, read only the assigned Issue, Task, Delivery, Run, source context, and existing evidence needed for this task.',
897
+ '- Write only execution facts for the assigned work: Run/RunStep progress, blockers, Evidence, Delivery report, or Implementation Change Request.',
279
898
  '- 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.',
280
899
  '- Use shared model/ORM surfaces when writing structured Pod data. Do not hand-patch business TTL or invent Pod paths.',
281
900
  '- If Pod access is unavailable, return the same facts as a structured report so AI Secretary can persist them.',
282
901
  '',
283
902
  '## Documentation Authority',
284
- '- 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.',
903
+ '- Pod Issue/Spec/Task records are the control authority for status, scope, acceptance, split, ownership, closure, and cross-client coordination.',
285
904
  '- Repository docs are the implementation authority for code-adjacent design, behavior notes, tests, examples, migration details, and file-level evidence.',
286
905
  '- When you edit repository docs, reference the Pod Issue/Spec/Task URI instead of creating a second Issue truth.',
287
906
  '- If repository findings contradict the Pod control record, write an Implementation Change Request instead of silently changing acceptance or scope.',
288
907
  ].join('\n');
289
908
  }
909
+ function createFollowUpIssuesFromAcceptanceReview(input) {
910
+ const now = input.now ?? new Date();
911
+ const timestamp = now.toISOString();
912
+ return input.review.followUps
913
+ .filter((followUp) => followUp.disposition === 'new_issue')
914
+ .map((followUp, index) => {
915
+ const uri = createSymphonyIssueUri({
916
+ now,
917
+ randomId: `${normalizeSymphonyRandomId(input.randomId)}-fu${index + 1}`,
918
+ });
919
+ return {
920
+ uri,
921
+ title: createSymphonyTitle(followUp.summary),
922
+ description: [
923
+ followUp.summary,
924
+ '',
925
+ `Origin issue: ${input.issue.uri}`,
926
+ `Origin task: ${input.worker.task}`,
927
+ `Origin delivery: ${input.worker.delivery.uri}`,
928
+ `Origin session: ${input.worker.session.uri}`,
929
+ `Disposition reason: ${followUp.reason}`,
930
+ ...(followUp.evidence?.length ? ['', 'Evidence:', ...followUp.evidence.map((item) => `- ${item}`)] : []),
931
+ ].join('\n'),
932
+ status: 'open',
933
+ priority: input.issue.priority,
934
+ source: input.issue.source,
935
+ issuer: input.issue.issuer,
936
+ parentIssue: input.issue.uri,
937
+ labels: ['symphony', 'follow-up', followUp.kind],
938
+ tasks: [],
939
+ deliveries: [],
940
+ sessions: [],
941
+ ...(input.issue.chat ? { chat: input.issue.chat } : {}),
942
+ ...(input.issue.thread ? { thread: input.issue.thread } : {}),
943
+ ...(input.issue.messages ? { messages: input.issue.messages } : {}),
944
+ createdAt: timestamp,
945
+ updatedAt: timestamp,
946
+ };
947
+ });
948
+ }
949
+ function createReusableExtractionDecision(candidates) {
950
+ if (candidates.length === 0) {
951
+ return {
952
+ disposition: 'evidence_only',
953
+ reason: 'No reusable-module extraction candidate was reported or detected for this delivery.',
954
+ candidates: [],
955
+ };
956
+ }
957
+ const blocking = candidates.find((candidate) => candidate.disposition === 'same_issue_task');
958
+ if (blocking) {
959
+ return {
960
+ disposition: 'same_issue_task',
961
+ reason: blocking.reason,
962
+ candidates,
963
+ };
964
+ }
965
+ const newIssue = candidates.find((candidate) => candidate.disposition === 'new_issue');
966
+ if (newIssue) {
967
+ return {
968
+ disposition: 'new_issue',
969
+ reason: newIssue.reason,
970
+ candidates,
971
+ };
972
+ }
973
+ const askUser = candidates.find((candidate) => candidate.disposition === 'ask_user');
974
+ if (askUser) {
975
+ return {
976
+ disposition: 'ask_user',
977
+ reason: askUser.reason,
978
+ candidates,
979
+ };
980
+ }
981
+ const idea = candidates.find((candidate) => candidate.disposition === 'idea');
982
+ if (idea) {
983
+ return {
984
+ disposition: 'idea',
985
+ reason: idea.reason,
986
+ candidates,
987
+ };
988
+ }
989
+ return {
990
+ disposition: 'evidence_only',
991
+ reason: 'Reusable extraction candidates were recorded as evidence only.',
992
+ candidates,
993
+ };
994
+ }
995
+ function normalizeSymphonyFollowUpCandidate(candidate) {
996
+ return {
997
+ kind: normalizeFollowUpKind(candidate.kind),
998
+ summary: normalizeOptionalText(candidate.summary) ?? 'Unspecified Symphony follow-up',
999
+ evidence: normalizeStringList(candidate.evidence),
1000
+ ...(normalizeDisposition(candidate.suggestedDisposition) ? { suggestedDisposition: normalizeDisposition(candidate.suggestedDisposition) } : {}),
1001
+ ...(normalizeOptionalText(candidate.reason) ? { reason: normalizeOptionalText(candidate.reason) } : {}),
1002
+ ...(candidate.requiredBeforeAcceptance ? { requiredBeforeAcceptance: true } : {}),
1003
+ ...(candidate.userDecisionRequired ? { userDecisionRequired: true } : {}),
1004
+ ...(normalizeOptionalText(candidate.targetPackage) ? { targetPackage: normalizeOptionalText(candidate.targetPackage) } : {}),
1005
+ };
1006
+ }
1007
+ function normalizeFollowUpKind(value) {
1008
+ if (value === 'new_defect'
1009
+ || value === 'missing_shared_abstraction'
1010
+ || value === 'app_local_glue'
1011
+ || value === 'models_gap'
1012
+ || value === 'orm_gap'
1013
+ || value === 'runtime_api_gap'
1014
+ || value === 'shared_runtime_utility'
1015
+ || value === 'test_harness'
1016
+ || value === 'live_verification_gap'
1017
+ || value === 'documentation'
1018
+ || value === 'cleanup'
1019
+ || value === 'other') {
1020
+ return value;
1021
+ }
1022
+ const text = normalizeOptionalText(typeof value === 'string' ? value : undefined)?.toLowerCase() ?? '';
1023
+ if (text.includes('shared') || text.includes('abstraction') || text.includes('reuse'))
1024
+ return 'missing_shared_abstraction';
1025
+ if (text.includes('glue') || text.includes('local'))
1026
+ return 'app_local_glue';
1027
+ if (text.includes('model'))
1028
+ return 'models_gap';
1029
+ if (text.includes('orm'))
1030
+ return 'orm_gap';
1031
+ if (text.includes('runtime'))
1032
+ return 'runtime_api_gap';
1033
+ if (text.includes('harness') || text.includes('fixture'))
1034
+ return 'test_harness';
1035
+ if (text.includes('verify') || text.includes('validation'))
1036
+ return 'live_verification_gap';
1037
+ if (text.includes('doc'))
1038
+ return 'documentation';
1039
+ if (text.includes('cleanup'))
1040
+ return 'cleanup';
1041
+ if (text.includes('bug') || text.includes('defect'))
1042
+ return 'new_defect';
1043
+ return 'other';
1044
+ }
1045
+ function normalizeDisposition(value) {
1046
+ if (value === 'same_issue_task'
1047
+ || value === 'new_issue'
1048
+ || value === 'idea'
1049
+ || value === 'evidence_only'
1050
+ || value === 'ask_user') {
1051
+ return value;
1052
+ }
1053
+ return undefined;
1054
+ }
1055
+ function isReusableExtractionFollowUp(candidate) {
1056
+ return isReusableExtractionKind(candidate.kind);
1057
+ }
1058
+ function isReusableExtractionKind(kind) {
1059
+ return kind === 'missing_shared_abstraction'
1060
+ || kind === 'app_local_glue'
1061
+ || kind === 'models_gap'
1062
+ || kind === 'orm_gap'
1063
+ || kind === 'runtime_api_gap'
1064
+ || kind === 'shared_runtime_utility'
1065
+ || kind === 'test_harness';
1066
+ }
1067
+ function inferFollowUpCandidatesFromReport(text) {
1068
+ const normalized = normalizeOptionalText(text);
1069
+ if (!normalized) {
1070
+ return [];
1071
+ }
1072
+ const lower = normalized.toLowerCase();
1073
+ const candidates = [];
1074
+ if (/(shared abstraction|reusable module|抽.*复用|复用.*模块|app-local glue|local glue|duplicated .*logic|duplicate .*helper)/iu.test(normalized)) {
1075
+ candidates.push({
1076
+ kind: 'missing_shared_abstraction',
1077
+ summary: extractFollowUpSummary(normalized, 'Reusable extraction candidate detected from worker report.'),
1078
+ evidence: extractEvidenceLines(normalized),
1079
+ });
1080
+ }
1081
+ if (/(models?|schema|resource helper|iri|uri|id helper)/u.test(lower)) {
1082
+ candidates.push({
1083
+ kind: 'models_gap',
1084
+ summary: extractFollowUpSummary(normalized, 'Model/helper gap detected from worker report.'),
1085
+ evidence: extractEvidenceLines(normalized),
1086
+ });
1087
+ }
1088
+ if (/(verification gap|not tested|not-tested|未验证|manual verification|live verification)/iu.test(normalized)) {
1089
+ candidates.push({
1090
+ kind: 'live_verification_gap',
1091
+ summary: extractFollowUpSummary(normalized, 'Verification follow-up detected from worker report.'),
1092
+ evidence: extractEvidenceLines(normalized),
1093
+ suggestedDisposition: 'new_issue',
1094
+ });
1095
+ }
1096
+ return dedupeFollowUpCandidates(candidates);
1097
+ }
1098
+ function dedupeFollowUpCandidates(candidates) {
1099
+ const seen = new Set();
1100
+ const deduped = [];
1101
+ for (const candidate of candidates) {
1102
+ const key = `${candidate.kind}\0${candidate.summary}`;
1103
+ if (seen.has(key)) {
1104
+ continue;
1105
+ }
1106
+ seen.add(key);
1107
+ deduped.push(candidate);
1108
+ }
1109
+ return deduped;
1110
+ }
1111
+ function extractFollowUpSummary(text, fallback) {
1112
+ const lines = text.split(/\r?\n/u)
1113
+ .map((line) => line.replace(/^[-*]\s*/u, '').trim())
1114
+ .filter(Boolean);
1115
+ const match = lines.find((line) => /(shared|reusable|复用|models?|schema|verification|not tested|未验证|helper|runtime|adapter|glue|duplicate)/iu.test(line));
1116
+ return createSymphonyTitle(match ?? fallback);
1117
+ }
1118
+ function extractEvidenceLines(text) {
1119
+ return text.split(/\r?\n/u)
1120
+ .map((line) => line.trim())
1121
+ .filter((line) => /(\bapps\/|\bpackages\/|\bdocs\/|\btests?\/|\.ts\b|\.tsx\b|\.mjs\b|\.md\b|not tested|未验证)/iu.test(line))
1122
+ .slice(0, 8);
1123
+ }
1124
+ function summarizeWorkerReportText(text) {
1125
+ const normalized = normalizeOptionalText(text);
1126
+ if (!normalized) {
1127
+ return undefined;
1128
+ }
1129
+ const line = normalized.split(/\r?\n/u).map((item) => item.trim()).find(Boolean);
1130
+ return line ? createSymphonyTitle(line) : undefined;
1131
+ }
1132
+ function defaultWorkerCompletionSummary(worker, status, exitCode) {
1133
+ return status === 'completed'
1134
+ ? `${worker.taskRecord.title} completed.`
1135
+ : `${worker.taskRecord.title} failed with exit code ${exitCode}.`;
1136
+ }
1137
+ function defaultSymphonyRunStepMessage(stepType, worker) {
1138
+ if (stepType === 'session.started')
1139
+ return `${worker.session.backend} worker session started.`;
1140
+ if (stepType === 'session.resumed')
1141
+ return `${worker.session.backend} worker session resumed.`;
1142
+ if (stepType === 'run.started')
1143
+ return `${worker.session.backend} worker run started.`;
1144
+ if (stepType === 'run.step')
1145
+ return `${worker.session.backend} worker progress heartbeat.`;
1146
+ if (stepType === 'approval.required')
1147
+ return `${worker.session.backend} worker requires approval.`;
1148
+ if (stepType === 'input.required')
1149
+ return `${worker.session.backend} worker requires input.`;
1150
+ if (stepType === 'worker.blocked')
1151
+ return `${worker.session.backend} worker is blocked.`;
1152
+ if (stepType === 'delivery.submitted')
1153
+ return `${worker.session.backend} worker submitted a delivery.`;
1154
+ if (stepType === 'delivery.completed')
1155
+ return `${worker.session.backend} worker delivery completed.`;
1156
+ if (stepType === 'delivery.failed')
1157
+ return `${worker.session.backend} worker delivery failed.`;
1158
+ if (stepType === 'run.completed')
1159
+ return `${worker.session.backend} worker run completed.`;
1160
+ return `${worker.session.backend} worker run failed.`;
1161
+ }
1162
+ function sanitizeSymphonyRunStepPayload(payload) {
1163
+ if (!payload) {
1164
+ return undefined;
1165
+ }
1166
+ const entries = Object.entries(payload).filter(([, value]) => value !== undefined);
1167
+ return entries.length > 0 ? Object.fromEntries(entries) : undefined;
1168
+ }
1169
+ function normalizeSymphonyStatusTimestamp(value) {
1170
+ if (value instanceof Date) {
1171
+ return value.toISOString();
1172
+ }
1173
+ const text = normalizeOptionalText(value);
1174
+ if (text) {
1175
+ const parsed = new Date(text);
1176
+ return Number.isFinite(parsed.getTime()) ? parsed.toISOString() : text;
1177
+ }
1178
+ return new Date().toISOString();
1179
+ }
1180
+ function normalizeSymphonyThreadReconcileEvent(value, input, index) {
1181
+ const hookEventName = normalizeOptionalText(value.hookEventName ?? value.hook_event_name);
1182
+ if (value.symphonyHookEvent === true || hookEventName) {
1183
+ return normalizeSymphonyCodexHookThreadEvent(value, input, index, hookEventName ?? 'unknown');
1184
+ }
1185
+ const data = normalizeSymphonyThreadEventData(value, {
1186
+ source: normalizeOptionalText(value.source) ?? 'codex-mcp',
1187
+ });
1188
+ return {
1189
+ id: normalizeOptionalText(value.id) ?? `event_${input.randomId ?? 'symphony'}_${index + 1}`,
1190
+ type: normalizeSymphonyThreadEventType(value.type ?? value.eventType ?? value.event_type) ?? 'run.updated',
1191
+ ...(normalizeOptionalText(value.chat ?? input.chat) ? { chat: normalizeOptionalText(value.chat ?? input.chat) } : {}),
1192
+ ...(normalizeOptionalText(value.thread ?? input.thread) ? { thread: normalizeOptionalText(value.thread ?? input.thread) } : {}),
1193
+ ...(normalizeOptionalText(value.resource) ? { resource: normalizeOptionalText(value.resource) } : {}),
1194
+ ...(normalizeSymphonyThreadEventActor(value.actor) ? { actor: normalizeSymphonyThreadEventActor(value.actor) } : {}),
1195
+ ...(normalizeOptionalText(value.content ?? value.message) ? { content: normalizeOptionalText(value.content ?? value.message) } : {}),
1196
+ ...(normalizeOptionalText(value.createdAt ?? value.created_at) ? { createdAt: normalizeOptionalText(value.createdAt ?? value.created_at) } : {}),
1197
+ ...(data ? { data } : {}),
1198
+ };
1199
+ }
1200
+ function normalizeSymphonyCodexHookThreadEvent(value, input, index, hookEventName) {
1201
+ const sessionId = normalizeOptionalText(value.sessionId ?? value.session_id);
1202
+ const commonData = normalizeSymphonyThreadEventData(value, {
1203
+ source: 'codex-native-hook',
1204
+ hookEventName,
1205
+ ...(sessionId ? { sessionId } : {}),
1206
+ });
1207
+ const id = normalizeOptionalText(value.id)
1208
+ ?? `codex_hook_${safeSymphonyIdSegment(sessionId ?? 'session')}_${safeSymphonyIdSegment(hookEventName)}_${index + 1}`;
1209
+ const content = normalizeOptionalText(value.content ?? value.message)
1210
+ ?? `Codex hook ${hookEventName}${sessionId ? ` for ${sessionId}` : ''}.`;
1211
+ const createdAt = normalizeOptionalText(value.createdAt ?? value.created_at);
1212
+ if (hookEventName === 'UserPromptSubmit') {
1213
+ return {
1214
+ id,
1215
+ type: 'message.appended',
1216
+ ...(normalizeOptionalText(input.chat) ? { chat: normalizeOptionalText(input.chat) } : {}),
1217
+ ...(normalizeOptionalText(input.thread) ? { thread: normalizeOptionalText(input.thread) } : {}),
1218
+ actor: { role: 'user' },
1219
+ content,
1220
+ ...(createdAt ? { createdAt } : {}),
1221
+ data: {
1222
+ ...(commonData ?? {}),
1223
+ role: 'user',
1224
+ },
1225
+ };
1226
+ }
1227
+ if (hookEventName === 'PreToolUse') {
1228
+ return {
1229
+ id,
1230
+ type: 'approval.required',
1231
+ ...(normalizeOptionalText(input.chat) ? { chat: normalizeOptionalText(input.chat) } : {}),
1232
+ ...(normalizeOptionalText(input.thread) ? { thread: normalizeOptionalText(input.thread) } : {}),
1233
+ actor: { role: 'tool' },
1234
+ content,
1235
+ ...(createdAt ? { createdAt } : {}),
1236
+ data: commonData,
1237
+ };
1238
+ }
1239
+ return {
1240
+ id,
1241
+ type: 'run.updated',
1242
+ ...(normalizeOptionalText(input.chat) ? { chat: normalizeOptionalText(input.chat) } : {}),
1243
+ ...(normalizeOptionalText(input.thread) ? { thread: normalizeOptionalText(input.thread) } : {}),
1244
+ actor: { role: 'runtime' },
1245
+ content,
1246
+ ...(createdAt ? { createdAt } : {}),
1247
+ data: commonData,
1248
+ };
1249
+ }
1250
+ function normalizeSymphonyThreadEventData(value, defaults) {
1251
+ const data = isPlainSymphonyRecord(value.data) ? value.data : {};
1252
+ return sanitizeSymphonyRunStepPayload({
1253
+ ...data,
1254
+ ...defaults,
1255
+ });
1256
+ }
1257
+ function normalizeSymphonyThreadEventActor(value) {
1258
+ if (!isPlainSymphonyRecord(value)) {
1259
+ return undefined;
1260
+ }
1261
+ const id = normalizeOptionalText(value.id);
1262
+ const role = normalizeOptionalText(value.role);
1263
+ const label = normalizeOptionalText(value.label);
1264
+ if (!id && !role && !label) {
1265
+ return undefined;
1266
+ }
1267
+ return {
1268
+ ...(id ? { id } : {}),
1269
+ ...(role ? { role: role } : {}),
1270
+ ...(label ? { label } : {}),
1271
+ };
1272
+ }
1273
+ function normalizeSymphonyThreadEventType(value) {
1274
+ return normalizeOptionalText(value);
1275
+ }
1276
+ function resolveSymphonyThreadReconcilerNextAction(wakeJobs, notificationEvents) {
1277
+ if (wakeJobs.some((job) => job.targetRole === 'secretary')) {
1278
+ return 'wake_secretary';
1279
+ }
1280
+ if (wakeJobs.some((job) => job.targetRole === 'worker')) {
1281
+ return 'wake_worker';
1282
+ }
1283
+ if (wakeJobs.some((job) => job.targetRole === 'reviewer')) {
1284
+ return 'wake_reviewer';
1285
+ }
1286
+ if (notificationEvents.length > 0) {
1287
+ return 'notify_user';
1288
+ }
1289
+ return 'noop';
1290
+ }
1291
+ function summarizeSymphonyThreadReconcilerResult(eventCount, nextAction, wakeJobCount, notificationCount) {
1292
+ if (eventCount === 0) {
1293
+ return 'No Symphony thread events to reconcile.';
1294
+ }
1295
+ if (nextAction === 'wake_secretary') {
1296
+ return `Reconciled ${eventCount} event(s); wake Secretary with ${wakeJobCount} queued job(s).`;
1297
+ }
1298
+ if (nextAction === 'wake_worker') {
1299
+ return `Reconciled ${eventCount} event(s); wake worker with ${wakeJobCount} queued job(s).`;
1300
+ }
1301
+ if (nextAction === 'wake_reviewer') {
1302
+ return `Reconciled ${eventCount} event(s); wake reviewer with ${wakeJobCount} queued job(s).`;
1303
+ }
1304
+ if (nextAction === 'notify_user') {
1305
+ return `Reconciled ${eventCount} event(s); notify user with ${notificationCount} notification(s).`;
1306
+ }
1307
+ return `Reconciled ${eventCount} event(s); no wake job is required.`;
1308
+ }
1309
+ function safeSymphonyIdSegment(value) {
1310
+ const normalized = value
1311
+ .replace(/[^a-zA-Z0-9._:-]/gu, '-')
1312
+ .replace(/-+/gu, '-')
1313
+ .replace(/^-|-$/gu, '')
1314
+ .slice(0, 96);
1315
+ return normalized || 'unknown';
1316
+ }
1317
+ function isPlainSymphonyRecord(value) {
1318
+ return isRecord(value) && !Array.isArray(value);
1319
+ }
1320
+ function extractSymphonyFinalJsonBlocks(text) {
1321
+ const blocks = [];
1322
+ for (const match of text.matchAll(/```(?:json)?\s*(?:symphony-final|linx-symphony-final)?\s*\n([\s\S]*?)```/giu)) {
1323
+ const body = match[1]?.trim();
1324
+ if (body && /"symphonyFinal"|"followUps"|"summary"/u.test(body)) {
1325
+ blocks.push(body);
1326
+ }
1327
+ }
1328
+ return blocks;
1329
+ }
1330
+ function extractSymphonyDeliveryJsonBlocks(text) {
1331
+ const blocks = [];
1332
+ for (const match of text.matchAll(/```(?:json)?\s*(?:symphony-delivery|linx-symphony-delivery)?\s*\n([\s\S]*?)```/giu)) {
1333
+ const body = match[1]?.trim();
1334
+ if (body && /"symphonyDelivery"|"symphonyFinal"|"events"|"reportText"|"exitCode"/u.test(body)) {
1335
+ blocks.push(body);
1336
+ }
1337
+ }
1338
+ return blocks;
1339
+ }
1340
+ function extractGenericJsonBlocks(text) {
1341
+ const blocks = [];
1342
+ for (const match of text.matchAll(/```json\s*\n([\s\S]*?)```/giu)) {
1343
+ const body = match[1]?.trim();
1344
+ if (body) {
1345
+ blocks.push(body);
1346
+ }
1347
+ }
1348
+ return blocks;
1349
+ }
1350
+ function normalizeRuntimeDeliveryReportText(value) {
1351
+ const explicitReport = normalizeOptionalText(value.reportText ?? value.report_text);
1352
+ if (explicitReport) {
1353
+ return explicitReport;
1354
+ }
1355
+ const report = value.report ?? value.finalReport ?? value.final_report;
1356
+ const reportText = normalizeOptionalText(report);
1357
+ if (reportText) {
1358
+ return reportText;
1359
+ }
1360
+ const reportEnvelope = normalizeFinalReportEnvelope(report);
1361
+ if (reportEnvelope) {
1362
+ return stringifySymphonyFinalReportEnvelope(reportEnvelope);
1363
+ }
1364
+ const envelope = normalizeFinalReportEnvelope(value);
1365
+ return envelope ? stringifySymphonyFinalReportEnvelope(envelope) : undefined;
1366
+ }
1367
+ function stringifySymphonyFinalReportEnvelope(envelope) {
1368
+ return [
1369
+ '```json',
1370
+ JSON.stringify({ symphonyFinal: true, ...envelope }),
1371
+ '```',
1372
+ ].join('\n');
1373
+ }
1374
+ function normalizeRuntimeDeliveryEvents(value) {
1375
+ if (!Array.isArray(value)) {
1376
+ return [];
1377
+ }
1378
+ return value
1379
+ .map((item) => normalizeRuntimeDeliveryEvent(item))
1380
+ .filter((item) => Boolean(item));
1381
+ }
1382
+ function normalizeRuntimeDeliveryEvent(value) {
1383
+ if (!isRecord(value)) {
1384
+ return null;
1385
+ }
1386
+ const stepType = normalizeRuntimeEventType(value.stepType ?? value.step_type ?? value.type);
1387
+ if (!stepType) {
1388
+ return null;
1389
+ }
1390
+ const payload = isRecord(value.payload) ? value.payload : undefined;
1391
+ return {
1392
+ stepType,
1393
+ ...(normalizeOptionalText(value.message) ? { message: normalizeOptionalText(value.message) } : {}),
1394
+ ...(payload ? { payload } : {}),
1395
+ ...(normalizeRuntimeDeliveryTimestamp(value.createdAt ?? value.created_at ?? value.timestamp) ? {
1396
+ createdAt: normalizeRuntimeDeliveryTimestamp(value.createdAt ?? value.created_at ?? value.timestamp),
1397
+ } : {}),
1398
+ ...(normalizeOptionalText(value.randomId ?? value.random_id) ? { randomId: normalizeOptionalText(value.randomId ?? value.random_id) } : {}),
1399
+ };
1400
+ }
1401
+ function normalizeRuntimeEventType(value) {
1402
+ const normalized = normalizeOptionalText(value);
1403
+ if (normalized === 'session.started'
1404
+ || normalized === 'session.resumed'
1405
+ || normalized === 'run.started'
1406
+ || normalized === 'run.step'
1407
+ || normalized === 'approval.required'
1408
+ || normalized === 'input.required'
1409
+ || normalized === 'worker.blocked'
1410
+ || normalized === 'delivery.submitted'
1411
+ || normalized === 'delivery.completed'
1412
+ || normalized === 'delivery.failed'
1413
+ || normalized === 'run.completed'
1414
+ || normalized === 'run.failed') {
1415
+ return normalized;
1416
+ }
1417
+ return undefined;
1418
+ }
1419
+ function normalizeRuntimeDeliveryStatus(value) {
1420
+ const normalized = normalizeOptionalText(value);
1421
+ if (normalized === 'completed' || normalized === 'success' || normalized === 'succeeded' || normalized === 'ok') {
1422
+ return 'completed';
1423
+ }
1424
+ if (normalized === 'failed' || normalized === 'failure' || normalized === 'error') {
1425
+ return 'failed';
1426
+ }
1427
+ return undefined;
1428
+ }
1429
+ function normalizeRuntimeDeliveryExitCode(value) {
1430
+ if (typeof value === 'number' && Number.isFinite(value)) {
1431
+ return Math.trunc(value);
1432
+ }
1433
+ const normalized = normalizeOptionalText(value);
1434
+ if (!normalized) {
1435
+ return undefined;
1436
+ }
1437
+ const parsed = Number.parseInt(normalized, 10);
1438
+ return Number.isFinite(parsed) ? parsed : undefined;
1439
+ }
1440
+ function normalizeRuntimeDeliveryTimestamp(value) {
1441
+ const normalized = normalizeOptionalText(value);
1442
+ if (!normalized) {
1443
+ return undefined;
1444
+ }
1445
+ const parsed = new Date(normalized);
1446
+ return Number.isFinite(parsed.getTime()) ? parsed.toISOString() : normalized;
1447
+ }
1448
+ function parseJsonObject(text) {
1449
+ const normalized = normalizeOptionalText(text);
1450
+ if (!normalized) {
1451
+ return null;
1452
+ }
1453
+ try {
1454
+ return JSON.parse(normalized);
1455
+ }
1456
+ catch {
1457
+ return null;
1458
+ }
1459
+ }
1460
+ function normalizeFinalReportEnvelope(value) {
1461
+ if (!isRecord(value)) {
1462
+ return null;
1463
+ }
1464
+ const followUps = normalizeFollowUpCandidateList(value.followUps ?? value.follow_up_candidates ?? value.followUpCandidates);
1465
+ const evidence = normalizeStringList(value.evidence);
1466
+ const risks = normalizeStringList(value.risks);
1467
+ const changedFiles = normalizeStringList(value.changedFiles ?? value.changed_files);
1468
+ const commands = normalizeStringList(value.commands);
1469
+ const envelope = {
1470
+ ...(normalizeOptionalText(value.summary) ? { summary: normalizeOptionalText(value.summary) } : {}),
1471
+ evidence,
1472
+ risks,
1473
+ changedFiles,
1474
+ commands,
1475
+ followUps,
1476
+ };
1477
+ const hasEnvelopeSignal = value.symphonyFinal === true
1478
+ || envelope.summary
1479
+ || evidence.length > 0
1480
+ || risks.length > 0
1481
+ || changedFiles.length > 0
1482
+ || commands.length > 0
1483
+ || followUps.length > 0;
1484
+ return hasEnvelopeSignal ? envelope : null;
1485
+ }
1486
+ function normalizeFollowUpCandidateList(value) {
1487
+ if (!Array.isArray(value)) {
1488
+ return [];
1489
+ }
1490
+ return value
1491
+ .map((item) => normalizeFollowUpCandidateFromUnknown(item))
1492
+ .filter((item) => Boolean(item));
1493
+ }
1494
+ function normalizeFollowUpCandidateFromUnknown(value) {
1495
+ if (!isRecord(value)) {
1496
+ return null;
1497
+ }
1498
+ const summary = normalizeOptionalText(value.summary);
1499
+ if (!summary) {
1500
+ return null;
1501
+ }
1502
+ return normalizeSymphonyFollowUpCandidate({
1503
+ kind: normalizeFollowUpKind(value.kind),
1504
+ summary,
1505
+ evidence: normalizeStringList(value.evidence),
1506
+ ...(normalizeDisposition(value.suggestedDisposition ?? value.suggested_disposition ?? value.disposition) ? {
1507
+ suggestedDisposition: normalizeDisposition(value.suggestedDisposition ?? value.suggested_disposition ?? value.disposition),
1508
+ } : {}),
1509
+ ...(normalizeOptionalText(value.reason) ? { reason: normalizeOptionalText(value.reason) } : {}),
1510
+ ...(value.requiredBeforeAcceptance === true || value.required_before_acceptance === true ? { requiredBeforeAcceptance: true } : {}),
1511
+ ...(value.userDecisionRequired === true || value.user_decision_required === true ? { userDecisionRequired: true } : {}),
1512
+ ...(normalizeOptionalText(value.targetPackage ?? value.target_package) ? { targetPackage: normalizeOptionalText(value.targetPackage ?? value.target_package) } : {}),
1513
+ });
1514
+ }
290
1515
  function createSymphonyDispatchReconcilerState(input) {
291
1516
  const { summary } = decideThreadControlEvent({
292
1517
  policy: {
@@ -371,6 +1596,17 @@ function normalizeOptionalText(value) {
371
1596
  const normalized = typeof value === 'string' ? value.trim() : '';
372
1597
  return normalized || undefined;
373
1598
  }
1599
+ function normalizeStringList(value) {
1600
+ if (!Array.isArray(value)) {
1601
+ return [];
1602
+ }
1603
+ return value
1604
+ .map((item) => normalizeOptionalText(item))
1605
+ .filter((item) => Boolean(item));
1606
+ }
1607
+ function isRecord(value) {
1608
+ return typeof value === 'object' && value !== null;
1609
+ }
374
1610
  function normalizeSymphonyChatThreadRef(input) {
375
1611
  const chat = normalizeOptionalText(input.chat);
376
1612
  const thread = normalizeOptionalText(input.thread);
@@ -406,7 +1642,7 @@ function normalizeSymphonyWorkerWorkspace(root, override, backend) {
406
1642
  ...(normalizeOptionalText(override?.repository ?? root.repository) ? { repository: normalizeOptionalText(override?.repository ?? root.repository) } : {}),
407
1643
  ...(normalizeOptionalText(override?.branch ?? root.branch) ? { branch: normalizeOptionalText(override?.branch ?? root.branch) } : {}),
408
1644
  ...(normalizeOptionalText(override?.worktree ?? root.worktree) ? { worktree: normalizeOptionalText(override?.worktree ?? root.worktree) } : {}),
409
- ...(normalizeOptionalText(override?.workspace ?? root.workspace) ? { workspace: normalizeOptionalText(override?.workspace ?? root.workspace) } : {}),
1645
+ ...(normalizeOptionalText(override?.container ?? root.container) ? { container: normalizeOptionalText(override?.container ?? root.container) } : {}),
410
1646
  ...(normalizeOptionalText(override?.baseRevision ?? root.baseRevision) ? { baseRevision: normalizeOptionalText(override?.baseRevision ?? root.baseRevision) } : {}),
411
1647
  environment: normalizeSymphonyWorkerEnvironment(override?.environment ?? root.environment, backend),
412
1648
  };
@@ -489,14 +1725,12 @@ function createSymphonySupervisorPolicy(intervalMs) {
489
1725
  }
490
1726
  function normalizeSymphonyDelegationTarget(input) {
491
1727
  const explicit = input.target ?? {};
492
- const backend = explicit.backend ?? input.backend;
493
1728
  const chatThread = normalizeSymphonyChatThreadRef({
494
1729
  chat: explicit.chat ?? input.chat,
495
1730
  thread: explicit.thread ?? input.thread,
496
1731
  messages: explicit.messages ?? input.messages,
497
1732
  });
498
- const contact = normalizeOptionalText(explicit.contact) ?? normalizeOptionalText(explicit.agent) ?? backend;
499
- const agent = normalizeOptionalText(explicit.agent) ?? contact;
1733
+ const agent = normalizeOptionalText(explicit.agent) ?? `${input.backend}-worker`;
500
1734
  const label = normalizeOptionalText(explicit.label);
501
1735
  const source = explicit.source
502
1736
  ?? (chatThread.chat || chatThread.thread
@@ -506,8 +1740,7 @@ function normalizeSymphonyDelegationTarget(input) {
506
1740
  : 'default');
507
1741
  return {
508
1742
  source,
509
- backend,
510
- contact,
1743
+ backend: explicit.backend ?? input.backend,
511
1744
  agent,
512
1745
  ...(label ? { label } : {}),
513
1746
  ...chatThread,
@@ -518,7 +1751,21 @@ function formatSymphonyTimestamp(now = new Date()) {
518
1751
  }
519
1752
  function normalizeSymphonyRandomId(randomId) {
520
1753
  const normalized = typeof randomId === 'string'
521
- ? randomId.replace(/[^a-zA-Z0-9_-]/gu, '').slice(0, 12)
1754
+ ? randomId.replace(/[^a-zA-Z0-9_-]/gu, '')
522
1755
  : '';
523
- return normalized || Math.random().toString(36).slice(2, 10);
1756
+ if (!normalized) {
1757
+ return Math.random().toString(36).slice(2, 10);
1758
+ }
1759
+ if (normalized.length <= 12) {
1760
+ return normalized;
1761
+ }
1762
+ return `${normalized.slice(0, 6)}${hashSymphonyRandomId(normalized)}`;
1763
+ }
1764
+ function hashSymphonyRandomId(value) {
1765
+ let hash = 2166136261;
1766
+ for (let index = 0; index < value.length; index += 1) {
1767
+ hash ^= value.charCodeAt(index);
1768
+ hash = Math.imul(hash, 16777619);
1769
+ }
1770
+ return (hash >>> 0).toString(36).slice(0, 6).padStart(6, '0');
524
1771
  }