@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
@@ -7,7 +7,7 @@ import { decideThreadControlEvent } from '../../../vendor/agent-runtime/dist/thr
7
7
  import { createLinxPodSyncScope } from '../../../vendor/agent-runtime/dist/sync.js';
8
8
  import { insertExactRecordOnce, resolvePodResourceTemplateValue, upsertExactRecord, } from '@undefineds.co/drizzle-solid';
9
9
  import { getDefaultPodDataSession } from '../pod-data-session.js';
10
- import { ContactClass, ContactType, chatRepository, agentResource, contactResource, deliveryResource, ideaResource, issueResource, messageResource, runResource, runStepResource, sessionResource, taskResource, threadRepository, } from '../models.js';
10
+ import { ContactClass, ContactType, chatRepository, agentResource, contactResource, deliveryResource, ideaResource, issueResource, messageResource, reportResource, runResource, runStepResource, sessionResource, taskResource, threadRepository, } from '../models.js';
11
11
  import { pathToWorkspaceUri } from '../pi-adapter/pod-mirror-mapping.js';
12
12
  import { getSymphonyHome } from './archive.js';
13
13
  const SYMPHONY_CHAT_ID = 'symphony';
@@ -16,6 +16,191 @@ const SYMPHONY_CONTACT_ID = 'symphony';
16
16
  const SYMPHONY_POLICY_VERSION = 'linx-symphony-session/v1';
17
17
  const SYMPHONY_WORKER_POD_ACCESS_POLICY_VERSION = 'linx-symphony-worker-pod-access/v1';
18
18
  const SYMPHONY_ARCHIVE_PROVENANCE_VERSION = 'linx-symphony-archive/v1';
