sentinelayer-cli 0.8.0 → 0.8.1

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 (153) hide show
  1. package/README.md +13 -0
  2. package/package.json +4 -4
  3. package/src/agents/ai-governance/index.js +12 -0
  4. package/src/agents/ai-governance/tools/base.js +171 -0
  5. package/src/agents/ai-governance/tools/eval-regression.js +47 -0
  6. package/src/agents/ai-governance/tools/hitl-audit.js +81 -0
  7. package/src/agents/ai-governance/tools/index.js +52 -0
  8. package/src/agents/ai-governance/tools/prompt-drift.js +42 -0
  9. package/src/agents/ai-governance/tools/provenance-check.js +69 -0
  10. package/src/agents/backend/index.js +12 -0
  11. package/src/agents/backend/tools/base.js +189 -0
  12. package/src/agents/backend/tools/circuit-breaker-check.js +123 -0
  13. package/src/agents/backend/tools/idempotency-audit.js +105 -0
  14. package/src/agents/backend/tools/index.js +87 -0
  15. package/src/agents/backend/tools/retry-audit.js +132 -0
  16. package/src/agents/backend/tools/timeout-audit.js +144 -0
  17. package/src/agents/code-quality/index.js +12 -0
  18. package/src/agents/code-quality/tools/base.js +159 -0
  19. package/src/agents/code-quality/tools/complexity-measure.js +197 -0
  20. package/src/agents/code-quality/tools/coupling-analysis.js +81 -0
  21. package/src/agents/code-quality/tools/cycle-detect.js +49 -0
  22. package/src/agents/code-quality/tools/dep-graph.js +196 -0
  23. package/src/agents/code-quality/tools/index.js +89 -0
  24. package/src/agents/data-layer/index.js +12 -0
  25. package/src/agents/data-layer/tools/base.js +181 -0
  26. package/src/agents/data-layer/tools/index-audit.js +165 -0
  27. package/src/agents/data-layer/tools/index.js +83 -0
  28. package/src/agents/data-layer/tools/migration-scan.js +135 -0
  29. package/src/agents/data-layer/tools/query-explain.js +120 -0
  30. package/src/agents/data-layer/tools/tenancy-scan.js +166 -0
  31. package/src/agents/documentation/index.js +12 -0
  32. package/src/agents/documentation/tools/api-diff.js +91 -0
  33. package/src/agents/documentation/tools/base.js +151 -0
  34. package/src/agents/documentation/tools/dead-link-check.js +58 -0
  35. package/src/agents/documentation/tools/docstring-coverage.js +78 -0
  36. package/src/agents/documentation/tools/index.js +52 -0
  37. package/src/agents/documentation/tools/readme-freshness.js +61 -0
  38. package/src/agents/envelope/fix-cycle.js +45 -0
  39. package/src/agents/envelope/index.js +31 -0
  40. package/src/agents/envelope/loop.js +150 -0
  41. package/src/agents/envelope/pulse.js +18 -0
  42. package/src/agents/envelope/stream.js +40 -0
  43. package/src/agents/infrastructure/index.js +12 -0
  44. package/src/agents/infrastructure/tools/base.js +171 -0
  45. package/src/agents/infrastructure/tools/checkov-run.js +32 -0
  46. package/src/agents/infrastructure/tools/drift-detect.js +59 -0
  47. package/src/agents/infrastructure/tools/iam-least-priv-check.js +78 -0
  48. package/src/agents/infrastructure/tools/index.js +52 -0
  49. package/src/agents/infrastructure/tools/tflint-run.js +31 -0
  50. package/src/agents/jules/loop.js +7 -4
  51. package/src/agents/jules/swarm/sub-agent.js +5 -1
  52. package/src/agents/jules/tools/auth-audit.js +10 -1
  53. package/src/agents/mode.js +113 -0
  54. package/src/agents/observability/index.js +12 -0
  55. package/src/agents/observability/tools/alert-audit.js +39 -0
  56. package/src/agents/observability/tools/base.js +181 -0
  57. package/src/agents/observability/tools/dashboard-gap.js +42 -0
  58. package/src/agents/observability/tools/index.js +54 -0
  59. package/src/agents/observability/tools/log-schema-check.js +74 -0
  60. package/src/agents/observability/tools/span-coverage.js +74 -0
  61. package/src/agents/persona-visuals.js +38 -0
  62. package/src/agents/release/index.js +12 -0
  63. package/src/agents/release/tools/base.js +181 -0
  64. package/src/agents/release/tools/changelog-diff.js +86 -0
  65. package/src/agents/release/tools/feature-flag-audit.js +126 -0
  66. package/src/agents/release/tools/index.js +61 -0
  67. package/src/agents/release/tools/rollback-verify.js +129 -0
  68. package/src/agents/release/tools/semver-check.js +109 -0
  69. package/src/agents/reliability/index.js +12 -0
  70. package/src/agents/reliability/tools/backpressure-check.js +129 -0
  71. package/src/agents/reliability/tools/base.js +181 -0
  72. package/src/agents/reliability/tools/chaos-probe.js +109 -0
  73. package/src/agents/reliability/tools/graceful-degradation-check.js +114 -0
  74. package/src/agents/reliability/tools/health-check-audit.js +111 -0
  75. package/src/agents/reliability/tools/index.js +87 -0
  76. package/src/agents/run-persona.js +109 -0
  77. package/src/agents/security/index.js +12 -0
  78. package/src/agents/security/tools/authz-audit.js +134 -0
  79. package/src/agents/security/tools/base.js +190 -0
  80. package/src/agents/security/tools/crypto-review.js +175 -0
  81. package/src/agents/security/tools/index.js +97 -0
  82. package/src/agents/security/tools/sast-scan.js +175 -0
  83. package/src/agents/security/tools/secrets-scan.js +216 -0
  84. package/src/agents/supply-chain/index.js +12 -0
  85. package/src/agents/supply-chain/tools/attestation-check.js +42 -0
  86. package/src/agents/supply-chain/tools/base.js +151 -0
  87. package/src/agents/supply-chain/tools/index.js +52 -0
  88. package/src/agents/supply-chain/tools/lockfile-integrity.js +73 -0
  89. package/src/agents/supply-chain/tools/package-verify.js +56 -0
  90. package/src/agents/supply-chain/tools/sbom-diff.js +34 -0
  91. package/src/agents/testing/index.js +12 -0
  92. package/src/agents/testing/tools/base.js +202 -0
  93. package/src/agents/testing/tools/coverage-gap.js +144 -0
  94. package/src/agents/testing/tools/flake-detect.js +125 -0
  95. package/src/agents/testing/tools/index.js +85 -0
  96. package/src/agents/testing/tools/mutation-test.js +143 -0
  97. package/src/agents/testing/tools/snapshot-diff.js +103 -0
  98. package/src/auth/gate.js +65 -37
  99. package/src/cli.js +1 -1
  100. package/src/commands/chat.js +3 -10
  101. package/src/commands/legacy-args.js +10 -0
  102. package/src/commands/omargate.js +36 -2
  103. package/src/commands/persona.js +46 -1
  104. package/src/commands/scan.js +3 -10
  105. package/src/commands/session.js +654 -6
  106. package/src/commands/spec.js +3 -10
  107. package/src/coord/events-log.js +141 -0
  108. package/src/coord/handshake.js +719 -0
  109. package/src/coord/index.js +35 -0
  110. package/src/coord/paths.js +84 -0
  111. package/src/coord/priority.js +62 -0
  112. package/src/coord/tarjan.js +157 -0
  113. package/src/cost/tokenizer.js +160 -0
  114. package/src/cost/tracker.js +61 -0
  115. package/src/daemon/artifact-lineage.js +362 -0
  116. package/src/daemon/assignment-ledger.js +117 -0
  117. package/src/daemon/ast-drift.js +496 -0
  118. package/src/daemon/ingest-refresh.js +69 -2
  119. package/src/ingest/engine.js +15 -0
  120. package/src/ingest/ownership.js +380 -0
  121. package/src/legacy-cli.js +68 -1
  122. package/src/orchestrator/kai-chen.js +126 -0
  123. package/src/review/ai-review.js +3 -10
  124. package/src/review/compliance-pack.js +389 -0
  125. package/src/review/investor-dd-config.js +54 -0
  126. package/src/review/investor-dd-file-loop.js +303 -0
  127. package/src/review/investor-dd-file-router.js +406 -0
  128. package/src/review/investor-dd-html-report.js +233 -0
  129. package/src/review/investor-dd-notification.js +120 -0
  130. package/src/review/investor-dd-orchestrator.js +405 -0
  131. package/src/review/investor-dd-persona-runner.js +275 -0
  132. package/src/review/live-validator.js +253 -0
  133. package/src/review/omargate-orchestrator.js +90 -2
  134. package/src/review/persona-prompts.js +244 -56
  135. package/src/review/reconciliation-rules.js +329 -0
  136. package/src/review/reproducibility-chain.js +136 -0
  137. package/src/review/scan-modes.js +102 -3
  138. package/src/session/agent-registry.js +7 -0
  139. package/src/session/analytics.js +479 -0
  140. package/src/session/daemon.js +609 -14
  141. package/src/session/file-locks.js +666 -0
  142. package/src/session/paths.js +4 -0
  143. package/src/session/recap.js +567 -0
  144. package/src/session/redact.js +82 -0
  145. package/src/session/runtime-bridge.js +24 -1
  146. package/src/session/scoring.js +406 -0
  147. package/src/session/setup-guides.js +304 -0
  148. package/src/session/store.js +318 -2
  149. package/src/session/stream.js +9 -1
  150. package/src/session/sync.js +753 -0
  151. package/src/session/tasks.js +1054 -0
  152. package/src/session/templates.js +188 -0
  153. package/src/swarm/runtime.js +1 -8
