@undefineds.co/linx 0.3.16 → 0.3.17

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.
@@ -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, EvidenceKind, chatRepository, agentResource, contactResource, deliveryResource, evidenceResource, 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,240 @@ 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 buildSymphonyEvidenceDocumentPath(worker, stage) {
101
+ const { yyyy, MM, dd } = datePathParts(worker.session.completedAt ?? worker.session.updatedAt);
102
+ return `/.data/evidence/${yyyy}/${MM}/${dd}/${getSymphonyArchiveKey(worker.session.uri)}-${stage}-evidence.md`;
103
+ }
104
+ function renderMarkdownList(items) {
105
+ const values = (items ?? []).map((item) => item.trim()).filter(Boolean);
106
+ return values.length > 0 ? values.map((item) => `- ${item}`).join('\n') : '- None recorded.';
107
+ }
108
+ function renderSymphonyIssueMarkdown(plan) {
109
+ const issue = plan.issue;
110
+ const sections = [
111
+ `# ${issue.title || buildSymphonyIssueId(issue)}`,
112
+ '',
113
+ '## Summary',
114
+ issue.description?.trim() || issue.title || 'No summary recorded.',
115
+ '',
116
+ '## Status',
117
+ `- Status: ${issue.status}`,
118
+ `- Priority: ${issue.priority}`,
119
+ `- Source: ${issue.source}`,
120
+ '',
121
+ '## Acceptance Criteria',
122
+ renderMarkdownList(plan.workers.flatMap((worker) => worker.taskRecord.acceptanceCriteria ?? [])),
123
+ '',
124
+ '## Tasks',
125
+ renderMarkdownList(plan.workers.map((worker) => `${worker.taskRecord.title}: ${worker.taskRecord.objective}`)),
126
+ '',
127
+ '## Source Context',
128
+ `- Chat: ${issue.chat ?? plan.session.chat ?? 'not recorded'}`,
129
+ `- Thread: ${issue.thread ?? plan.session.thread ?? 'not recorded'}`,
130
+ `- Messages: ${(issue.messages ?? []).join(', ') || 'not recorded'}`,
131
+ '',
132
+ '## Control Records',
133
+ `- Issue: ${issue.uri}`,
134
+ ...plan.workers.flatMap((worker) => [
135
+ `- Task: ${worker.task}`,
136
+ `- Delivery: ${worker.delivery.uri}`,
137
+ `- Session: ${worker.session.uri}`,
138
+ ]),
139
+ ];
140
+ return sections.join('\n');
141
+ }
142
+ function renderSymphonyIdeaMarkdown(idea) {
143
+ return [
144
+ `# ${idea.summary || getSymphonyArchiveKey(idea.uri)}`,
145
+ '',
146
+ '## Input',
147
+ idea.input?.trim() || idea.summary || 'No input recorded.',
148
+ '',
149
+ '## Current Understanding',
150
+ idea.currentUnderstanding?.trim() || 'No current understanding recorded.',
151
+ '',
152
+ '## Open Questions',
153
+ renderMarkdownList(idea.openQuestions),
154
+ '',
155
+ '## Conflicts',
156
+ renderMarkdownList(idea.conflicts),
157
+ '',
158
+ '## Next Step',
159
+ idea.nextStep?.trim() || 'No next step recorded.',
160
+ '',
161
+ '## Source Context',
162
+ `- Status: ${idea.status}`,
163
+ `- Commitment: ${idea.commitment}`,
164
+ `- Chat: ${idea.chat ?? 'not recorded'}`,
165
+ `- Thread: ${idea.thread ?? 'not recorded'}`,
166
+ `- Messages: ${(idea.messages ?? []).join(', ') || 'not recorded'}`,
167
+ `- Idea: ${idea.uri}`,
168
+ ].join('\n');
169
+ }
170
+ function renderSymphonyReportMarkdown(plan, worker, stage) {
171
+ const status = worker.session.status === 'failed' || stage === 'failed' ? 'failed' : 'completed';
172
+ const summary = status === 'completed'
173
+ ? `${worker.taskRecord.title} completed.`
174
+ : `${worker.taskRecord.title} failed: ${worker.session.error ?? worker.delivery.error ?? 'worker did not complete successfully.'}`;
175
+ return [
176
+ `# ${worker.taskRecord.title} — ${status}`,
177
+ '',
178
+ '## Summary',
179
+ summary,
180
+ '',
181
+ '## Outcome',
182
+ `- Status: ${status}`,
183
+ `- Backend: ${worker.session.backend}`,
184
+ `- Agent: ${worker.session.target.agent ?? worker.delivery.targetAgent}`,
185
+ `- Auto mode session: ${worker.session.autoModeSessionId ?? 'not recorded'}`,
186
+ `- Exit code: ${worker.session.exitCode ?? 'not recorded'}`,
187
+ '',
188
+ '## Task',
189
+ worker.taskRecord.objective,
190
+ '',
191
+ '## Acceptance Criteria',
192
+ renderMarkdownList(worker.taskRecord.acceptanceCriteria),
193
+ '',
194
+ '## Linked Control Records',
195
+ `- Issue: ${buildSymphonyIssueId(plan.issue)}`,
196
+ `- Task: ${worker.task}`,
197
+ `- Delivery: ${worker.delivery.uri}`,
198
+ `- Session: ${worker.session.uri}`,
199
+ `- Run status: ${worker.session.status}`,
200
+ '',
201
+ '## Post-Run Reconciliation',
202
+ '- Secretary must review this report and linked Evidence before closing the task.',
203
+ '- Classify any follow-up as same_issue_task, new_issue, idea, evidence_only, or ask_user.',
204
+ '',
205
+ ...(worker.session.error || worker.delivery.error || worker.taskRecord.error ? [
206
+ '## Error',
207
+ worker.session.error ?? worker.delivery.error ?? worker.taskRecord.error ?? '',
208
+ '',
209
+ ] : []),
210
+ ].join('\n');
211
+ }
212
+ function renderSymphonyEvidenceMarkdown(plan, webId, worker, stage) {
213
+ const status = worker.session.status === 'failed' || stage === 'failed' ? 'failed' : 'completed';
214
+ const summary = status === 'completed'
215
+ ? `${worker.taskRecord.title} completed with runtime status ${worker.session.status}.`
216
+ : `${worker.taskRecord.title} failed: ${worker.session.error ?? worker.delivery.error ?? worker.taskRecord.error ?? 'worker did not complete successfully.'}`;
217
+ const run = buildSymphonyRunIri(webId, worker);
218
+ const runStep = buildSymphonyRunStepIri(webId, worker, stage);
219
+ return [
220
+ `# ${worker.taskRecord.title} — ${status} evidence`,
221
+ '',
222
+ '## Summary',
223
+ summary,
224
+ '',
225
+ '## Runtime Facts',
226
+ `- Backend: ${worker.session.backend}`,
227
+ `- Agent: ${worker.session.target.agent ?? worker.delivery.targetAgent}`,
228
+ `- Model: ${worker.session.model ?? 'not recorded'}`,
229
+ `- Auto mode session: ${worker.session.autoModeSessionId ?? 'not recorded'}`,
230
+ `- Exit code: ${worker.session.exitCode ?? 'not recorded'}`,
231
+ `- Run status: ${worker.session.status}`,
232
+ '',
233
+ '## Acceptance Criteria',
234
+ renderMarkdownList(worker.taskRecord.acceptanceCriteria),
235
+ '',
236
+ '## Linked Control Records',
237
+ `- Issue: ${buildSymphonyIssueIri(webId, plan.issue)}`,
238
+ `- Task: ${buildSymphonyTaskIri(webId, worker.task)}`,
239
+ `- Delivery: ${buildSymphonyDeliveryIri(webId, worker)}`,
240
+ `- Run: ${run}`,
241
+ `- Source RunStep: ${runStep}`,
242
+ `- Worker Thread: ${selectWorkerThreadIri(plan, webId, worker)}`,
243
+ '',
244
+ ...(worker.session.error || worker.delivery.error || worker.taskRecord.error ? [
245
+ '## Error',
246
+ worker.session.error ?? worker.delivery.error ?? worker.taskRecord.error ?? '',
247
+ '',
248
+ ] : []),
249
+ '## Secretary Follow-Up Review',
250
+ 'This Evidence is append-only proof/finding material. Secretary must use it with the Report and RunSteps to decide acceptance and whether follow-up work should be captured.',
251
+ ].join('\n');
252
+ }
19
253
  function normalizeSymphonyRunPlan(plan) {
20
254
  const workers = Array.isArray(plan.workers) && plan.workers.length > 0
21
255
  ? plan.workers
@@ -119,12 +353,15 @@ async function createDefaultRuntime() {
119
353
  issueResource: models.issueResource,
120
354
  taskResource: models.taskResource,
121
355
  deliveryResource: models.deliveryResource,
356
+ evidenceResource: models.evidenceResource,
357
+ reportResource: models.reportResource,
122
358
  runResource: models.runResource,
123
359
  runStepResource: models.runStepResource,
124
360
  agentResource: models.agentResource,
125
- contactResource: models.contactResource,
361
+ contactResource: models.contactTable,
126
362
  auditResource: models.auditResource,
127
363
  inboxNotificationResource: models.inboxNotificationResource,
364
+ writePodFile: writePodFileToSession,
128
365
  };
129
366
  }