19
+ function ensureTrailingSlash(value) {
20
+ return value.endsWith('/') ? value : `${value}/`;
21
+ }
22
+ function podBaseUrlFromWebId(webId) {
23
+ const marker = '/profile/card#me';
24
+ if (webId.includes(marker)) {
25
+ return `${webId.slice(0, webId.indexOf(marker) + 1)}`;
26
+ }
27
+ const url = new URL(webId);
28
+ return `${url.origin}/`;
29
+ }
30
+ function podFileUrlFromWebId(webId, path) {
31
+ return new URL(path.replace(/^\/+/, ''), podBaseUrlFromWebId(webId)).toString();
32
+ }
33
+ function podFileUrl(podSession, path) {
34
+ return new URL(path.replace(/^\/+/, ''), ensureTrailingSlash(podSession.podUrl)).toString();
35
+ }
36
+ async function writePodFileToSession(session, file) {
37
+ const url = podFileUrl(session, file.path);
38
+ await ensurePodResourceContainers(session.fetch, url);
39
+ const response = await session.fetch(url, {
40
+ method: 'PUT',
41
+ headers: { 'Content-Type': file.contentType },
42
+ body: file.content.endsWith('\n') ? file.content : `${file.content}\n`,
43
+ });
44
+ if (!response.ok) {
45
+ const details = await response.text().catch(() => '');
46
+ const suffix = details.trim() ? ` - ${details.trim().slice(0, 500)}` : '';
47
+ throw new Error(`Failed to write Symphony Pod file ${url}: ${response.status} ${response.statusText}${suffix}`);
48
+ }
49
+ }
50
+ async function ensurePodResourceContainers(fetcher, resourceUrl) {
51
+ for (const containerUrl of containerUrlsForResource(resourceUrl)) {
52
+ const existing = await fetcher(containerUrl, { method: 'HEAD' }).catch(() => null);
53
+ if (existing?.ok)
54
+ continue;
55
+ if (existing && existing.status !== 404 && existing.status !== 405)
56
+ continue;
57
+ const response = await fetcher(containerUrl, {
58
+ method: 'PUT',
59
+ headers: {
60
+ Link: '<http://www.w3.org/ns/ldp#BasicContainer>; rel="type"',
61
+ 'Content-Type': 'text/turtle; charset=utf-8',
62
+ },
63
+ body: '',
64
+ });
65
+ if (!response.ok && response.status !== 409) {
66
+ throw new Error(`Failed to ensure Symphony Pod container ${containerUrl}: ${response.status} ${response.statusText}`);
67
+ }
68
+ }
69
+ }
70
+ function containerUrlsForResource(resourceUrl) {
71
+ const url = new URL(resourceUrl);
72
+ const parts = url.pathname.split('/').filter(Boolean);
73
+ const containers = [];
74
+ let path = '/';
75
+ for (let index = 0; index < parts.length - 1; index += 1) {
76
+ path += `${parts[index]}/`;
77
+ containers.push(new URL(path, url.origin).toString());
78
+ }
79
+ return containers;
80
+ }
81
+ function datePathParts(input) {
82
+ const date = safeDate(input);
83
+ return {
84
+ yyyy: String(date.getUTCFullYear()),
85
+ MM: String(date.getUTCMonth() + 1).padStart(2, '0'),
86
+ dd: String(date.getUTCDate()).padStart(2, '0'),
87
+ };
88
+ }
89
+ function buildSymphonyIssueDocumentPath(issue) {
90
+ return `/.data/issues/${buildSymphonyIssueId(issue)}/issue.md`;
91
+ }
92
+ function buildSymphonyIdeaDocumentPath(idea) {
93
+ const { yyyy, MM, dd } = datePathParts(idea.createdAt);
94
+ return `/.data/ideas/${yyyy}/${MM}/${dd}/${getSymphonyArchiveKey(idea.uri)}/idea.md`;
95
+ }
96
+ function buildSymphonyReportDocumentPath(worker) {
97
+ const { yyyy, MM, dd } = datePathParts(worker.session.completedAt ?? worker.session.updatedAt);
98
+ return `/.data/reports/${yyyy}/${MM}/${dd}/${getSymphonyArchiveKey(worker.session.uri)}-report.md`;
99
+ }
100
+ function renderMarkdownList(items) {
101
+ const values = (items ?? []).map((item) => item.trim()).filter(Boolean);
102
+ return values.length > 0 ? values.map((item) => `- ${item}`).join('\n') : '- None recorded.';
103
+ }
104
+ function renderSymphonyIssueMarkdown(plan) {
105
+ const issue = plan.issue;
106
+ const sections = [
107
+ `# ${issue.title || buildSymphonyIssueId(issue)}`,
108
+ '',
109
+ '## Summary',
110
+ issue.description?.trim() || issue.title || 'No summary recorded.',
111
+ '',
112
+ '## Status',
113
+ `- Status: ${issue.status}`,
114
+ `- Priority: ${issue.priority}`,
115
+ `- Source: ${issue.source}`,
116
+ '',
117
+ '## Acceptance Criteria',
118
+ renderMarkdownList(plan.workers.flatMap((worker) => worker.taskRecord.acceptanceCriteria ?? [])),
119
+ '',
120
+ '## Tasks',
121
+ renderMarkdownList(plan.workers.map((worker) => `${worker.taskRecord.title}: ${worker.taskRecord.objective}`)),
122
+ '',
123
+ '## Source Context',
124
+ `- Chat: ${issue.chat ?? plan.session.chat ?? 'not recorded'}`,
125
+ `- Thread: ${issue.thread ?? plan.session.thread ?? 'not recorded'}`,
126
+ `- Messages: ${(issue.messages ?? []).join(', ') || 'not recorded'}`,
127
+ '',
128
+ '## Control Records',
129
+ `- Issue: ${issue.uri}`,
130
+ ...plan.workers.flatMap((worker) => [
131
+ `- Task: ${worker.task}`,
132
+ `- Delivery: ${worker.delivery.uri}`,
133
+ `- Session: ${worker.session.uri}`,
134
+ ]),
135
+ ];
136
+ return sections.join('\n');
137
+ }
138
+ function renderSymphonyIdeaMarkdown(idea) {
139
+ return [
140
+ `# ${idea.summary || getSymphonyArchiveKey(idea.uri)}`,
141
+ '',
142
+ '## Input',
143
+ idea.input?.trim() || idea.summary || 'No input recorded.',
144
+ '',
145
+ '## Current Understanding',
146
+ idea.currentUnderstanding?.trim() || 'No current understanding recorded.',
147
+ '',
148
+ '## Open Questions',
149
+ renderMarkdownList(idea.openQuestions),
150
+ '',
151
+ '## Conflicts',
152
+ renderMarkdownList(idea.conflicts),
153
+ '',
154
+ '## Next Step',
155
+ idea.nextStep?.trim() || 'No next step recorded.',
156
+ '',
157
+ '## Source Context',
158
+ `- Status: ${idea.status}`,
159
+ `- Commitment: ${idea.commitment}`,
160
+ `- Chat: ${idea.chat ?? 'not recorded'}`,
161
+ `- Thread: ${idea.thread ?? 'not recorded'}`,
162
+ `- Messages: ${(idea.messages ?? []).join(', ') || 'not recorded'}`,
163
+ `- Idea: ${idea.uri}`,
164
+ ].join('\n');
165
+ }
166
+ function renderSymphonyReportMarkdown(plan, worker, stage) {
167
+ const status = worker.session.status === 'failed' || stage === 'failed' ? 'failed' : 'completed';
168
+ const summary = status === 'completed'
169
+ ? `${worker.taskRecord.title} completed.`
170
+ : `${worker.taskRecord.title} failed: ${worker.session.error ?? worker.delivery.error ?? 'worker did not complete successfully.'}`;
171
+ return [
172
+ `# ${worker.taskRecord.title} — ${status}`,
173
+ '',
174
+ '## Summary',
175
+ summary,
176
+ '',
177
+ '## Outcome',
178
+ `- Status: ${status}`,
179
+ `- Backend: ${worker.session.backend}`,
180
+ `- Agent: ${worker.session.target.agent ?? worker.delivery.targetAgent}`,
181
+ `- Auto mode session: ${worker.session.autoModeSessionId ?? 'not recorded'}`,
182
+ `- Exit code: ${worker.session.exitCode ?? 'not recorded'}`,
183
+ '',
184
+ '## Task',
185
+ worker.taskRecord.objective,
186
+ '',
187
+ '## Acceptance Criteria',
188
+ renderMarkdownList(worker.taskRecord.acceptanceCriteria),
189
+ '',
190
+ '## Linked Control Records',
191
+ `- Issue: ${buildSymphonyIssueId(plan.issue)}`,
192
+ `- Task: ${worker.task}`,
193
+ `- Delivery: ${worker.delivery.uri}`,
194
+ `- Session: ${worker.session.uri}`,
195
+ `- Run status: ${worker.session.status}`,
196
+ '',
197
+ ...(worker.session.error || worker.delivery.error || worker.taskRecord.error ? [
198
+ '## Error',
199
+ worker.session.error ?? worker.delivery.error ?? worker.taskRecord.error ?? '',
200
+ '',
201
+ ] : []),
202
+ ].join('\n');
203
+ }
19
204
  function normalizeSymphonyRunPlan(plan) {
20
205
  const workers = Array.isArray(plan.workers) && plan.workers.length > 0
21
206
  ? plan.workers
@@ -119,12 +304,14 @@ async function createDefaultRuntime() {
119
304
  issueResource: models.issueResource,
120
305
  taskResource: models.taskResource,
121
306
  deliveryResource: models.deliveryResource,
307
+ reportResource: models.reportResource,
122
308
  runResource: models.runResource,
123
309
  runStepResource: models.runStepResource,
124
310
  agentResource: models.agentResource,
125
311
  contactResource: models.contactTable,
126
312
  auditResource: models.auditResource,
127
313
  inboxNotificationResource: models.inboxNotificationResource,
314
+ writePodFile: writePodFileToSession,
128
315
  };
129
316
  }