@@ -1,3 +1,4 @@
1
+ import { createHash } from "node:crypto";
1
2
  import fsp from "node:fs/promises";
2
3
  import path from "node:path";
3
4
 
@@ -6,6 +7,7 @@ import { getBudgetHealthColor, resolveOperatorControlStorage } from "./operator-
6
7
  import { listBudgetStates, resolveBudgetGovernorStorage } from "./budget-governor.js";
7
8
  import { listErrorQueue, resolveErrorDaemonStorage, WORK_ITEM_STATUSES } from "./error-worker.js";
8
9
  import { listJiraIssues, resolveJiraLifecycleStorage } from "./jira-lifecycle.js";
10
+ import { resolveSessionPaths } from "../session/paths.js";
9
11
 
10
12
  const LINEAGE_SCHEMA_VERSION = "1.0.0";
11
13
  const WORK_ITEM_STATUS_SET = new Set(WORK_ITEM_STATUSES);
@@ -46,6 +48,348 @@ function toRelativePosix(baseDir, absolutePath) {
46
48
  return toPosixPath(relative);
47
49
  }
48
50
 
51
+ function normalizeDateKey(value = "", fallbackIso = new Date().toISOString()) {
52
+ const normalized = normalizeString(value);
53
+ if (normalized && /^\d{4}-\d{2}-\d{2}$/.test(normalized)) {
54
+ return normalized;
55
+ }
56
+ return normalizeIsoTimestamp(normalized, fallbackIso).slice(0, 10);
57
+ }
58
+
59
+ function buildDayKey(nowIso = new Date().toISOString()) {
60
+ return normalizeDateKey(nowIso, nowIso);
61
+ }
62
+
63
+ function normalizeStringArray(values = []) {
64
+ if (!Array.isArray(values)) {
65
+ return [];
66
+ }
67
+ return Array.from(
68
+ new Set(
69
+ values
70
+ .map((value) => normalizeString(value))
71
+ .filter(Boolean)
72
+ )
73
+ );
74
+ }
75
+
76
+ async function readFileBufferOptional(filePath) {
77
+ try {
78
+ return await fsp.readFile(filePath);
79
+ } catch (error) {
80
+ if (error && typeof error === "object" && error.code === "ENOENT") {
81
+ return null;
82
+ }
83
+ throw error;
84
+ }
85
+ }
86
+
87
+ async function hashFileSha256(filePath) {
88
+ const payload = await fsp.readFile(filePath);
89
+ return createHash("sha256").update(payload).digest("hex");
90
+ }
91
+
92
+ function resolveWorkItemArtifactDir(storage, workItemId, date) {
93
+ return path.join(
94
+ storage.observabilityRoot,
95
+ normalizeDateKey(date, new Date().toISOString()),
96
+ normalizeString(workItemId)
97
+ );
98
+ }
99
+
100
+ function canonicalizeArtifactRecords(artifactFiles = []) {
101
+ return artifactFiles
102
+ .map((artifact) => ({
103
+ name: normalizeString(artifact.name),
104
+ path: toPosixPath(normalizeString(artifact.path)),
105
+ sha256: normalizeString(artifact.sha256).toLowerCase(),
106
+ sizeBytes: Number(artifact.sizeBytes || 0),
107
+ }))
108
+ .sort((left, right) => left.path.localeCompare(right.path));
109
+ }
110
+
111
+ function buildCloseoutAnchorPayload({
112
+ workItemId,
113
+ sessionId,
114
+ date,
115
+ artifacts = [],
116
+ sessionStream = null,
117
+ cosignAttestationRef = "",
118
+ sbomRef = "",
119
+ evidenceLinks = [],
120
+ } = {}) {
121
+ return {
122
+ workItemId: normalizeString(workItemId),
123
+ sessionId: normalizeString(sessionId) || null,
124
+ date: normalizeDateKey(date, new Date().toISOString()),
125
+ artifacts: canonicalizeArtifactRecords(artifacts),
126
+ sessionStream: sessionStream
127
+ ? {
128
+ path: toPosixPath(normalizeString(sessionStream.path)),
129
+ sha256: normalizeString(sessionStream.sha256).toLowerCase(),
130
+ sizeBytes: Number(sessionStream.sizeBytes || 0),
131
+ }
132
+ : null,
133
+ cosignAttestationRef: normalizeString(cosignAttestationRef) || null,
134
+ sbomRef: normalizeString(sbomRef) || null,
135
+ evidenceLinks: normalizeStringArray(evidenceLinks).sort((left, right) =>
136
+ left.localeCompare(right)
137
+ ),
138
+ };
139
+ }
140
+
141
+ function computeAnchorSha256(payload = {}) {
142
+ return createHash("sha256")
143
+ .update(JSON.stringify(payload))
144
+ .digest("hex");
145
+ }
146
+
147
+ function resolveArtifactAbsolutePath(storage, artifactPath = "") {
148
+ const normalized = normalizeString(artifactPath);
149
+ if (!normalized) {
150
+ return "";
151
+ }
152
+ if (path.isAbsolute(normalized)) {
153
+ return normalized;
154
+ }
155
+ return path.join(storage.outputRoot, normalized);
156
+ }
157
+
158
+ async function resolveSessionStreamDigest(sessionId, { targetPath = "." } = {}) {
159
+ const normalizedSessionId = normalizeString(sessionId);
160
+ if (!normalizedSessionId) {
161
+ return null;
162
+ }
163
+
164
+ const sessionPaths = resolveSessionPaths(normalizedSessionId, { targetPath });
165
+ const [rotatedBuffer, streamBuffer] = await Promise.all([
166
+ readFileBufferOptional(sessionPaths.rotatedStreamPath),
167
+ readFileBufferOptional(sessionPaths.streamPath),
168
+ ]);
169
+ if (!rotatedBuffer && !streamBuffer) {
170
+ return null;
171
+ }
172
+
173
+ const hash = createHash("sha256");
174
+ let totalBytes = 0;
175
+ if (rotatedBuffer) {
176
+ hash.update(rotatedBuffer);
177
+ totalBytes += rotatedBuffer.length;
178
+ }
179
+ if (streamBuffer) {
180
+ hash.update(streamBuffer);
181
+ totalBytes += streamBuffer.length;
182
+ }
183
+ return {
184
+ path: toPosixPath(path.relative(path.resolve(String(targetPath || ".")), sessionPaths.streamPath)),
185
+ sha256: hash.digest("hex"),
186
+ sizeBytes: totalBytes,
187
+ };
188
+ }
189
+
190
+ export async function writeCloseoutArtifact({
191
+ workItemId,
192
+ sessionId = "",
193
+ date = "",
194
+ targetPath = ".",
195
+ outputDir = "",
196
+ env,
197
+ homeDir,
198
+ nowIso = new Date().toISOString(),
199
+ cosignAttestationRef = "",
200
+ sbomRef = "",
201
+ evidenceLinks = [],
202
+ chainVerified = true,
203
+ } = {}) {
204
+ const normalizedWorkItemId = normalizeString(workItemId);
205
+ if (!normalizedWorkItemId) {
206
+ throw new Error("workItemId is required.");
207
+ }
208
+ const normalizedTargetPath = path.resolve(String(targetPath || "."));
209
+ const normalizedNow = normalizeIsoTimestamp(nowIso, new Date().toISOString());
210
+ const storage = await resolveArtifactLineageStorage({
211
+ targetPath: normalizedTargetPath,
212
+ outputDir,
213
+ env,
214
+ homeDir,
215
+ });
216
+ const dateKey = normalizeDateKey(date, normalizedNow);
217
+ const artifactDir = resolveWorkItemArtifactDir(storage, normalizedWorkItemId, dateKey);
218
+ await fsp.mkdir(artifactDir, { recursive: true });
219
+ const closeoutPath = path.join(artifactDir, "closeout.json");
220
+
221
+ const entries = await fsp.readdir(artifactDir, { withFileTypes: true }).catch((error) => {
222
+ if (error && typeof error === "object" && error.code === "ENOENT") {
223
+ return [];
224
+ }
225
+ throw error;
226
+ });
227
+ const artifactFiles = [];
228
+ for (const entry of entries) {
229
+ if (!entry.isFile()) {
230
+ continue;
231
+ }
232
+ const lower = entry.name.toLowerCase();
233
+ if (lower === "closeout.json") {
234
+ continue;
235
+ }
236
+ const absolutePath = path.join(artifactDir, entry.name);
237
+ const stat = await fsp.stat(absolutePath);
238
+ artifactFiles.push({
239
+ name: entry.name,
240
+ path: toRelativePosix(storage.outputRoot, absolutePath),
241
+ sha256: await hashFileSha256(absolutePath),
242
+ sizeBytes: Number(stat.size || 0),
243
+ });
244
+ }
245
+ const sessionStream = await resolveSessionStreamDigest(normalizeString(sessionId), {
246
+ targetPath: normalizedTargetPath,
247
+ });
248
+ const anchorPayload = buildCloseoutAnchorPayload({
249
+ workItemId: normalizedWorkItemId,
250
+ sessionId,
251
+ date: dateKey,
252
+ artifacts: artifactFiles,
253
+ sessionStream,
254
+ cosignAttestationRef,
255
+ sbomRef,
256
+ evidenceLinks,
257
+ });
258
+ const anchorSha256 = computeAnchorSha256(anchorPayload);
259
+ const payload = {
260
+ schemaVersion: "1.0.0",
261
+ generatedAt: normalizedNow,
262
+ chainVerified: Boolean(chainVerified),
263
+ workItemId: normalizedWorkItemId,
264
+ sessionId: normalizeString(sessionId) || null,
265
+ date: dateKey,
266
+ artifactDir: toRelativePosix(storage.outputRoot, artifactDir),
267
+ artifacts: canonicalizeArtifactRecords(artifactFiles),
268
+ sessionStream,
269
+ cosignAttestationRef: normalizeString(cosignAttestationRef) || null,
270
+ sbomRef: normalizeString(sbomRef) || null,
271
+ evidenceLinks: normalizeStringArray(evidenceLinks).sort((left, right) =>
272
+ left.localeCompare(right)
273
+ ),
274
+ anchorSha256,
275
+ };
276
+ await writeJsonFile(closeoutPath, payload);
277
+ return {
278
+ closeoutPath,
279
+ payload,
280
+ anchorSha256,
281
+ artifactCount: payload.artifacts.length,
282
+ };
283
+ }
284
+
285
+ export async function verifyArtifactChain({
286
+ workItemId,
287
+ date = "",
288
+ targetPath = ".",
289
+ outputDir = "",
290
+ env,
291
+ homeDir,
292
+ } = {}) {
293
+ const normalizedWorkItemId = normalizeString(workItemId);
294
+ if (!normalizedWorkItemId) {
295
+ throw new Error("workItemId is required.");
296
+ }
297
+ const normalizedTargetPath = path.resolve(String(targetPath || "."));
298
+ const storage = await resolveArtifactLineageStorage({
299
+ targetPath: normalizedTargetPath,
300
+ outputDir,
301
+ env,
302
+ homeDir,
303
+ });
304
+ const dateKey = normalizeDateKey(date, new Date().toISOString());
305
+ const artifactDir = resolveWorkItemArtifactDir(storage, normalizedWorkItemId, dateKey);
306
+ const closeoutPath = path.join(artifactDir, "closeout.json");
307
+ const closeout = await readJsonFile(closeoutPath, null);
308
+ if (!closeout || typeof closeout !== "object") {
309
+ throw new Error(`closeout.json was not found for work item '${normalizedWorkItemId}' on '${dateKey}'.`);
310
+ }
311
+
312
+ const mismatches = [];
313
+ const artifacts = Array.isArray(closeout.artifacts) ? closeout.artifacts : [];
314
+ for (const artifact of artifacts) {
315
+ const expectedPath = normalizeString(artifact.path);
316
+ const absolutePath = resolveArtifactAbsolutePath(storage, expectedPath);
317
+ if (!absolutePath) {
318
+ mismatches.push({
319
+ type: "artifact_path_missing",
320
+ path: expectedPath,
321
+ });
322
+ continue;
323
+ }
324
+ const buffer = await readFileBufferOptional(absolutePath);
325
+ if (!buffer) {
326
+ mismatches.push({
327
+ type: "artifact_missing",
328
+ path: expectedPath,
329
+ });
330
+ continue;
331
+ }
332
+ const actualSha256 = createHash("sha256").update(buffer).digest("hex");
333
+ if (normalizeString(artifact.sha256).toLowerCase() !== actualSha256) {
334
+ mismatches.push({
335
+ type: "artifact_sha_mismatch",
336
+ path: expectedPath,
337
+ expected: normalizeString(artifact.sha256).toLowerCase(),
338
+ actual: actualSha256,
339
+ });
340
+ }
341
+ }
342
+
343
+ let sessionStream = null;
344
+ const sessionId = normalizeString(closeout.sessionId);
345
+ if (sessionId) {
346
+ sessionStream = await resolveSessionStreamDigest(sessionId, {
347
+ targetPath: normalizedTargetPath,
348
+ });
349
+ if (closeout.sessionStream && sessionStream) {
350
+ const expectedStreamSha = normalizeString(closeout.sessionStream.sha256).toLowerCase();
351
+ if (expectedStreamSha !== normalizeString(sessionStream.sha256).toLowerCase()) {
352
+ mismatches.push({
353
+ type: "session_stream_sha_mismatch",
354
+ path: normalizeString(closeout.sessionStream.path),
355
+ expected: expectedStreamSha,
356
+ actual: normalizeString(sessionStream.sha256).toLowerCase(),
357
+ });
358
+ }
359
+ }
360
+ }
361
+
362
+ const recomputedAnchorPayload = buildCloseoutAnchorPayload({
363
+ workItemId: normalizedWorkItemId,
364
+ sessionId,
365
+ date: closeout.date,
366
+ artifacts,
367
+ sessionStream: sessionStream || closeout.sessionStream || null,
368
+ cosignAttestationRef: closeout.cosignAttestationRef,
369
+ sbomRef: closeout.sbomRef,
370
+ evidenceLinks: closeout.evidenceLinks,
371
+ });
372
+ const recomputedAnchorSha256 = computeAnchorSha256(recomputedAnchorPayload);
373
+ if (normalizeString(closeout.anchorSha256).toLowerCase() !== recomputedAnchorSha256) {
374
+ mismatches.push({
375
+ type: "anchor_sha_mismatch",
376
+ expected: normalizeString(closeout.anchorSha256).toLowerCase(),
377
+ actual: recomputedAnchorSha256,
378
+ });
379
+ }
380
+
381
+ return {
382
+ valid: mismatches.length === 0,
383
+ closeoutPath,
384
+ workItemId: normalizedWorkItemId,
385
+ date: normalizeDateKey(closeout.date, dateKey),
386
+ mismatches,
387
+ artifactCount: artifacts.length,
388
+ anchorSha256: normalizeString(closeout.anchorSha256).toLowerCase(),
389
+ recomputedAnchorSha256,
390
+ };
391
+ }
392
+
49
393
  async function writeJsonFile(filePath, payload = {}) {
50
394
  await fsp.mkdir(path.dirname(filePath), { recursive: true });
51
395
  await fsp.writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
@@ -345,6 +689,7 @@ export async function buildArtifactLineageIndex({
345
689
  firstSeenAt: queueItem.firstSeenAt,
346
690
  lastSeenAt: queueItem.lastSeenAt,
347
691
  links: {
692
+ sessionId: assignment?.sessionId || null,
348
693
  agentIdentity: assignment?.assignedAgentIdentity || null,
349
694
  assignmentStatus: assignment?.status || null,
350
695
  assignmentStage: assignment?.stage || null,
@@ -370,6 +715,23 @@ export async function buildArtifactLineageIndex({
370
715
  };
371
716
  });
372
717
 
718
+ await Promise.all(
719
+ workItems.map(async (workItem) => {
720
+ const closeout = await writeCloseoutArtifact({
721
+ workItemId: workItem.workItemId,
722
+ sessionId: normalizeString(workItem.links.sessionId),
723
+ date: buildDayKey(normalizedNow),
724
+ targetPath,
725
+ outputDir,
726
+ env,
727
+ homeDir,
728
+ nowIso: normalizedNow,
729
+ });
730
+ workItem.artifacts.closeoutPath = toRelativePosix(storage.outputRoot, closeout.closeoutPath);
731
+ workItem.artifacts.closeoutAnchorSha256 = closeout.anchorSha256;
732
+ })
733
+ );
734
+
373
735
  const lineageRunId = createLineageRunId(normalizedNow);
374
736
  const statusCounts = summarizeStatusCounts(workItems);
375
737
  const linkedAgentIdentities = new Set(
@@ -1,3 +1,4 @@
1
+ import { createHash } from "node:crypto";
1
2
  import fsp from "node:fs/promises";
2
3
  import path from "node:path";
3
4
 
@@ -55,6 +56,14 @@ function normalizeMetadata(value) {
55
56
  return { ...value };
56
57
  }
57
58
 
59
+ function normalizeSeverity(value, fallbackValue = "P2") {
60
+ const normalized = normalizeString(value).toUpperCase();
61
+ if (normalized === "P0" || normalized === "P1" || normalized === "P2" || normalized === "P3") {
62
+ return normalized;
63
+ }
64
+ return fallbackValue;
65
+ }
66
+
58
67
  function normalizeAssignmentStatus(value, fallbackValue = "QUEUED") {
59
68
  const normalized = normalizeString(value).toUpperCase();
60
69
  if (ASSIGNMENT_STATUS_SET.has(normalized)) {
@@ -964,3 +973,111 @@ export async function reassignLease({
964
973
  nowIso,
965
974
  });
966
975
  }
976
+
977
+ function buildQueueFingerprint(seed = "") {
978
+ return createHash("sha256").update(normalizeString(seed) || "session-task").digest("hex");
979
+ }
980
+
981
+ export async function ensureWorkItemQueued({
982
+ targetPath = ".",
983
+ outputDir = "",
984
+ workItemId,
985
+ sessionId = "",
986
+ status = "QUEUED",
987
+ severity = "P2",
988
+ service = "session",
989
+ endpoint = "",
990
+ errorCode = "SESSION_TASK_ASSIGNMENT",
991
+ source = "session_task",
992
+ message = "",
993
+ dedupKey = "",
994
+ metadata = {},
995
+ env,
996
+ homeDir,
997
+ nowIso = new Date().toISOString(),
998
+ } = {}) {
999
+ const normalizedNow = normalizeIsoTimestamp(nowIso, new Date().toISOString());
1000
+ const normalizedWorkItemId = normalizeString(workItemId);
1001
+ if (!normalizedWorkItemId) {
1002
+ throw new Error("workItemId is required.");
1003
+ }
1004
+ const normalizedSessionId = normalizeSessionId(sessionId);
1005
+ const normalizedStatus = normalizeWorkItemStatus(status, "QUEUED");
1006
+ const normalizedService = normalizeString(service) || "session";
1007
+ const normalizedEndpoint =
1008
+ normalizeString(endpoint) ||
1009
+ (normalizedSessionId
1010
+ ? `/sessions/${normalizedSessionId}/tasks`
1011
+ : "/sessions/global/tasks");
1012
+ const normalizedErrorCode = normalizeString(errorCode) || "SESSION_TASK_ASSIGNMENT";
1013
+ const normalizedSource = normalizeString(source) || "session_task";
1014
+ const normalizedMessage = normalizeString(message) || "Session task assignment";
1015
+ const normalizedDedupKey =
1016
+ normalizeString(dedupKey) ||
1017
+ `session|${normalizedSessionId || "none"}|${normalizedWorkItemId}`;
1018
+ const normalizedMetadata = {
1019
+ ...normalizeMetadata(metadata),
1020
+ sessionId: normalizedSessionId,
1021
+ source: normalizedSource,
1022
+ workItemType: "session_task",
1023
+ };
1024
+
1025
+ const storage = await resolveAssignmentLedgerStorage({
1026
+ targetPath,
1027
+ outputDir,
1028
+ env,
1029
+ homeDir,
1030
+ });
1031
+ const queue = await loadQueue(storage.queuePath, normalizedNow);
1032
+ const existingIndex = queue.items.findIndex(
1033
+ (item) => normalizeString(item.workItemId) === normalizedWorkItemId
1034
+ );
1035
+ const existing = existingIndex >= 0 ? queue.items[existingIndex] : null;
1036
+ const fingerprintSeed = `${normalizedDedupKey}|${normalizedService}|${normalizedEndpoint}|${normalizedErrorCode}`;
1037
+ const normalizedItem = normalizeQueueItem(
1038
+ {
1039
+ ...existing,
1040
+ workItemId: normalizedWorkItemId,
1041
+ fingerprint:
1042
+ normalizeString(existing?.fingerprint) || buildQueueFingerprint(fingerprintSeed),
1043
+ source: normalizedSource,
1044
+ service: normalizedService,
1045
+ endpoint: normalizedEndpoint,
1046
+ errorCode: normalizedErrorCode,
1047
+ severity: normalizeSeverity(existing?.severity || severity, "P2"),
1048
+ status: normalizedStatus,
1049
+ message: normalizedMessage,
1050
+ stackFingerprint:
1051
+ normalizeString(existing?.stackFingerprint) ||
1052
+ buildQueueFingerprint(`${normalizedWorkItemId}|stack`).slice(0, 64),
1053
+ commitSha: existing?.commitSha || null,
1054
+ dedupKey: normalizedDedupKey,
1055
+ firstSeenAt: normalizeIsoTimestamp(existing?.firstSeenAt, normalizedNow),
1056
+ lastSeenAt: normalizedNow,
1057
+ latestEventId: normalizeString(existing?.latestEventId) || null,
1058
+ occurrenceCount: Math.max(1, Number(existing?.occurrenceCount || 1)),
1059
+ requestIds: Array.isArray(existing?.requestIds) ? [...existing.requestIds] : [],
1060
+ createdAt: normalizeIsoTimestamp(existing?.createdAt, normalizedNow),
1061
+ updatedAt: normalizedNow,
1062
+ metadata: {
1063
+ ...normalizeMetadata(existing?.metadata),
1064
+ ...normalizedMetadata,
1065
+ },
1066
+ },
1067
+ normalizedNow
1068
+ );
1069
+
1070
+ if (existingIndex >= 0) {
1071
+ queue.items[existingIndex] = normalizedItem;
1072
+ } else {
1073
+ queue.items.push(normalizedItem);
1074
+ }
1075
+
1076
+ const savedQueue = await writeQueue(storage.queuePath, queue, normalizedNow);
1077
+ const queueItem = savedQueue.items.find((item) => item.workItemId === normalizedWorkItemId) || normalizedItem;
1078
+ return {
1079
+ ...storage,
1080
+ queue: savedQueue,
1081
+ queueItem,
1082
+ };
1083
+ }