130
367
  function selectTargetChatIri(value, webId, plan) {
@@ -211,6 +448,19 @@ function buildSymphonyReportDeliveryIri(webId, worker) {
211
448
  createdAt: safeDate(worker.session.completedAt ?? worker.session.updatedAt),
212
449
  });
213
450
  }
451
+ function buildSymphonyReportIri(webId, worker) {
452
+ return reportResource.buildIri(webId, {
453
+ id: `${getSymphonyArchiveKey(worker.session.uri)}-report`,
454
+ task: buildSymphonyTaskIri(webId, worker.task),
455
+ createdAt: safeDate(worker.session.completedAt ?? worker.session.updatedAt),
456
+ });
457
+ }
458
+ function buildSymphonyEvidenceIri(webId, worker, stage) {
459
+ return evidenceResource.buildIri(webId, {
460
+ id: `${getSymphonyArchiveKey(worker.session.uri)}-${stage}`,
461
+ createdAt: safeDate(worker.session.completedAt ?? worker.session.updatedAt),
462
+ });
463
+ }
214
464
  function buildSymphonyRunIri(webId, worker) {
215
465
  return runResource.buildIri(webId, {
216
466
  id: getSymphonyArchiveKey(worker.session.uri),
@@ -808,8 +1058,11 @@ function buildSymphonyIssueRow(plan, webId) {
808
1058
  const updatedAt = safeDate(plan.issue.updatedAt);
809
1059
  return {
810
1060
  id: buildSymphonyIssueId(plan.issue),
1061
+ // File-primary: title remains a compact index label for existing Issue schemas.
1062
+ // The full problem statement and acceptance narrative live in issue.md.
811
1063
  title: plan.issue.title,
812
- description: plan.issue.description,
1064
+ document: podFileUrlFromWebId(webId, buildSymphonyIssueDocumentPath(plan.issue)),
1065
+ description: undefined,
813
1066
  status: plan.issue.status,
814
1067
  priority: plan.issue.priority,
815
1068
  labels: ['symphony'],
@@ -830,15 +1083,17 @@ function buildSymphonyIdeaRow(idea, webId) {
830
1083
  return {
831
1084
  id: getSymphonyArchiveKey(idea.uri),
832
1085
  summary: idea.summary,
833
- input: idea.input,
1086
+ document: podFileUrlFromWebId(webId, buildSymphonyIdeaDocumentPath(idea)),
1087
+ // File-primary: the source text lives in idea.md; TTL keeps only routing/index facts.
1088
+ input: undefined,
834
1089
  status: idea.status,
835
1090
  commitment: idea.commitment,
836
1091
  affectedArea: idea.affectedArea,
837
- currentUnderstanding: idea.currentUnderstanding,
838
- openQuestions: idea.openQuestions,
1092
+ currentUnderstanding: undefined,
1093
+ openQuestions: undefined,
839
1094
  related: idea.relatedRecords,
840
- conflicts: idea.conflicts,
841
- nextStep: idea.nextStep,
1095
+ conflicts: undefined,
1096
+ nextStep: undefined,
842
1097
  promotedTo: idea.promotedTo,
843
1098
  chat: idea.chat,
844
1099
  thread: idea.thread,
@@ -846,6 +1101,8 @@ function buildSymphonyIdeaRow(idea, webId) {
846
1101
  createdBy: webId,
847
1102
  metadata: {
848
1103
  surface: 'symphony',
1104
+ filePrimary: true,
1105
+ documentPath: buildSymphonyIdeaDocumentPath(idea),
849
1106
  ...buildSymphonyArchiveMetadata({ idea: idea.uri }),
850
1107
  },
851
1108
  createdAt,
@@ -965,6 +1222,138 @@ function buildSymphonyDeliveryRow(plan, webId, worker) {
965
1222
  updatedAt,
966
1223
  };
967
1224
  }
1225
+ function buildSymphonyReportRow(plan, webId, worker, stage) {
1226
+ const completedAt = safeDate(worker.session.completedAt ?? worker.session.updatedAt);
1227
+ const workerAgent = agentResource.buildIri(webId, {
1228
+ id: buildWorkerAgentId(worker.session.backend, worker.session.target.agent),
1229
+ });
1230
+ const run = buildSymphonyRunIri(webId, worker);
1231
+ const task = buildSymphonyTaskIri(webId, worker.task);
1232
+ const status = worker.session.status === 'failed' || stage === 'failed' ? 'failed' : 'completed';
1233
+ const summary = status === 'completed'
1234
+ ? `${worker.taskRecord.title} completed.`
1235
+ : `${worker.taskRecord.title} failed: ${worker.session.error ?? worker.delivery.error ?? 'worker did not complete successfully.'}`;
1236
+ const evidence = buildSymphonyEvidenceIri(webId, worker, stage);
1237
+ return {
1238
+ id: reportResource.buildId({
1239
+ id: `${getSymphonyArchiveKey(worker.session.uri)}-report`,
1240
+ task,
1241
+ createdAt: completedAt,
1242
+ }),
1243
+ reportKind: 'handoff',
1244
+ status: 'published',
1245
+ outcome: status === 'completed' ? 'accepted' : 'blocked',
1246
+ about: run,
1247
+ issue: buildSymphonyIssueIri(webId, plan.issue),
1248
+ task,
1249
+ delivery: buildSymphonyDeliveryIri(webId, worker),
1250
+ run,
1251
+ thread: selectWorkerThreadIri(plan, webId, worker),
1252
+ evidence: [
1253
+ evidence,
1254
+ ],
1255
+ summary,
1256
+ actor: workerAgent,
1257
+ source: podFileUrlFromWebId(webId, buildSymphonyReportDocumentPath(worker)),
1258
+ metricFacts: {
1259
+ backend: worker.session.backend,
1260
+ agent: worker.session.target.agent,
1261
+ autoModeSessionId: worker.session.autoModeSessionId,
1262
+ exitCode: worker.session.exitCode,
1263
+ },
1264
+ metadata: {
1265
+ surface: 'symphony',
1266
+ filePrimary: true,
1267
+ reportFile: buildSymphonyReportDocumentPath(worker),
1268
+ reportDelivery: buildSymphonyReportDeliveryIri(webId, worker),
1269
+ postRunReconciliation: buildPostRunReconciliationMetadata(plan, webId, worker, stage),
1270
+ ...buildSymphonyArchiveMetadata({
1271
+ issue: plan.issue.uri,
1272
+ task: worker.task,
1273
+ delivery: worker.delivery.uri,
1274
+ session: worker.session.uri,
1275
+ }),
1276
+ },
1277
+ createdAt: completedAt,
1278
+ publishedAt: completedAt,
1279
+ updatedAt: completedAt,
1280
+ };
1281
+ }
1282
+ function buildSymphonyEvidenceRow(plan, webId, worker, stage) {
1283
+ const createdAt = safeDate(worker.session.completedAt ?? worker.session.updatedAt);
1284
+ const status = worker.session.status === 'failed' || stage === 'failed' ? 'failed' : 'completed';
1285
+ const run = buildSymphonyRunIri(webId, worker);
1286
+ const task = buildSymphonyTaskIri(webId, worker.task);
1287
+ const delivery = buildSymphonyDeliveryIri(webId, worker);
1288
+ const runStep = buildSymphonyRunStepIri(webId, worker, stage);
1289
+ const workerAgent = agentResource.buildIri(webId, {
1290
+ id: buildWorkerAgentId(worker.session.backend, worker.session.target.agent),
1291
+ });
1292
+ return {
1293
+ id: evidenceResource.buildId({
1294
+ id: `${getSymphonyArchiveKey(worker.session.uri)}-${stage}`,
1295
+ createdAt,
1296
+ }),
1297
+ evidenceKind: EvidenceKind.RUNTIME_LOG,
1298
+ about: run,
1299
+ issue: buildSymphonyIssueIri(webId, plan.issue),
1300
+ task,
1301
+ delivery,
1302
+ run,
1303
+ thread: selectWorkerThreadIri(plan, webId, worker),
1304
+ summary: status === 'completed'
1305
+ ? `${worker.taskRecord.title} completed with runtime status ${worker.session.status}.`
1306
+ : `${worker.taskRecord.title} failed: ${worker.session.error ?? worker.delivery.error ?? worker.taskRecord.error ?? 'worker did not complete successfully.'}`,
1307
+ source: podFileUrlFromWebId(webId, buildSymphonyEvidenceDocumentPath(worker, stage)),
1308
+ actor: workerAgent,
1309
+ outcome: status,
1310
+ metadata: {
1311
+ surface: 'symphony',
1312
+ filePrimary: true,
1313
+ evidenceFile: buildSymphonyEvidenceDocumentPath(worker, stage),
1314
+ sourceRunStep: runStep,
1315
+ report: buildSymphonyReportIri(webId, worker),
1316
+ reportDelivery: buildSymphonyReportDeliveryIri(webId, worker),
1317
+ postRunReconciliation: buildPostRunReconciliationMetadata(plan, webId, worker, stage),
1318
+ runtime: {
1319
+ backend: worker.session.backend,
1320
+ model: worker.session.model,
1321
+ autoModeSessionId: worker.session.autoModeSessionId,
1322
+ exitCode: worker.session.exitCode,
1323
+ status: worker.session.status,
1324
+ },
1325
+ ...buildSymphonyArchiveMetadata({
1326
+ issue: plan.issue.uri,
1327
+ task: worker.task,
1328
+ delivery: worker.delivery.uri,
1329
+ session: worker.session.uri,
1330
+ }),
1331
+ },
1332
+ createdAt,
1333
+ };
1334
+ }
1335
+ function buildPostRunReconciliationMetadata(plan, webId, worker, stage) {
1336
+ return {
1337
+ required: true,
1338
+ status: 'pending_secretary_review',
1339
+ owner: agentResource.buildIri(webId, { id: SYMPHONY_SECRETARY_AGENT_ID }),
1340
+ sourceIssue: buildSymphonyIssueIri(webId, plan.issue),
1341
+ sourceTask: buildSymphonyTaskIri(webId, worker.task),
1342
+ sourceDelivery: buildSymphonyDeliveryIri(webId, worker),
1343
+ sourceRun: buildSymphonyRunIri(webId, worker),
1344
+ sourceRunStep: buildSymphonyRunStepIri(webId, worker, stage),
1345
+ sourceEvidence: buildSymphonyEvidenceIri(webId, worker, stage),
1346
+ sourceReport: buildSymphonyReportIri(webId, worker),
1347
+ classifications: [
1348
+ 'same_issue_task',
1349
+ 'new_issue',
1350
+ 'idea',
1351
+ 'evidence_only',
1352
+ 'ask_user',
1353
+ ],
1354
+ rule: 'Secretary must reconcile acceptance and follow-up before task closure.',
1355
+ };
1356
+ }
968
1357
  function buildSymphonyReportDeliveryRow(plan, webId, worker, stage) {
969
1358
  const completedAt = safeDate(worker.session.completedAt ?? worker.session.updatedAt);
970
1359
  const workerAgent = agentResource.buildIri(webId, {
@@ -972,12 +1361,15 @@ function buildSymphonyReportDeliveryRow(plan, webId, worker, stage) {
972
1361
  });
973
1362
  const secretaryAgent = agentResource.buildIri(webId, { id: SYMPHONY_SECRETARY_AGENT_ID });
974
1363
  const run = buildSymphonyRunIri(webId, worker);
1364
+ const report = buildSymphonyReportIri(webId, worker);
975
1365
  const task = buildSymphonyTaskIri(webId, worker.task);
976
1366
  const originalDelivery = buildSymphonyDeliveryIri(webId, worker);
977
1367
  const status = worker.session.status === 'failed' || stage === 'failed' ? 'failed' : 'completed';
978
1368
  const summary = status === 'completed'
979
1369
  ? `${worker.taskRecord.title} completed.`
980
1370
  : `${worker.taskRecord.title} failed: ${worker.session.error ?? worker.delivery.error ?? 'worker did not complete successfully.'}`;
1371
+ const evidence = buildSymphonyEvidenceIri(webId, worker, stage);
1372
+ const sourceRunStep = buildSymphonyRunStepIri(webId, worker, stage);
981
1373
  return {
982
1374
  id: deliveryResource.buildId({
983
1375
  id: `${getSymphonyArchiveKey(worker.session.uri)}-report`,
@@ -994,7 +1386,7 @@ function buildSymphonyReportDeliveryRow(plan, webId, worker, stage) {
994
1386
  targetThread: selectTargetThreadIri(plan.issue.thread ?? worker.session.target?.thread, webId, plan),
995
1387
  targetSession: buildSymphonyControlSessionUri(webId, plan),
996
1388
  actor: workerAgent,
997
- object: run,
1389
+ object: report,
998
1390
  objective: summary,
999
1391
  payload: {
1000
1392
  kind: 'symphony_report',
@@ -1003,6 +1395,7 @@ function buildSymphonyReportDeliveryRow(plan, webId, worker, stage) {
1003
1395
  issue: buildSymphonyIssueIri(webId, plan.issue),
1004
1396
  task,
1005
1397
  delivery: originalDelivery,
1398
+ report,
1006
1399
  reportDelivery: buildSymphonyReportDeliveryIri(webId, worker),
1007
1400
  session: buildSymphonyControlSessionUri(webId, plan),
1008
1401
  run,
@@ -1011,9 +1404,13 @@ function buildSymphonyReportDeliveryRow(plan, webId, worker, stage) {
1011
1404
  autoModeSessionId: worker.session.autoModeSessionId,
1012
1405
  exitCode: worker.session.exitCode,
1013
1406
  error: worker.session.error ?? worker.delivery.error ?? worker.taskRecord.error,
1407
+ reportFile: buildSymphonyReportDocumentPath(worker),
1408
+ evidenceFile: buildSymphonyEvidenceDocumentPath(worker, stage),
1409
+ postRunReconciliation: buildPostRunReconciliationMetadata(plan, webId, worker, stage),
1014
1410
  evidence: {
1015
1411
  statusMessage: buildSymphonyMessageUri(webId, plan, buildStatusMessageRow(plan, webId, stage)),
1016
- runStep: buildSymphonyRunStepIri(webId, worker, stage),
1412
+ sourceRunStep,
1413
+ evidence,
1017
1414
  },
1018
1415
  },
1019
1416
  projection: {
@@ -1030,6 +1427,11 @@ function buildSymphonyReportDeliveryRow(plan, webId, worker, stage) {
1030
1427
  session: worker.session.uri,
1031
1428
  }),
1032
1429
  reportKind: 'worker-completion',
1430
+ filePrimary: true,
1431
+ reportFile: buildSymphonyReportDocumentPath(worker),
1432
+ evidence,
1433
+ evidenceFile: buildSymphonyEvidenceDocumentPath(worker, stage),
1434
+ postRunReconciliation: buildPostRunReconciliationMetadata(plan, webId, worker, stage),
1033
1435
  },
1034
1436
  error: status === 'failed' ? worker.session.error ?? worker.delivery.error ?? worker.taskRecord.error : undefined,
1035
1437
  createdAt: completedAt,
@@ -1371,6 +1773,7 @@ async function upsertIssue(db, runtime, row) {
1371
1773
  await upsertExactRecord(db, runtime.issueResource, { id: row.id }, row, {
1372
1774
  title: row.title,
1373
1775
  description: row.description,
1776
+ document: row.document,
1374
1777
  status: row.status,
1375
1778
  priority: row.priority,
1376
1779
  labels: row.labels,
@@ -1385,6 +1788,7 @@ async function upsertIssue(db, runtime, row) {
1385
1788
  async function upsertIdea(db, runtime, row) {
1386
1789
  await upsertExactRecord(db, runtime.ideaResource, { id: row.id }, row, {
1387
1790
  summary: row.summary,
1791
+ document: row.document,
1388
1792
  input: row.input,
1389
1793
  status: row.status,
1390
1794
  commitment: row.commitment,
@@ -1419,6 +1823,51 @@ async function upsertTask(db, runtime, row) {
1419
1823
  updatedAt: row.updatedAt,
1420
1824
  });
1421
1825
  }
1826
+ async function upsertReport(db, runtime, row) {
1827
+ await upsertExactRecord(db, runtime.reportResource, {
1828
+ id: row.id,
1829
+ task: row.task,
1830
+ createdAt: row.createdAt,
1831
+ }, row, {
1832
+ reportKind: row.reportKind,
1833
+ status: row.status,
1834
+ outcome: row.outcome,
1835
+ about: row.about,
1836
+ issue: row.issue,
1837
+ task: row.task,
1838
+ delivery: row.delivery,
1839
+ run: row.run,
1840
+ thread: row.thread,
1841
+ evidence: row.evidence,
1842
+ summary: row.summary,
1843
+ reviewer: row.reviewer,
1844
+ actor: row.actor,
1845
+ source: row.source,
1846
+ metricFacts: row.metricFacts,
1847
+ metadata: row.metadata,
1848
+ publishedAt: row.publishedAt,
1849
+ updatedAt: row.updatedAt,
1850
+ });
1851
+ }
1852
+ async function upsertEvidence(db, runtime, row) {
1853
+ await upsertExactRecord(db, runtime.evidenceResource, {
1854
+ id: row.id,
1855
+ createdAt: row.createdAt,
1856
+ }, row, {
1857
+ evidenceKind: row.evidenceKind,
1858
+ about: row.about,
1859
+ issue: row.issue,
1860
+ task: row.task,
1861
+ delivery: row.delivery,
1862
+ run: row.run,
1863
+ thread: row.thread,
1864
+ summary: row.summary,
1865
+ source: row.source,
1866
+ actor: row.actor,
1867
+ outcome: row.outcome,
1868
+ metadata: row.metadata,
1869
+ });
1870
+ }
1422
1871
  async function upsertDelivery(db, runtime, row) {
1423
1872
  await upsertExactRecord(db, runtime.deliveryResource, { id: row.id }, row, {
1424
1873
  kind: row.kind,
@@ -1538,6 +1987,9 @@ function collectSymphonyProjectionResources(webId, plan, stages) {
1538
1987
  add('runStep', buildSymphonyRunStepIri(webId, worker, stage));
1539
1988
  }
1540
1989
  if (worker.session.status === 'completed' || worker.session.status === 'failed') {
1990
+ const terminalStage = worker.session.status === 'failed' ? 'failed' : 'completed';
1991
+ add('evidence', buildSymphonyEvidenceIri(webId, worker, terminalStage));
1992
+ add('report', buildSymphonyReportIri(webId, worker));
1541
1993
  add('delivery', buildSymphonyReportDeliveryIri(webId, worker));
1542
1994
  }
1543
1995
  }
@@ -1612,6 +2064,7 @@ export async function persistSymphonyControlStateToPod(plan, options = {}) {
1612
2064
  stages,
1613
2065
  latestMessage,
1614
2066
  shouldUpsertChat: !normalizedPlan.session.target?.chat,
2067
+ podSession,
1615
2068
  }),
1616
2069
  });
1617
2070
  return {
@@ -1685,6 +2138,10 @@ function resolveProjectionResourceModel(runtime, kind) {
1685
2138
  return runtime.taskResource;
1686
2139
  if (kind === 'delivery')
1687
2140
  return runtime.deliveryResource;
2141
+ if (kind === 'evidence')
2142
+ return runtime.evidenceResource;
2143
+ if (kind === 'report')
2144
+ return runtime.reportResource;
1688
2145
  if (kind === 'run')
1689
2146
  return runtime.runResource;
1690
2147
  if (kind === 'runStep')
@@ -1822,7 +2279,18 @@ export async function persistSymphonyIdeaToPod(idea, options = {}) {
1822
2279
  },
1823
2280
  },
1824
2281
  {
1825
- id: 'symphony.idea.upsert',
2282
+ id: 'symphony.idea.write-file-primary-document',
2283
+ kind: 'upsert',
2284
+ apply: async () => {
2285
+ await runtime.writePodFile?.(podSession, {
2286
+ path: buildSymphonyIdeaDocumentPath(idea),
2287
+ content: renderSymphonyIdeaMarkdown(idea),
2288
+ contentType: 'text/markdown; charset=utf-8',
2289
+ });
2290
+ },
2291
+ },
2292
+ {
2293
+ id: 'symphony.idea.upsert-meta',
1826
2294
  kind: 'upsert',
1827
2295
  apply: () => upsertIdea(db, runtime, buildSymphonyIdeaRow(idea, podSession.webId)),
1828
2296
  },
@@ -2102,6 +2570,8 @@ function buildSymphonyProjectionOperations(input) {
2102
2570
  input.runtime.issueResource,
2103
2571
  input.runtime.taskResource,
2104
2572
  input.runtime.deliveryResource,
2573
+ input.runtime.evidenceResource,
2574
+ input.runtime.reportResource,
2105
2575
  input.runtime.runResource,
2106
2576
  input.runtime.runStepResource,
2107
2577
  input.runtime.agentResource,
@@ -2112,7 +2582,18 @@ function buildSymphonyProjectionOperations(input) {
2112
2582
  },
2113
2583
  },
2114
2584
  {
2115
- id: 'symphony.upsert-issue',
2585
+ id: 'symphony.write-file-primary-documents',
2586
+ kind: 'upsert',
2587
+ apply: async () => {
2588
+ await input.runtime.writePodFile?.(input.podSession, {
2589
+ path: buildSymphonyIssueDocumentPath(input.plan.issue),
2590
+ content: renderSymphonyIssueMarkdown(input.plan),
2591
+ contentType: 'text/markdown; charset=utf-8',
2592
+ });
2593
+ },
2594
+ },
2595
+ {
2596
+ id: 'symphony.upsert-issue-meta',
2116
2597
  kind: 'upsert',
2117
2598
  apply: () => upsertIssue(input.db, input.runtime, buildSymphonyIssueRow(input.plan, input.webId)),
2118
2599
  },
@@ -2185,6 +2666,38 @@ function buildSymphonyReportOperations(input) {
2185
2666
  }
2186
2667
  const terminalStage = input.stage;
2187
2668
  return input.plan.workers.flatMap((worker, index) => [
2669
+ {
2670
+ id: `symphony.write-evidence-file:${index + 1}`,
2671
+ kind: 'upsert',
2672
+ apply: async () => {
2673
+ await input.runtime.writePodFile?.(input.podSession, {
2674
+ path: buildSymphonyEvidenceDocumentPath(worker, terminalStage),
2675
+ content: renderSymphonyEvidenceMarkdown(input.plan, input.webId, worker, terminalStage),
2676
+ contentType: 'text/markdown; charset=utf-8',
2677
+ });
2678
+ },
2679
+ },
2680
+ {
2681
+ id: `symphony.upsert-evidence:${index + 1}`,
2682
+ kind: 'upsert',
2683
+ apply: () => upsertEvidence(input.db, input.runtime, buildSymphonyEvidenceRow(input.plan, input.webId, worker, terminalStage)),
2684
+ },
2685
+ {
2686
+ id: `symphony.write-report-file:${index + 1}`,
2687
+ kind: 'upsert',
2688
+ apply: async () => {
2689
+ await input.runtime.writePodFile?.(input.podSession, {
2690
+ path: buildSymphonyReportDocumentPath(worker),
2691
+ content: renderSymphonyReportMarkdown(input.plan, worker, terminalStage),
2692
+ contentType: 'text/markdown; charset=utf-8',
2693
+ });
2694
+ },
2695
+ },
2696
+ {
2697
+ id: `symphony.upsert-report:${index + 1}`,
2698
+ kind: 'upsert',
2699
+ apply: () => upsertReport(input.db, input.runtime, buildSymphonyReportRow(input.plan, input.webId, worker, terminalStage)),
2700
+ },
2188
2701
  {
2189
2702
  id: `symphony.upsert-report-delivery:${index + 1}`,
2190
2703
  kind: 'upsert',
@@ -2218,6 +2731,7 @@ export const __symphonyPodProjectionInternal = {
2218
2731
  buildSymphonyIdeaRow,
2219
2732
  buildSymphonyTaskRow,
2220
2733
  buildSymphonyDeliveryRow,
2734
+ buildSymphonyEvidenceRow,
2221
2735
  buildSymphonyReportDeliveryRow,
2222
2736
  buildSymphonyReportInboxNotificationRow,
2223
2737
  buildSymphonyRunRow,