130
317
  function selectTargetChatIri(value, webId, plan) {
@@ -211,6 +398,13 @@ function buildSymphonyReportDeliveryIri(webId, worker) {
211
398
  createdAt: safeDate(worker.session.completedAt ?? worker.session.updatedAt),
212
399
  });
213
400
  }
401
+ function buildSymphonyReportIri(webId, worker) {
402
+ return reportResource.buildIri(webId, {
403
+ id: `${getSymphonyArchiveKey(worker.session.uri)}-report`,
404
+ task: buildSymphonyTaskIri(webId, worker.task),
405
+ createdAt: safeDate(worker.session.completedAt ?? worker.session.updatedAt),
406
+ });
407
+ }
214
408
  function buildSymphonyRunIri(webId, worker) {
215
409
  return runResource.buildIri(webId, {
216
410
  id: getSymphonyArchiveKey(worker.session.uri),
@@ -808,8 +1002,11 @@ function buildSymphonyIssueRow(plan, webId) {
808
1002
  const updatedAt = safeDate(plan.issue.updatedAt);
809
1003
  return {
810
1004
  id: buildSymphonyIssueId(plan.issue),
1005
+ // File-primary: title remains a compact index label for existing Issue schemas.
1006
+ // The full problem statement and acceptance narrative live in issue.md.
811
1007
  title: plan.issue.title,
812
- description: plan.issue.description,
1008
+ document: podFileUrlFromWebId(webId, buildSymphonyIssueDocumentPath(plan.issue)),
1009
+ description: undefined,
813
1010
  status: plan.issue.status,
814
1011
  priority: plan.issue.priority,
815
1012
  labels: ['symphony'],
@@ -830,15 +1027,17 @@ function buildSymphonyIdeaRow(idea, webId) {
830
1027
  return {
831
1028
  id: getSymphonyArchiveKey(idea.uri),
832
1029
  summary: idea.summary,
833
- input: idea.input,
1030
+ document: podFileUrlFromWebId(webId, buildSymphonyIdeaDocumentPath(idea)),
1031
+ // File-primary: the source text lives in idea.md; TTL keeps only routing/index facts.
1032
+ input: undefined,
834
1033
  status: idea.status,
835
1034
  commitment: idea.commitment,
836
1035
  affectedArea: idea.affectedArea,
837
- currentUnderstanding: idea.currentUnderstanding,
838
- openQuestions: idea.openQuestions,
1036
+ currentUnderstanding: undefined,
1037
+ openQuestions: undefined,
839
1038
  related: idea.relatedRecords,
840
- conflicts: idea.conflicts,
841
- nextStep: idea.nextStep,
1039
+ conflicts: undefined,
1040
+ nextStep: undefined,
842
1041
  promotedTo: idea.promotedTo,
843
1042
  chat: idea.chat,
844
1043
  thread: idea.thread,
@@ -846,6 +1045,8 @@ function buildSymphonyIdeaRow(idea, webId) {
846
1045
  createdBy: webId,
847
1046
  metadata: {
848
1047
  surface: 'symphony',
1048
+ filePrimary: true,
1049
+ documentPath: buildSymphonyIdeaDocumentPath(idea),
849
1050
  ...buildSymphonyArchiveMetadata({ idea: idea.uri }),
850
1051
  },
851
1052
  createdAt,
@@ -965,6 +1166,61 @@ function buildSymphonyDeliveryRow(plan, webId, worker) {
965
1166
  updatedAt,
966
1167
  };
967
1168
  }
1169
+ function buildSymphonyReportRow(plan, webId, worker, stage) {
1170
+ const completedAt = safeDate(worker.session.completedAt ?? worker.session.updatedAt);
1171
+ const workerAgent = agentResource.buildIri(webId, {
1172
+ id: buildWorkerAgentId(worker.session.backend, worker.session.target.agent),
1173
+ });
1174
+ const run = buildSymphonyRunIri(webId, worker);
1175
+ const task = buildSymphonyTaskIri(webId, worker.task);
1176
+ const status = worker.session.status === 'failed' || stage === 'failed' ? 'failed' : 'completed';
1177
+ const summary = status === 'completed'
1178
+ ? `${worker.taskRecord.title} completed.`
1179
+ : `${worker.taskRecord.title} failed: ${worker.session.error ?? worker.delivery.error ?? 'worker did not complete successfully.'}`;
1180
+ return {
1181
+ id: reportResource.buildId({
1182
+ id: `${getSymphonyArchiveKey(worker.session.uri)}-report`,
1183
+ task,
1184
+ createdAt: completedAt,
1185
+ }),
1186
+ reportKind: 'handoff',
1187
+ status: 'published',
1188
+ outcome: status === 'completed' ? 'accepted' : 'blocked',
1189
+ about: run,
1190
+ issue: buildSymphonyIssueIri(webId, plan.issue),
1191
+ task,
1192
+ delivery: buildSymphonyDeliveryIri(webId, worker),
1193
+ run,
1194
+ thread: selectWorkerThreadIri(plan, webId, worker),
1195
+ evidence: [
1196
+ buildSymphonyRunStepIri(webId, worker, stage),
1197
+ ],
1198
+ summary,
1199
+ actor: workerAgent,
1200
+ source: podFileUrlFromWebId(webId, buildSymphonyReportDocumentPath(worker)),
1201
+ metricFacts: {
1202
+ backend: worker.session.backend,
1203
+ agent: worker.session.target.agent,
1204
+ autoModeSessionId: worker.session.autoModeSessionId,
1205
+ exitCode: worker.session.exitCode,
1206
+ },
1207
+ metadata: {
1208
+ surface: 'symphony',
1209
+ filePrimary: true,
1210
+ reportFile: buildSymphonyReportDocumentPath(worker),
1211
+ reportDelivery: buildSymphonyReportDeliveryIri(webId, worker),
1212
+ ...buildSymphonyArchiveMetadata({
1213
+ issue: plan.issue.uri,
1214
+ task: worker.task,
1215
+ delivery: worker.delivery.uri,
1216
+ session: worker.session.uri,
1217
+ }),
1218
+ },
1219
+ createdAt: completedAt,
1220
+ publishedAt: completedAt,
1221
+ updatedAt: completedAt,
1222
+ };
1223
+ }
968
1224
  function buildSymphonyReportDeliveryRow(plan, webId, worker, stage) {
969
1225
  const completedAt = safeDate(worker.session.completedAt ?? worker.session.updatedAt);
970
1226
  const workerAgent = agentResource.buildIri(webId, {
@@ -972,6 +1228,7 @@ function buildSymphonyReportDeliveryRow(plan, webId, worker, stage) {
972
1228
  });
973
1229
  const secretaryAgent = agentResource.buildIri(webId, { id: SYMPHONY_SECRETARY_AGENT_ID });
974
1230
  const run = buildSymphonyRunIri(webId, worker);
1231
+ const report = buildSymphonyReportIri(webId, worker);
975
1232
  const task = buildSymphonyTaskIri(webId, worker.task);
976
1233
  const originalDelivery = buildSymphonyDeliveryIri(webId, worker);
977
1234
  const status = worker.session.status === 'failed' || stage === 'failed' ? 'failed' : 'completed';
@@ -994,7 +1251,7 @@ function buildSymphonyReportDeliveryRow(plan, webId, worker, stage) {
994
1251
  targetThread: selectTargetThreadIri(plan.issue.thread ?? worker.session.target?.thread, webId, plan),
995
1252
  targetSession: buildSymphonyControlSessionUri(webId, plan),
996
1253
  actor: workerAgent,
997
- object: run,
1254
+ object: report,
998
1255
  objective: summary,
999
1256
  payload: {
1000
1257
  kind: 'symphony_report',
@@ -1003,6 +1260,7 @@ function buildSymphonyReportDeliveryRow(plan, webId, worker, stage) {
1003
1260
  issue: buildSymphonyIssueIri(webId, plan.issue),
1004
1261
  task,
1005
1262
  delivery: originalDelivery,
1263
+ report,
1006
1264
  reportDelivery: buildSymphonyReportDeliveryIri(webId, worker),
1007
1265
  session: buildSymphonyControlSessionUri(webId, plan),
1008
1266
  run,
@@ -1011,6 +1269,7 @@ function buildSymphonyReportDeliveryRow(plan, webId, worker, stage) {
1011
1269
  autoModeSessionId: worker.session.autoModeSessionId,
1012
1270
  exitCode: worker.session.exitCode,
1013
1271
  error: worker.session.error ?? worker.delivery.error ?? worker.taskRecord.error,
1272
+ reportFile: buildSymphonyReportDocumentPath(worker),
1014
1273
  evidence: {
1015
1274
  statusMessage: buildSymphonyMessageUri(webId, plan, buildStatusMessageRow(plan, webId, stage)),
1016
1275
  runStep: buildSymphonyRunStepIri(webId, worker, stage),
@@ -1030,6 +1289,8 @@ function buildSymphonyReportDeliveryRow(plan, webId, worker, stage) {
1030
1289
  session: worker.session.uri,
1031
1290
  }),
1032
1291
  reportKind: 'worker-completion',
1292
+ filePrimary: true,
1293
+ reportFile: buildSymphonyReportDocumentPath(worker),
1033
1294
  },
1034
1295
  error: status === 'failed' ? worker.session.error ?? worker.delivery.error ?? worker.taskRecord.error : undefined,
1035
1296
  createdAt: completedAt,
@@ -1371,6 +1632,7 @@ async function upsertIssue(db, runtime, row) {
1371
1632
  await upsertExactRecord(db, runtime.issueResource, { id: row.id }, row, {
1372
1633
  title: row.title,
1373
1634
  description: row.description,
1635
+ document: row.document,
1374
1636
  status: row.status,
1375
1637
  priority: row.priority,
1376
1638
  labels: row.labels,
@@ -1385,6 +1647,7 @@ async function upsertIssue(db, runtime, row) {
1385
1647
  async function upsertIdea(db, runtime, row) {
1386
1648
  await upsertExactRecord(db, runtime.ideaResource, { id: row.id }, row, {
1387
1649
  summary: row.summary,
1650
+ document: row.document,
1388
1651
  input: row.input,
1389
1652
  status: row.status,
1390
1653
  commitment: row.commitment,
@@ -1419,6 +1682,32 @@ async function upsertTask(db, runtime, row) {
1419
1682
  updatedAt: row.updatedAt,
1420
1683
  });
1421
1684
  }
1685
+ async function upsertReport(db, runtime, row) {
1686
+ await upsertExactRecord(db, runtime.reportResource, {
1687
+ id: row.id,
1688
+ task: row.task,
1689
+ createdAt: row.createdAt,
1690
+ }, row, {
1691
+ reportKind: row.reportKind,
1692
+ status: row.status,
1693
+ outcome: row.outcome,
1694
+ about: row.about,
1695
+ issue: row.issue,
1696
+ task: row.task,
1697
+ delivery: row.delivery,
1698
+ run: row.run,
1699
+ thread: row.thread,
1700
+ evidence: row.evidence,
1701
+ summary: row.summary,
1702
+ reviewer: row.reviewer,
1703
+ actor: row.actor,
1704
+ source: row.source,
1705
+ metricFacts: row.metricFacts,
1706
+ metadata: row.metadata,
1707
+ publishedAt: row.publishedAt,
1708
+ updatedAt: row.updatedAt,
1709
+ });
1710
+ }
1422
1711
  async function upsertDelivery(db, runtime, row) {
1423
1712
  await upsertExactRecord(db, runtime.deliveryResource, { id: row.id }, row, {
1424
1713
  kind: row.kind,
@@ -1538,6 +1827,7 @@ function collectSymphonyProjectionResources(webId, plan, stages) {
1538
1827
  add('runStep', buildSymphonyRunStepIri(webId, worker, stage));
1539
1828
  }
1540
1829
  if (worker.session.status === 'completed' || worker.session.status === 'failed') {
1830
+ add('report', buildSymphonyReportIri(webId, worker));
1541
1831
  add('delivery', buildSymphonyReportDeliveryIri(webId, worker));
1542
1832
  }
1543
1833
  }
@@ -1552,7 +1842,7 @@ function projectionStagesForStatus(status) {
1552
1842
  return ['planned', 'running', 'failed'];
1553
1843
  return ['planned'];
1554
1844
  }
1555
- export async function persistSymphonyProjectionToPod(plan, options = {}) {
1845
+ export async function persistSymphonyControlStateToPod(plan, options = {}) {
1556
1846
  const normalizedPlan = normalizeSymphonyRunPlan(plan);
1557
1847
  const runtime = options.runtime ?? await createDefaultRuntime();
1558
1848
  const podSession = await runtime.getPodDataSession();
@@ -1575,9 +1865,12 @@ export async function persistSymphonyProjectionToPod(plan, options = {}) {
1575
1865
  const projected = withTargetRefs(normalizedPlan, refs, podSession.webId);
1576
1866
  const resources = collectSymphonyProjectionResources(podSession.webId, projected, stages);
1577
1867
  const latestMessage = buildStatusMessageRow(projected, podSession.webId, stage);
1578
- const projectionSync = createLinxPodSyncScope({ source: 'symphony-run-plan' });
1579
- await projectionSync.runOperations({
1580
- action: 'symphony.project',
1868
+ const controlWrite = createLinxPodSyncScope({
1869
+ source: 'symphony-control-state',
1870
+ plane: 'control-plane',
1871
+ });
1872
+ await controlWrite.runOperations({
1873
+ action: 'symphony.write',
1581
1874
  resourceBindings: {
1582
1875
  session: {
1583
1876
  uri: buildSymphonyControlSessionUri(podSession.webId, projected),
@@ -1609,6 +1902,7 @@ export async function persistSymphonyProjectionToPod(plan, options = {}) {
1609
1902
  stages,
1610
1903
  latestMessage,
1611
1904
  shouldUpsertChat: !normalizedPlan.session.target?.chat,
1905
+ podSession,
1612
1906
  }),
1613
1907
  });
1614
1908
  return {
@@ -1617,6 +1911,8 @@ export async function persistSymphonyProjectionToPod(plan, options = {}) {
1617
1911
  resources,
1618
1912
  };
1619
1913
  }
1914
+ /** @deprecated Use persistSymphonyControlStateToPod for LinX-owned Symphony records. */
1915
+ export const persistSymphonyProjectionToPod = persistSymphonyControlStateToPod;
1620
1916
  export async function mirrorSymphonyProjectionJsonLdFromPod(projection, options = {}) {
1621
1917
  const runtime = options.runtime ?? await createDefaultRuntime();
1622
1918
  const podSession = await runtime.getPodDataSession();
@@ -1680,6 +1976,8 @@ function resolveProjectionResourceModel(runtime, kind) {
1680
1976
  return runtime.taskResource;
1681
1977
  if (kind === 'delivery')
1682
1978
  return runtime.deliveryResource;
1979
+ if (kind === 'report')
1980
+ return runtime.reportResource;
1683
1981
  if (kind === 'run')
1684
1982
  return runtime.runResource;
1685
1983
  if (kind === 'runStep')
@@ -1782,9 +2080,12 @@ export async function persistSymphonyIdeaToPod(idea, options = {}) {
1782
2080
  return null;
1783
2081
  }
1784
2082
  const db = runtime.createDb(podSession);
1785
- const ideaSync = createLinxPodSyncScope({ source: 'symphony-idea' });
1786
- await ideaSync.runOperations({
1787
- action: 'symphony.idea.project',
2083
+ const ideaWrite = createLinxPodSyncScope({
2084
+ source: 'symphony-control-state',
2085
+ plane: 'control-plane',
2086
+ });
2087
+ await ideaWrite.runOperations({
2088
+ action: 'symphony.idea.write',
1788
2089
  resourceBindings: {
1789
2090
  idea: {
1790
2091
  uri: ideaResource.buildIri(podSession.webId, {
@@ -1814,7 +2115,18 @@ export async function persistSymphonyIdeaToPod(idea, options = {}) {
1814
2115
  },
1815
2116
  },
1816
2117
  {
1817
- id: 'symphony.idea.upsert',
2118
+ id: 'symphony.idea.write-file-primary-document',
2119
+ kind: 'upsert',
2120
+ apply: async () => {
2121
+ await runtime.writePodFile?.(podSession, {
2122
+ path: buildSymphonyIdeaDocumentPath(idea),
2123
+ content: renderSymphonyIdeaMarkdown(idea),
2124
+ contentType: 'text/markdown; charset=utf-8',
2125
+ });
2126
+ },
2127
+ },
2128
+ {
2129
+ id: 'symphony.idea.upsert-meta',
1818
2130
  kind: 'upsert',
1819
2131
  apply: () => upsertIdea(db, runtime, buildSymphonyIdeaRow(idea, podSession.webId)),
1820
2132
  },
@@ -2094,6 +2406,7 @@ function buildSymphonyProjectionOperations(input) {
2094
2406
  input.runtime.issueResource,
2095
2407
  input.runtime.taskResource,
2096
2408
  input.runtime.deliveryResource,
2409
+ input.runtime.reportResource,
2097
2410
  input.runtime.runResource,
2098
2411
  input.runtime.runStepResource,
2099
2412
  input.runtime.agentResource,
@@ -2104,7 +2417,18 @@ function buildSymphonyProjectionOperations(input) {
2104
2417
  },
2105
2418
  },
2106
2419
  {
2107
- id: 'symphony.upsert-issue',
2420
+ id: 'symphony.write-file-primary-documents',
2421
+ kind: 'upsert',
2422
+ apply: async () => {
2423
+ await input.runtime.writePodFile?.(input.podSession, {
2424
+ path: buildSymphonyIssueDocumentPath(input.plan.issue),
2425
+ content: renderSymphonyIssueMarkdown(input.plan),
2426
+ contentType: 'text/markdown; charset=utf-8',
2427
+ });
2428
+ },
2429
+ },
2430
+ {
2431
+ id: 'symphony.upsert-issue-meta',
2108
2432
  kind: 'upsert',
2109
2433
  apply: () => upsertIssue(input.db, input.runtime, buildSymphonyIssueRow(input.plan, input.webId)),
2110
2434
  },
@@ -2177,6 +2501,22 @@ function buildSymphonyReportOperations(input) {
2177
2501
  }
2178
2502
  const terminalStage = input.stage;
2179
2503
  return input.plan.workers.flatMap((worker, index) => [
2504
+ {
2505
+ id: `symphony.write-report-file:${index + 1}`,
2506
+ kind: 'upsert',
2507
+ apply: async () => {
2508
+ await input.runtime.writePodFile?.(input.podSession, {
2509
+ path: buildSymphonyReportDocumentPath(worker),
2510
+ content: renderSymphonyReportMarkdown(input.plan, worker, terminalStage),
2511
+ contentType: 'text/markdown; charset=utf-8',
2512
+ });
2513
+ },
2514
+ },
2515
+ {
2516
+ id: `symphony.upsert-report:${index + 1}`,
2517
+ kind: 'upsert',
2518
+ apply: () => upsertReport(input.db, input.runtime, buildSymphonyReportRow(input.plan, input.webId, worker, terminalStage)),
2519
+ },
2180
2520
  {
2181
2521
  id: `symphony.upsert-report-delivery:${index + 1}`,
2182
2522
  kind: 'upsert',