@fieldwangai/agentflow 0.1.135 → 0.1.137

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.
@@ -156,6 +156,7 @@ import {
156
156
  ensurePrdWorkflowCollaboration,
157
157
  getPrdWorkflowCollaborationById,
158
158
  getPrdWorkflowCollaborationByShareToken,
159
+ getPrdWorkflowCollaborationByTapdId,
159
160
  getPrdWorkflowCollaborationForUser,
160
161
  ensurePrdWorkflowShareLink,
161
162
  listPrdWorkflowCollaborationsForUser,
@@ -164,6 +165,7 @@ import {
164
165
  prdWorkflowCollaborationSummary,
165
166
  removePrdWorkflowCollaborationMember,
166
167
  revokePrdWorkflowShareLink,
168
+ syncPrdWorkflowAuthority,
167
169
  } from "./prd-workflow-collaboration.mjs";
168
170
  import {
169
171
  createTeam,
@@ -176,6 +178,7 @@ import {
176
178
  } from "./teams.mjs";
177
179
  import {
178
180
  legacyOverallToGlobalState,
181
+ materializeWorkflowExtensions,
179
182
  materializeWorkflowGlobalState,
180
183
  materializeWorkflowProjections,
181
184
  mergeWorkflowArtifactLists,
@@ -183,7 +186,10 @@ import {
183
186
  mergeWorkflowGlobalState,
184
187
  normalizeWorkflowReference,
185
188
  normalizeWorkflowReport,
189
+ removeWorkflowGlobalStatePath,
190
+ workflowReportResourceKeys,
186
191
  workflowRuntimeRevision,
192
+ workflowSnapshotResourceVersions,
187
193
  } from "./workflow-report.mjs";
188
194
 
189
195
  const MIME = {
@@ -1445,11 +1451,31 @@ function skillhubInstallArgs(payload, { uninstall = false } = {}) {
1445
1451
  return args;
1446
1452
  }
1447
1453
 
1448
- function readBody(req) {
1454
+ function readBody(req, maxBytes = 5 * 1024 * 1024) {
1449
1455
  return new Promise((resolve, reject) => {
1450
1456
  const chunks = [];
1451
- req.on("data", (c) => chunks.push(c));
1452
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
1457
+ let total = 0;
1458
+ let exceeded = false;
1459
+ req.on("data", (chunk) => {
1460
+ if (exceeded) return;
1461
+ const value = Buffer.from(chunk);
1462
+ total += value.length;
1463
+ if (total > maxBytes) {
1464
+ exceeded = true;
1465
+ chunks.length = 0;
1466
+ return;
1467
+ }
1468
+ chunks.push(value);
1469
+ });
1470
+ req.on("end", () => {
1471
+ if (exceeded) {
1472
+ const error = new Error(`Request body exceeds ${maxBytes} bytes`);
1473
+ error.status = 413;
1474
+ reject(error);
1475
+ return;
1476
+ }
1477
+ resolve(Buffer.concat(chunks).toString("utf8"));
1478
+ });
1453
1479
  req.on("error", reject);
1454
1480
  });
1455
1481
  }
@@ -3314,6 +3340,21 @@ function findWorkspaceShareUser(username) {
3314
3340
  return null;
3315
3341
  }
3316
3342
 
3343
+ function workflowAuthorityIdentity(value) {
3344
+ if (typeof value === "string" || typeof value === "number") return String(value || "").trim();
3345
+ if (!value || typeof value !== "object" || Array.isArray(value)) return "";
3346
+ return String(value.username || value.userId || value.user_id || value.nick || value.name || "").trim();
3347
+ }
3348
+
3349
+ function workflowAuthorityIdentities(value) {
3350
+ const values = Array.isArray(value) ? value : value == null ? [] : [value];
3351
+ return [...new Set(values.flatMap((item) => {
3352
+ if (typeof item === "string") return item.split(/[;,,;]/).map((entry) => entry.trim()).filter(Boolean);
3353
+ const identity = workflowAuthorityIdentity(item);
3354
+ return identity ? [identity] : [];
3355
+ }))];
3356
+ }
3357
+
3317
3358
  function workspaceCollaborationSummaryWithUsers(record, userId) {
3318
3359
  const summary = workspaceCollaborationSummary(record, userId);
3319
3360
  if (!summary) return null;
@@ -7779,6 +7820,7 @@ const workspaceCollaborationSequences = new Map();
7779
7820
  const prdWorkflowSubscribers = new Map();
7780
7821
  const prdWorkflowIdempotency = new Map();
7781
7822
  const prdWorkflowActionLocks = new Map();
7823
+ const prdWorkflowWriteQueues = new Map();
7782
7824
  const PRD_WORKFLOW_IDEMPOTENCY_MAX = 1000;
7783
7825
  const PRD_WORKFLOW_RUNTIME_EVENTS_MAX = 1000;
7784
7826
  const WORKSPACE_SCHEDULES_FILENAME = "workspace-schedules.json";
@@ -7787,6 +7829,22 @@ const WORKSPACE_IMPLEMENTATION_REFERENCE_ENABLED = true;
7787
7829
  const WORKSPACE_IMPLEMENTATION_SUMMARY_ENABLED = false;
7788
7830
  const WORKSPACE_NODE_HISTORY_MAX_CHARS = 80000;
7789
7831
 
7832
+ async function prdWorkflowAcquireWriteLock(key) {
7833
+ const lockKey = String(key || "").trim();
7834
+ const previous = prdWorkflowWriteQueues.get(lockKey) || Promise.resolve();
7835
+ let releaseCurrent;
7836
+ const current = new Promise((resolve) => { releaseCurrent = resolve; });
7837
+ prdWorkflowWriteQueues.set(lockKey, current);
7838
+ await previous.catch(() => {});
7839
+ let released = false;
7840
+ return () => {
7841
+ if (released) return;
7842
+ released = true;
7843
+ releaseCurrent();
7844
+ if (prdWorkflowWriteQueues.get(lockKey) === current) prdWorkflowWriteQueues.delete(lockKey);
7845
+ };
7846
+ }
7847
+
7790
7848
  function resolvePrdWorkflowScope(workspaceRoot, params = {}, userCtx = {}, capability = "read") {
7791
7849
  const tapdId = String(params.tapdId || params.tapd_id || "").trim();
7792
7850
  const flowId = String(params.flowId || "").trim();
@@ -7811,6 +7869,10 @@ function resolvePrdWorkflowScope(workspaceRoot, params = {}, userCtx = {}, capab
7811
7869
  const memberCollaboration = tapdId
7812
7870
  ? getPrdWorkflowCollaborationForUser(tapdId, userCtx?.userId)
7813
7871
  : null;
7872
+ const existingCollaboration = tapdId ? getPrdWorkflowCollaborationByTapdId(tapdId) : null;
7873
+ if (!adminOwner && !linkCollaboration && existingCollaboration && !memberCollaboration) {
7874
+ return { error: "PRD Workflow collaboration permission denied", status: 403 };
7875
+ }
7814
7876
  const collaboration = adminOwner ? null : (linkCollaboration || memberCollaboration);
7815
7877
  const access = adminOwner
7816
7878
  ? { allowed: true, writable: false, role: "admin-viewer", via: "admin-review" }
@@ -7824,7 +7886,8 @@ function resolvePrdWorkflowScope(workspaceRoot, params = {}, userCtx = {}, capab
7824
7886
  return { error: "PRD Workflow collaboration edit permission denied", status: 403 };
7825
7887
  }
7826
7888
  const ownerId = String(adminOwner?.userId || collaboration?.ownerId || userCtx?.userId || "").trim();
7827
- const stateRoot = path.resolve(getAgentflowUserDataRoot(ownerId));
7889
+ const stateOwnerId = String(adminOwner?.userId || collaboration?.stateOwnerId || collaboration?.ownerId || userCtx?.userId || "").trim();
7890
+ const stateRoot = path.resolve(getAgentflowUserDataRoot(stateOwnerId));
7828
7891
  let executionRoot = path.resolve(workspaceRoot);
7829
7892
  if (flowId) {
7830
7893
  const projectScope = resolveWorkspaceScopeRoot(workspaceRoot, {
@@ -7846,6 +7909,7 @@ function resolvePrdWorkflowScope(workspaceRoot, params = {}, userCtx = {}, capab
7846
7909
  executionRoot,
7847
7910
  stateRoot,
7848
7911
  ownerId,
7912
+ stateOwnerId,
7849
7913
  collaboration,
7850
7914
  collaborationAccess: access,
7851
7915
  shareToken,
@@ -7862,7 +7926,7 @@ function prdWorkflowKey(userCtx = {}, flowSource = "user", flowId = "", tapdId =
7862
7926
  const collaboration = getPrdWorkflowCollaborationByShareToken(shareToken)
7863
7927
  || getPrdWorkflowCollaborationForUser(id, userCtx?.userId);
7864
7928
  const adminOwnerId = userCtx?.isAdmin === true ? String(userCtx?.adminOwnerId || "").trim() : "";
7865
- const actorScope = `user:${String(collaboration?.ownerId || adminOwnerId || userCtx?.userId || "")}`;
7929
+ const actorScope = `user:${String(collaboration?.stateOwnerId || collaboration?.ownerId || adminOwnerId || userCtx?.userId || "")}`;
7866
7930
  return [actorScope, id].join("\t");
7867
7931
  }
7868
7932
 
@@ -7997,7 +8061,17 @@ function prdWorkflowSafeStateId(value) {
7997
8061
  }
7998
8062
 
7999
8063
  function prdWorkflowReviewIdFromRequest(tapdId, payload = {}, durability = "temporary") {
8000
- if (durability === "temporary") return `r-${crypto.randomBytes(5).toString("hex")}`;
8064
+ if (durability === "temporary") {
8065
+ const idempotencyKey = String(payload.idempotencyKey || payload.idempotency_key || "").trim();
8066
+ if (!idempotencyKey) return `r-${crypto.randomBytes(5).toString("hex")}`;
8067
+ const source = String(payload.source || "agentflow-cli").trim().toLowerCase() || "agentflow-cli";
8068
+ const digest = crypto
8069
+ .createHash("sha256")
8070
+ .update(JSON.stringify({ tapdId: String(tapdId || ""), source, idempotencyKey }))
8071
+ .digest("hex")
8072
+ .slice(0, 12);
8073
+ return `r-${digest}`;
8074
+ }
8001
8075
  const requested = String(payload.reviewId || payload.review_id || "").trim();
8002
8076
  if (!requested) return `review_${Date.now().toString(36)}_${crypto.randomBytes(4).toString("hex")}`;
8003
8077
  const safeRequested = prdWorkflowSafeStateId(requested);
@@ -8064,6 +8138,11 @@ function prdWorkflowEventsPath(scopedRoot, tapdId) {
8064
8138
  return path.join(rootDir, ".workspace", "prd-flow", "workflow-state", `${prdWorkflowSafeStateId(tapdId)}.events.json`);
8065
8139
  }
8066
8140
 
8141
+ function prdWorkflowEventsArchivePath(scopedRoot, tapdId) {
8142
+ const rootDir = scopedRoot || process.cwd();
8143
+ return path.join(rootDir, ".workspace", "prd-flow", "workflow-state", `${prdWorkflowSafeStateId(tapdId)}.events.archive.jsonl`);
8144
+ }
8145
+
8067
8146
  function prdWorkflowAuditPath(scopedRoot, tapdId) {
8068
8147
  const rootDir = scopedRoot || process.cwd();
8069
8148
  return path.join(rootDir, ".workspace", "prd-flow", "workflow-state", `${prdWorkflowSafeStateId(tapdId)}.audit.jsonl`);
@@ -9701,10 +9780,20 @@ export function prdWorkflowReviewHtml(title, markdown, meta = {}) {
9701
9780
  }
9702
9781
 
9703
9782
  function prdWorkflowCreateReview(scopedRoot, tapdId, payload = {}, urlBase = "", ownerId = "") {
9704
- const content = String(payload.markdown || payload.content || payload.rawOutput || "").slice(0, 500000);
9783
+ const content = String(payload.markdown || payload.content || payload.rawOutput || "");
9705
9784
  if (!content.trim()) throw new Error("Missing review markdown");
9785
+ if (Buffer.byteLength(content, "utf-8") > 500000) {
9786
+ const error = new Error("Review markdown exceeds 500000 bytes");
9787
+ error.status = 413;
9788
+ throw error;
9789
+ }
9706
9790
  const title = String(payload.title || payload.label || "PRD Workflow Review").trim().slice(0, 160) || "PRD Workflow Review";
9707
9791
  const durability = String(payload.durability || (payload.durable === true || payload.permanent === true ? "durable" : "temporary")).trim().toLowerCase() || "temporary";
9792
+ if (!["temporary", "durable"].includes(durability)) {
9793
+ const error = new Error("durability must be temporary or durable");
9794
+ error.status = 400;
9795
+ throw error;
9796
+ }
9708
9797
  const reviewId = prdWorkflowReviewIdFromRequest(tapdId, payload, durability);
9709
9798
  const paths = prdWorkflowReviewPaths(scopedRoot, tapdId, reviewId);
9710
9799
  const ttlDaysRaw = Number(payload.ttlDays || payload.ttl_days || (durability === "temporary" ? 7 : 0));
@@ -9924,11 +10013,13 @@ function prdWorkflowReadClientStateWithFallback(root, scopedRoot, tapdId) {
9924
10013
 
9925
10014
  function prdWorkflowWriteClientObservation(scopedRoot, tapdId, meta, snapshot) {
9926
10015
  const state = prdWorkflowReadClientState(scopedRoot, tapdId);
9927
- const clientId = prdWorkflowSafeStateId(meta.clientId || "anonymous");
10016
+ const reportSource = String(meta.reportSource || meta.source || "legacy").trim().toLowerCase() || "legacy";
10017
+ const clientId = prdWorkflowSafeStateId(`${reportSource}:${meta.clientId || "anonymous"}`);
9928
10018
  const nextClients = {
9929
10019
  ...state.clients,
9930
10020
  [clientId]: {
9931
10021
  clientId: String(meta.clientId || clientId),
10022
+ source: reportSource,
9932
10023
  userId: String(meta.userId || ""),
9933
10024
  observedAt: String(meta.observedAt || ""),
9934
10025
  reportedAt: String(meta.reportedAt || new Date().toISOString()),
@@ -10161,6 +10252,7 @@ function prdWorkflowSnapshotMetaFromReport(payload = {}, rawSnapshot = {}, req =
10161
10252
  reportedAt,
10162
10253
  observedAt: String(payload.observedAt || payload.observed_at || rawSnapshot.observedAt || rawSnapshot.observed_at || sources.observedAt || sources.checkedAt || headerObservedAt || reportedAt),
10163
10254
  clientId: String(payload.clientId || payload.client_id || sources.clientId || headerClientId || userCtx?.userId || "anonymous").slice(0, 160),
10255
+ reportSource: String(payload.reportSource || payload.report_source || payload.source || "legacy").trim().toLowerCase().slice(0, 120) || "legacy",
10164
10256
  userId: String(userCtx?.userId || payload.userId || payload.user_id || "").slice(0, 160),
10165
10257
  baseRevision: String(payload.baseRevision || payload.base_revision || payload.expectedRevision || payload.expected_revision || rawSnapshot.baseRevision || sources.baseRevision || "").trim(),
10166
10258
  scope: String(payload.scope || rawSnapshot.scope || rawSnapshot.next?.scope || sources.scope || "client").trim().toLowerCase() || "client",
@@ -10171,6 +10263,89 @@ function prdWorkflowSnapshotMetaFromReport(payload = {}, rawSnapshot = {}, req =
10171
10263
  };
10172
10264
  }
10173
10265
 
10266
+ function prdWorkflowStoreClientObservation({
10267
+ scopedRoot,
10268
+ tapdId,
10269
+ rawState,
10270
+ payload = {},
10271
+ req = null,
10272
+ userCtx = {},
10273
+ flowSource = "user",
10274
+ flowId = "",
10275
+ }) {
10276
+ const normalizedSnapshot = {
10277
+ ...prdWorkflowSnapshotFromParsed(scopedRoot, tapdId, rawState, userCtx, { flowSource, flowId }),
10278
+ clientReportedAt: new Date().toISOString(),
10279
+ sources: {
10280
+ ...(rawState.sources && typeof rawState.sources === "object" ? rawState.sources : {}),
10281
+ executionMode: "workflow-report",
10282
+ },
10283
+ };
10284
+ const reportMeta = prdWorkflowSnapshotMetaFromReport(payload, rawState, req, userCtx);
10285
+ const reportSource = {
10286
+ ...(normalizedSnapshot.sources && typeof normalizedSnapshot.sources === "object" ? normalizedSnapshot.sources : {}),
10287
+ executionMode: "workflow-report",
10288
+ truth: "observation",
10289
+ authority: "client",
10290
+ persistence: "runtime",
10291
+ clientId: reportMeta.clientId,
10292
+ clientUserId: reportMeta.userId,
10293
+ clientReportedAt: reportMeta.reportedAt,
10294
+ clientObservedAt: reportMeta.observedAt,
10295
+ baseRevision: reportMeta.baseRevision,
10296
+ scope: reportMeta.scope,
10297
+ platform: reportMeta.platform,
10298
+ issueKey: reportMeta.issueKey,
10299
+ stageKey: reportMeta.stageKey,
10300
+ };
10301
+ const existingClientState = prdWorkflowReadClientState(scopedRoot, tapdId);
10302
+ const existingClientId = prdWorkflowSafeStateId(`${reportMeta.reportSource || "legacy"}:${reportMeta.clientId || "anonymous"}`);
10303
+ const previousClientSnapshot = existingClientState.clients?.[existingClientId]?.snapshot || null;
10304
+ const stampedSnapshot = prdWorkflowStampCurrentActionEntryTimes(
10305
+ scopedRoot,
10306
+ tapdId,
10307
+ normalizedSnapshot,
10308
+ existingClientState,
10309
+ reportMeta,
10310
+ );
10311
+ const storedObservationSnapshot = prdWorkflowStoredObservationSnapshot(stampedSnapshot, reportSource);
10312
+ const actionChanges = prdWorkflowSnapshotActionChanges(previousClientSnapshot || {}, storedObservationSnapshot);
10313
+ prdWorkflowWriteClientObservation(scopedRoot, tapdId, reportMeta, storedObservationSnapshot);
10314
+ prdWorkflowAppendAudit(scopedRoot, tapdId, {
10315
+ type: "workflow-report-observation-stored",
10316
+ flowSource,
10317
+ flowId,
10318
+ clientId: reportMeta.clientId,
10319
+ userId: reportMeta.userId,
10320
+ observedAt: reportMeta.observedAt,
10321
+ reportedAt: reportMeta.reportedAt,
10322
+ phase: String(storedObservationSnapshot?.phase || ""),
10323
+ pointer: String(storedObservationSnapshot?.pointer || ""),
10324
+ revision: String(storedObservationSnapshot?.revision || ""),
10325
+ actionCount: prdWorkflowSnapshotActionCount(storedObservationSnapshot),
10326
+ truth: "observation",
10327
+ authority: "client",
10328
+ persistence: "runtime",
10329
+ note: "producer observation accepted through the canonical Workflow Report endpoint",
10330
+ });
10331
+ for (const change of actionChanges) {
10332
+ prdWorkflowAppendAudit(scopedRoot, tapdId, {
10333
+ type: "snapshot-action-change",
10334
+ source: "workflow-report",
10335
+ clientId: reportMeta.clientId,
10336
+ userId: reportMeta.userId,
10337
+ observedAt: reportMeta.observedAt,
10338
+ reportedAt: reportMeta.reportedAt,
10339
+ revision: String(storedObservationSnapshot.revision || ""),
10340
+ previousRevision: String(previousClientSnapshot?.revision || ""),
10341
+ pointer: String(storedObservationSnapshot.pointer || ""),
10342
+ previousPointer: String(previousClientSnapshot?.pointer || ""),
10343
+ ...change,
10344
+ });
10345
+ }
10346
+ return { reportMeta, storedObservationSnapshot, previousClientSnapshot };
10347
+ }
10348
+
10174
10349
  function prdWorkflowSnapshotReportConflict(existingRecord, incomingSnapshot, meta) {
10175
10350
  if (!existingRecord?.snapshot || meta.force) return null;
10176
10351
  const current = existingRecord.snapshot;
@@ -10285,6 +10460,7 @@ function prdWorkflowMaterializeSnapshot(root, scopedRoot, tapdId, userCtx = {},
10285
10460
  );
10286
10461
  const clientObservations = prdWorkflowClientObservationRows(root, scopedRoot, tapdId).map((item) => ({
10287
10462
  clientId: item.clientId,
10463
+ source: item.source || "legacy",
10288
10464
  userId: item.userId || "",
10289
10465
  phase: item.phase || "",
10290
10466
  pointer: item.pointer || "",
@@ -10477,13 +10653,22 @@ function prdWorkflowNormalizeStoredRuntimeEvent(tapdId, event = {}) {
10477
10653
  function prdWorkflowReadRuntimeEvents(scopedRoot, tapdId) {
10478
10654
  try {
10479
10655
  const p = prdWorkflowEventsPath(scopedRoot, tapdId);
10480
- if (!fs.existsSync(p)) return { version: 1, tapdId: String(tapdId || ""), events: [] };
10481
- const data = JSON.parse(fs.readFileSync(p, "utf-8"));
10482
- const events = Array.isArray(data?.events)
10483
- ? data.events
10484
- .map((item) => prdWorkflowNormalizeStoredRuntimeEvent(data?.tapdId || tapdId, item))
10485
- .filter((item) => item && typeof item === "object")
10656
+ const archivePath = prdWorkflowEventsArchivePath(scopedRoot, tapdId);
10657
+ const data = fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, "utf-8")) : {};
10658
+ const archivedEvents = fs.existsSync(archivePath)
10659
+ ? fs.readFileSync(archivePath, "utf-8").split("\n").filter(Boolean).flatMap((line) => {
10660
+ try { return [JSON.parse(line)]; } catch { return []; }
10661
+ })
10486
10662
  : [];
10663
+ const normalizedEvents = [...archivedEvents, ...(Array.isArray(data?.events) ? data.events : [])]
10664
+ .map((item) => prdWorkflowNormalizeStoredRuntimeEvent(data?.tapdId || tapdId, item))
10665
+ .filter((item) => item && typeof item === "object");
10666
+ const eventsByKey = new Map();
10667
+ for (const event of normalizedEvents) {
10668
+ const key = `${prdWorkflowRuntimeEventProducer(event)}:${prdWorkflowRuntimeEventOperation(event)}:${String(event.id || "")}`;
10669
+ eventsByKey.set(key, event);
10670
+ }
10671
+ const events = [...eventsByKey.values()];
10487
10672
  return {
10488
10673
  version: 1,
10489
10674
  tapdId: String(data?.tapdId || tapdId || ""),
@@ -10498,11 +10683,17 @@ function prdWorkflowReadRuntimeEvents(scopedRoot, tapdId) {
10498
10683
  function prdWorkflowWriteRuntimeEvents(scopedRoot, tapdId, events) {
10499
10684
  const p = prdWorkflowEventsPath(scopedRoot, tapdId);
10500
10685
  fs.mkdirSync(path.dirname(p), { recursive: true });
10686
+ const allEvents = Array.isArray(events) ? events : [];
10687
+ const overflow = allEvents.slice(0, Math.max(0, allEvents.length - PRD_WORKFLOW_RUNTIME_EVENTS_MAX));
10688
+ const archivePath = prdWorkflowEventsArchivePath(scopedRoot, tapdId);
10689
+ const archiveTmp = `${archivePath}.${process.pid}.${Date.now()}.tmp`;
10690
+ fs.writeFileSync(archiveTmp, overflow.length ? `${overflow.map((event) => JSON.stringify(event)).join("\n")}\n` : "", "utf-8");
10691
+ fs.renameSync(archiveTmp, archivePath);
10501
10692
  const data = {
10502
10693
  version: 1,
10503
10694
  tapdId: String(tapdId || ""),
10504
10695
  updatedAt: new Date().toISOString(),
10505
- events: Array.isArray(events) ? events.slice(-PRD_WORKFLOW_RUNTIME_EVENTS_MAX) : [],
10696
+ events: allEvents.slice(-PRD_WORKFLOW_RUNTIME_EVENTS_MAX),
10506
10697
  };
10507
10698
  const tmp = `${p}.${process.pid}.${Date.now()}.tmp`;
10508
10699
  fs.writeFileSync(tmp, JSON.stringify(data, null, 2) + "\n", "utf-8");
@@ -10565,6 +10756,19 @@ function prdWorkflowRuntimeEventCanonicalAction(stage = "") {
10565
10756
  : "";
10566
10757
  }
10567
10758
 
10759
+ function prdWorkflowRuntimeEventProducer(event = {}) {
10760
+ return String(event.source || event.producer || "agentflow")
10761
+ .trim()
10762
+ .toLowerCase()
10763
+ .slice(0, 120) || "agentflow";
10764
+ }
10765
+
10766
+ function prdWorkflowRuntimeEventOperation(event = {}) {
10767
+ return String(event.operation || (event.type === "review-link" ? "artifact.publish" : "report"))
10768
+ .trim()
10769
+ .toLowerCase() || "report";
10770
+ }
10771
+
10568
10772
  function prdWorkflowRuntimeOwnedArtifacts(values, stage = "") {
10569
10773
  if (!Array.isArray(values)) return values;
10570
10774
  const ownsOnlyChangedArtifact = /^(?:issue-plan|implementation|bugfix|integration):/.test(String(stage || ""));
@@ -10586,16 +10790,21 @@ function prdWorkflowRuntimeEventId(event = {}) {
10586
10790
  const scope = String(event.scope || "").trim();
10587
10791
  const platform = String(event.platform || "").trim();
10588
10792
  const aggregateByStage = event.aggregateByStage !== false && event.aggregate_by_stage !== false;
10793
+ const operation = prdWorkflowRuntimeEventOperation(event);
10794
+ const stableActionKey = String(event?.actionModel?.key || "").trim();
10795
+ if (stableActionKey && aggregateByStage && String(event?.type || "") === "workflow-report") {
10796
+ return `stage_${prdWorkflowSafeStateId([prdWorkflowRuntimeEventProducer(event), operation, stableActionKey].join(":"))}`;
10797
+ }
10589
10798
  if ((stage || action) && aggregateByStage) {
10590
10799
  const issue = String(event.issueKey || event.issue_key || event.issue || "").trim();
10591
- const key = [scope, issue, platform, stage || action].filter(Boolean).join(":");
10800
+ const key = [prdWorkflowRuntimeEventProducer(event), operation, scope, issue, platform, stage || action].filter(Boolean).join(":");
10592
10801
  return `stage_${prdWorkflowSafeStateId(key)}`;
10593
10802
  }
10594
10803
  const existing = String(event.id || event.eventId || event.event_id || "").trim();
10595
10804
  if (existing) return existing.slice(0, 160);
10596
10805
  if (stage || action) {
10597
10806
  const issue = String(event.issueKey || event.issue_key || event.issue || "").trim();
10598
- const key = [scope, issue, platform, stage || action].filter(Boolean).join(":");
10807
+ const key = [prdWorkflowRuntimeEventProducer(event), operation, scope, issue, platform, stage || action].filter(Boolean).join(":");
10599
10808
  return `stage_${prdWorkflowSafeStateId(key)}`;
10600
10809
  }
10601
10810
  return `evt_${Date.now().toString(36)}_${crypto.randomBytes(4).toString("hex")}`;
@@ -10659,8 +10868,13 @@ function prdWorkflowNormalizeRuntimeEvent(tapdId, event = {}) {
10659
10868
  entry.stage = stage;
10660
10869
  entry.stageKey = stage;
10661
10870
  }
10662
- entry.artifacts = prdWorkflowRuntimeOwnedArtifacts(entry.artifacts, stage);
10663
- entry.links = prdWorkflowRuntimeOwnedArtifacts(entry.links, stage);
10871
+ const attachProducer = (values) => (Array.isArray(values) ? values.map((item) => (
10872
+ item && typeof item === "object" && !Array.isArray(item)
10873
+ ? { ...item, producer: String(item.producer || source).trim().toLowerCase() || source }
10874
+ : item
10875
+ )) : values);
10876
+ entry.artifacts = attachProducer(prdWorkflowRuntimeOwnedArtifacts(entry.artifacts, stage));
10877
+ entry.links = attachProducer(prdWorkflowRuntimeOwnedArtifacts(entry.links, stage));
10664
10878
  if (scope) entry.scope = scope;
10665
10879
  if (platform) entry.platform = platform;
10666
10880
  if (!entry.createdAt) entry.createdAt = event.startedAt || now;
@@ -10759,8 +10973,14 @@ function prdWorkflowAppendRuntimeEvent(scopedRoot, tapdId, event = {}) {
10759
10973
  const current = prdWorkflowReadRuntimeEvents(scopedRoot, tapdId);
10760
10974
  const entry = prdWorkflowNormalizeRuntimeEvent(tapdId, event);
10761
10975
  const entryIdem = String(entry.idempotencyKey || "").trim();
10976
+ const entryProducer = prdWorkflowRuntimeEventProducer(entry);
10977
+ const entryOperation = prdWorkflowRuntimeEventOperation(entry);
10978
+ const entryDedupeKey = prdWorkflowRuntimeEventDedupeKey(entry);
10762
10979
  const index = current.events.findIndex((item) => {
10980
+ if (prdWorkflowRuntimeEventProducer(item) !== entryProducer) return false;
10763
10981
  if (String(item?.id || "") === entry.id) return true;
10982
+ if (prdWorkflowRuntimeEventOperation(item) !== entryOperation) return false;
10983
+ if (prdWorkflowRuntimeEventDedupeKey(item) === entryDedupeKey) return true;
10764
10984
  if (!entryIdem) return false;
10765
10985
  if (String(item?.idempotencyKey || "").trim() === entryIdem) return true;
10766
10986
  return Array.isArray(item?.idempotencyHistory) && item.idempotencyHistory.includes(entryIdem);
@@ -10795,17 +11015,43 @@ function prdWorkflowAppendRuntimeEvent(scopedRoot, tapdId, event = {}) {
10795
11015
  incomingGlobalStatePatch,
10796
11016
  )
10797
11017
  : previousGlobalStatePatch;
11018
+ const incomingPatchPaths = Array.isArray(entry?.globalStateOwnerPaths) ? entry.globalStateOwnerPaths : [];
11019
+ const previousRemovePaths = Array.isArray(events[index]?.globalStateRemove || events[index]?.global_state_remove)
11020
+ ? (events[index].globalStateRemove || events[index].global_state_remove)
11021
+ : [];
11022
+ const incomingRemovePaths = Array.isArray(entry?.globalStateRemove || entry?.global_state_remove)
11023
+ ? (entry.globalStateRemove || entry.global_state_remove)
11024
+ : [];
11025
+ const overlapsPath = (left, right) => left === right || left.startsWith(`${right}.`) || right.startsWith(`${left}.`);
11026
+ const mergedGlobalStateRemove = [...new Set([
11027
+ ...previousRemovePaths.filter((removedPath) => !incomingPatchPaths.some((patchPath) => overlapsPath(String(removedPath), String(patchPath)))),
11028
+ ...incomingRemovePaths,
11029
+ ])];
11030
+ let normalizedGlobalStatePatch = mergedGlobalStatePatch;
11031
+ for (const removedPath of incomingRemovePaths) {
11032
+ normalizedGlobalStatePatch = removeWorkflowGlobalStatePath(normalizedGlobalStatePatch, removedPath);
11033
+ }
10798
11034
  const prevArtifact = prdWorkflowRuntimeEventArtifactSignature(events[index]);
10799
11035
  const nextArtifact = prdWorkflowRuntimeEventArtifactSignature(entry);
10800
11036
  artifactConflict = Boolean(prevArtifact && nextArtifact && prevArtifact !== nextArtifact &&
10801
11037
  prdWorkflowRuntimeEventShouldConflictOnArtifact(events[index]) &&
10802
11038
  prdWorkflowRuntimeEventShouldConflictOnArtifact(entry));
10803
- const idempotencyHistory = [
10804
- ...(events[index].idempotencyKey ? [events[index].idempotencyKey] : []),
10805
- ...(entry.idempotencyKey ? [entry.idempotencyKey] : []),
11039
+ const idempotencyHistory = [...new Set([
10806
11040
  ...(Array.isArray(events[index].idempotencyHistory) ? events[index].idempotencyHistory : []),
11041
+ ...(events[index].idempotencyKey ? [events[index].idempotencyKey] : []),
10807
11042
  ...(Array.isArray(entry.idempotencyHistory) ? entry.idempotencyHistory : []),
10808
- ];
11043
+ ...(entry.idempotencyKey ? [entry.idempotencyKey] : []),
11044
+ ])].slice(-50);
11045
+ const allIdempotencyFingerprints = {
11046
+ ...(events[index].idempotencyFingerprints || {}),
11047
+ ...(events[index].idempotencyKey && events[index].idempotencyFingerprint
11048
+ ? { [events[index].idempotencyKey]: events[index].idempotencyFingerprint }
11049
+ : {}),
11050
+ ...(entry.idempotencyFingerprints || {}),
11051
+ ...(entry.idempotencyKey && entry.idempotencyFingerprint
11052
+ ? { [entry.idempotencyKey]: entry.idempotencyFingerprint }
11053
+ : {}),
11054
+ };
10809
11055
  events[index] = {
10810
11056
  ...events[index],
10811
11057
  ...entry,
@@ -10814,19 +11060,28 @@ function prdWorkflowAppendRuntimeEvent(scopedRoot, tapdId, event = {}) {
10814
11060
  outputs: prdWorkflowMergeRuntimeEventArrays(events[index].outputs, entry.outputs),
10815
11061
  results: prdWorkflowMergeRuntimeEventArrays(events[index].results, entry.results),
10816
11062
  actionModel: mergeWorkflowGlobalState(events[index].actionModel, entry.actionModel),
10817
- globalStateRemove: prdWorkflowMergeRuntimeEventArrays(
10818
- events[index].globalStateRemove || events[index].global_state_remove,
10819
- entry.globalStateRemove || entry.global_state_remove,
11063
+ globalStateRemove: mergedGlobalStateRemove,
11064
+ extensionsPatch: entry.extensionsPatch
11065
+ ? mergeWorkflowGlobalState(events[index].extensionsPatch, entry.extensionsPatch)
11066
+ : events[index].extensionsPatch,
11067
+ globalStateOwnerPaths: prdWorkflowMergeRuntimeEventArrays(
11068
+ events[index].globalStateOwnerPaths,
11069
+ entry.globalStateOwnerPaths,
10820
11070
  ),
10821
11071
  createdAt: events[index].createdAt || entry.createdAt,
10822
11072
  startedAt: events[index].startedAt || entry.startedAt,
10823
- idempotencyHistory: [...new Set(idempotencyHistory)].slice(-50),
11073
+ idempotencyHistory,
11074
+ idempotencyFingerprints: Object.fromEntries(
11075
+ idempotencyHistory
11076
+ .filter((key) => allIdempotencyFingerprints[key])
11077
+ .map((key) => [key, allIdempotencyFingerprints[key]]),
11078
+ ),
10824
11079
  };
10825
11080
  if (mergedImplementationMetadata && typeof mergedImplementationMetadata === "object" && !Array.isArray(mergedImplementationMetadata)) {
10826
11081
  events[index].implementationMetadata = mergedImplementationMetadata;
10827
11082
  }
10828
- if (mergedGlobalStatePatch && typeof mergedGlobalStatePatch === "object" && !Array.isArray(mergedGlobalStatePatch)) {
10829
- events[index].globalStatePatch = mergedGlobalStatePatch;
11083
+ if (normalizedGlobalStatePatch && typeof normalizedGlobalStatePatch === "object" && !Array.isArray(normalizedGlobalStatePatch)) {
11084
+ events[index].globalStatePatch = normalizedGlobalStatePatch;
10830
11085
  }
10831
11086
  if (artifactConflict) {
10832
11087
  events[index] = {
@@ -10871,36 +11126,128 @@ function prdWorkflowAppendRuntimeEvent(scopedRoot, tapdId, event = {}) {
10871
11126
  }
10872
11127
  }
10873
11128
 
10874
- function prdWorkflowFindCompletedIdempotencyEvent(scopedRoot, tapdId, idempotencyKey) {
11129
+ function prdWorkflowFindIdempotencyEvent(scopedRoot, tapdId, idempotencyKey, source = "", completedOnly = true, operation = "") {
10875
11130
  const key = String(idempotencyKey || "").trim();
10876
11131
  if (!key) return null;
11132
+ const producer = String(source || "").trim().toLowerCase();
11133
+ const operationKey = String(operation || "").trim().toLowerCase();
10877
11134
  const events = prdWorkflowReadRuntimeEvents(scopedRoot, tapdId).events;
10878
11135
  return [...events].reverse().find((event) => (
11136
+ (!producer || prdWorkflowRuntimeEventProducer(event) === producer) &&
11137
+ (!operationKey || String(event?.operation || (event?.type === "review-link" ? "artifact.publish" : "report")).toLowerCase() === operationKey) &&
10879
11138
  (String(event?.idempotencyKey || "") === key || (Array.isArray(event?.idempotencyHistory) && event.idempotencyHistory.includes(key))) &&
10880
- ["done", "success", "completed"].includes(String(event?.status || "").toLowerCase())
11139
+ (!completedOnly || ["done", "success", "completed"].includes(String(event?.status || "").toLowerCase()))
10881
11140
  )) || null;
10882
11141
  }
10883
11142
 
11143
+ function prdWorkflowFindCompletedIdempotencyEvent(scopedRoot, tapdId, idempotencyKey, source = "") {
11144
+ return prdWorkflowFindIdempotencyEvent(scopedRoot, tapdId, idempotencyKey, source, false, "report");
11145
+ }
11146
+
11147
+ function prdWorkflowIdempotencyFingerprint(event = {}, idempotencyKey = "") {
11148
+ const key = String(idempotencyKey || "").trim();
11149
+ return String(
11150
+ event?.idempotencyFingerprints?.[key]
11151
+ || (String(event?.idempotencyKey || "").trim() === key ? event?.idempotencyFingerprint : "")
11152
+ || "",
11153
+ );
11154
+ }
11155
+
11156
+ function prdWorkflowResourceVersionConflicts(expectedVersions = {}, currentVersions = {}) {
11157
+ const conflicts = [];
11158
+ for (const [resourceKey, expectedVersion] of Object.entries(expectedVersions || {})) {
11159
+ const currentVersion = String(currentVersions?.[resourceKey] || "absent");
11160
+ const expected = String(expectedVersion || "absent");
11161
+ if (expected === currentVersion) continue;
11162
+ conflicts.push({ resourceKey, expectedVersion: expected, currentVersion });
11163
+ }
11164
+ return conflicts;
11165
+ }
11166
+
11167
+ function prdWorkflowGlobalPathOwners(snapshot = {}) {
11168
+ const owners = new Map();
11169
+ for (const event of Array.isArray(snapshot.runtimeEvents) ? snapshot.runtimeEvents : []) {
11170
+ const source = prdWorkflowRuntimeEventProducer(event);
11171
+ for (const path of Array.isArray(event?.globalStateOwnerPaths) ? event.globalStateOwnerPaths : []) {
11172
+ const normalized = String(path || "").trim();
11173
+ if (normalized) owners.set(normalized, source);
11174
+ }
11175
+ }
11176
+ return owners;
11177
+ }
11178
+
11179
+ function prdWorkflowGlobalOwnershipConflicts(report, currentSnapshot = {}) {
11180
+ const source = prdWorkflowRuntimeEventProducer(report?.event || {});
11181
+ const owners = prdWorkflowGlobalPathOwners(currentSnapshot);
11182
+ const conflicts = [];
11183
+ for (const path of Array.isArray(report?.event?.globalStateOwnerPaths) ? report.event.globalStateOwnerPaths : []) {
11184
+ const normalizedPath = String(path || "");
11185
+ const match = [...owners.entries()].find(([ownedPath, owner]) => (
11186
+ owner !== source && (
11187
+ ownedPath === normalizedPath ||
11188
+ ownedPath.startsWith(`${normalizedPath}.`) ||
11189
+ normalizedPath.startsWith(`${ownedPath}.`)
11190
+ )
11191
+ ));
11192
+ if (match) conflicts.push({ path: normalizedPath, owner: match[1], source });
11193
+ }
11194
+ return conflicts;
11195
+ }
11196
+
11197
+ function prdWorkflowMergeProducerTimeline(report, currentSnapshot = {}) {
11198
+ if (!report?.projections || !Array.isArray(report.projections.timeline)) return report;
11199
+ const source = prdWorkflowRuntimeEventProducer(report.event);
11200
+ const current = Array.isArray(currentSnapshot?.projections?.timeline) ? currentSnapshot.projections.timeline : [];
11201
+ const incoming = report.projections.timeline;
11202
+ const foreignCurrent = current.filter((item) => prdWorkflowRuntimeEventProducer(item) !== source);
11203
+ const ownIncoming = incoming.filter((item) => prdWorkflowRuntimeEventProducer(item) === source);
11204
+ const foreignIncoming = incoming.filter((item) => prdWorkflowRuntimeEventProducer(item) !== source);
11205
+ const foreignByKey = new Map(foreignCurrent.map((item) => [String(item?.key || `${item?.source}:${item?.kind}:${item?.id}`), item]));
11206
+ for (const item of foreignIncoming) {
11207
+ const key = String(item?.key || `${item?.source}:${item?.kind}:${item?.id}`);
11208
+ const existing = foreignByKey.get(key);
11209
+ const existingVersion = existing
11210
+ ? Object.values(workflowSnapshotResourceVersions({ projections: { timeline: [existing] } }))[0]
11211
+ : "";
11212
+ const incomingVersion = Object.values(workflowSnapshotResourceVersions({ projections: { timeline: [item] } }))[0] || "";
11213
+ if (!existing || existingVersion !== incomingVersion) {
11214
+ return { error: `projections.timeline may not modify entries owned by source ${item?.source || "unknown"}` };
11215
+ }
11216
+ }
11217
+ const timeline = [...foreignCurrent, ...ownIncoming];
11218
+ return {
11219
+ ...report,
11220
+ projections: { ...report.projections, timeline },
11221
+ event: { ...report.event, projections: { ...report.event.projections, timeline } },
11222
+ };
11223
+ }
11224
+
10884
11225
  function prdWorkflowRuntimeEventDedupeKey(event = {}, index = 0) {
11226
+ const producer = prdWorkflowRuntimeEventProducer(event);
11227
+ const operation = prdWorkflowRuntimeEventOperation(event);
10885
11228
  const stage = prdWorkflowRuntimeEventCanonicalStage(event);
10886
11229
  const aggregateByStage = event.aggregateByStage !== false && event.aggregate_by_stage !== false;
11230
+ const stableActionKey = String(event?.actionModel?.key || "").trim();
11231
+ if (stableActionKey && aggregateByStage && String(event?.type || "") === "workflow-report") {
11232
+ return `producer:${producer}:operation:${operation}:action:${stableActionKey}`;
11233
+ }
10887
11234
  if (stage) {
10888
11235
  const issue = event?.issueKey || event?.issue_key || event?.issue;
10889
11236
  const platform = event?.platform;
10890
11237
  if (aggregateByStage || issue || platform) {
10891
- return ["stage", event?.scope, issue, platform, stage]
11238
+ return ["producer", producer, "operation", operation, "stage", event?.scope, issue, platform, stage]
10892
11239
  .map((value) => String(value || "").trim())
10893
11240
  .join(":");
10894
11241
  }
10895
11242
  }
10896
11243
  const id = String(event?.id || event?.eventId || event?.event_id || "").trim();
10897
- if (id) return `id:${id}`;
11244
+ if (id) return `producer:${producer}:operation:${operation}:id:${id}`;
10898
11245
  if (stage) {
10899
- return ["stage", event?.scope, event?.issueKey || event?.issue_key || event?.issue, event?.platform, stage]
11246
+ return ["producer", producer, "operation", operation, "stage", event?.scope, event?.issueKey || event?.issue_key || event?.issue, event?.platform, stage]
10900
11247
  .map((value) => String(value || "").trim())
10901
11248
  .join(":");
10902
11249
  }
10903
- return `idx:${index}`;
11250
+ return `producer:${producer}:operation:${operation}:idx:${index}`;
10904
11251
  }
10905
11252
 
10906
11253
  function prdWorkflowMergeRuntimeEventList(snapshotEvents = [], runtimeEvents = []) {
@@ -11169,14 +11516,24 @@ function prdWorkflowMergeRuntimeEvents(scopedRoot, tapdId, snapshot) {
11169
11516
  const globalState = prdWorkflowGlobalStateFromEvents(tapdId, snapshot, runtimeEvents);
11170
11517
  const artifacts = mergeWorkflowArtifacts(snapshot?.artifacts, runtimeEvents);
11171
11518
  const projections = materializeWorkflowProjections(snapshot, runtimeEvents);
11172
- return {
11519
+ const extensions = materializeWorkflowExtensions(snapshot, runtimeEvents);
11520
+ const prdFlowExtension = extensions["prd-flow"] && typeof extensions["prd-flow"] === "object"
11521
+ ? extensions["prd-flow"]
11522
+ : {};
11523
+ const prdFlowExtensionView = {};
11524
+ for (const key of ["issues", "issueGroups", "issue_groups", "epics", "epicGroups", "epic_groups", "aiDocs", "ai_docs"]) {
11525
+ if (Object.prototype.hasOwnProperty.call(prdFlowExtension, key)) prdFlowExtensionView[key] = prdFlowExtension[key];
11526
+ }
11527
+ const materialized = {
11173
11528
  ...snapshot,
11529
+ ...prdFlowExtensionView,
11174
11530
  workflow: globalState.workflow,
11175
11531
  overall,
11176
11532
  globalState,
11177
11533
  artifacts,
11178
11534
  projections,
11179
- runtimeRevision: workflowRuntimeRevision(globalState, artifacts, runtimeEvents, projections),
11535
+ extensions,
11536
+ runtimeRevision: workflowRuntimeRevision(globalState, artifacts, runtimeEvents, projections, extensions),
11180
11537
  runtimeEvents,
11181
11538
  events,
11182
11539
  sources: {
@@ -11184,6 +11541,8 @@ function prdWorkflowMergeRuntimeEvents(scopedRoot, tapdId, snapshot) {
11184
11541
  runtimeEventsUpdatedAt: runtime.updatedAt || "",
11185
11542
  },
11186
11543
  };
11544
+ materialized.resourceVersions = workflowSnapshotResourceVersions(materialized);
11545
+ return materialized;
11187
11546
  }
11188
11547
 
11189
11548
  function prdWorkflowMockSnapshot(scopedRoot, tapdId = "mock-prd") {
@@ -12587,6 +12946,95 @@ export function startUiServer({
12587
12946
  json(res, 200, { token: getSessionTokenFromRequest(req) || "" });
12588
12947
  return;
12589
12948
  }
12949
+ if (req.method === "POST" && url.pathname === "/api/workflows/access/sync") {
12950
+ if (!authUser?.userId) {
12951
+ json(res, 401, { error: "Authentication required" });
12952
+ return;
12953
+ }
12954
+ let payload;
12955
+ try {
12956
+ payload = JSON.parse(await readBody(req, 256 * 1024));
12957
+ } catch (error) {
12958
+ json(res, error?.status === 413 ? 413 : 400, { error: error?.status === 413 ? error.message : "Invalid JSON body" });
12959
+ return;
12960
+ }
12961
+ try {
12962
+ const workflow = normalizeWorkflowReference(payload);
12963
+ if (workflow.error) {
12964
+ json(res, 400, { error: workflow.error });
12965
+ return;
12966
+ }
12967
+ if (workflow.namespace !== "tapd") {
12968
+ json(res, 400, { error: `Unsupported Workflow authority namespace: ${workflow.namespace}` });
12969
+ return;
12970
+ }
12971
+ const authorityPayload = payload?.authority && typeof payload.authority === "object" && !Array.isArray(payload.authority)
12972
+ ? payload.authority
12973
+ : {};
12974
+ const authorityType = String(authorityPayload.type || payload.authorityType || "tapd").trim().toLowerCase();
12975
+ const ownerIdentity = workflowAuthorityIdentity(authorityPayload.owner ?? payload.owner);
12976
+ const participantIdentities = workflowAuthorityIdentities(authorityPayload.participants ?? payload.participants);
12977
+ if (!ownerIdentity) {
12978
+ json(res, 400, { error: "authority.owner is required" });
12979
+ return;
12980
+ }
12981
+ const ownerUser = findWorkspaceShareUser(ownerIdentity);
12982
+ if (!ownerUser) {
12983
+ json(res, 422, {
12984
+ error: "TAPD owner has not registered or logged in to AgentFlow",
12985
+ owner: ownerIdentity,
12986
+ });
12987
+ return;
12988
+ }
12989
+ const resolvedParticipants = [];
12990
+ const unresolvedParticipants = [];
12991
+ for (const identity of participantIdentities) {
12992
+ const user = findWorkspaceShareUser(identity);
12993
+ if (user) resolvedParticipants.push(user);
12994
+ else unresolvedParticipants.push(identity);
12995
+ }
12996
+ const result = syncPrdWorkflowAuthority({
12997
+ tapdId: workflow.id,
12998
+ userId: userCtx.userId,
12999
+ isAdmin: userCtx.isAdmin === true,
13000
+ authority: authorityType,
13001
+ ownerUserId: ownerUser.userId,
13002
+ ownerIdentity,
13003
+ participantUserIds: resolvedParticipants.map((user) => user.userId),
13004
+ participantIdentities,
13005
+ unresolvedParticipants,
13006
+ observedAt: authorityPayload.observedAt || authorityPayload.observed_at || payload.observedAt || payload.observed_at,
13007
+ revision: authorityPayload.revision || payload.revision,
13008
+ });
13009
+ if (result.error) {
13010
+ json(res, result.status || 400, { error: result.error });
13011
+ return;
13012
+ }
13013
+ const collaboration = prdWorkflowCollaborationSummaryWithUsers(result.record, userCtx.userId);
13014
+ prdWorkflowBroadcast(prdWorkflowKey(userCtx, "", "", workflow.id), {
13015
+ type: "authority.synced",
13016
+ tapdId: workflow.id,
13017
+ ownerId: result.record.ownerId,
13018
+ });
13019
+ json(res, 200, {
13020
+ ok: true,
13021
+ workflow,
13022
+ created: result.created === true,
13023
+ ownerChanged: result.ownerChanged === true,
13024
+ collaboration,
13025
+ matchedParticipants: resolvedParticipants.map((user) => ({
13026
+ userId: user.userId,
13027
+ username: user.username,
13028
+ role: "viewer",
13029
+ source: "tapd",
13030
+ })),
13031
+ unresolvedParticipants,
13032
+ });
13033
+ } catch (error) {
13034
+ json(res, 500, { error: (error && error.message) || String(error) });
13035
+ }
13036
+ return;
13037
+ }
12590
13038
  if (req.method === "GET" && url.pathname === "/api/prd-workflows") {
12591
13039
  if (!authUser?.userId) {
12592
13040
  json(res, 401, { error: "Unauthorized" });
@@ -12610,7 +13058,7 @@ export function startUiServer({
12610
13058
  records = listPrdWorkflowCollaborationsForUser(userCtx.userId);
12611
13059
  }
12612
13060
  const workflows = records.map((record) => {
12613
- const stateRoot = path.resolve(getAgentflowUserDataRoot(record.ownerId));
13061
+ const stateRoot = path.resolve(getAgentflowUserDataRoot(record.stateOwnerId || record.ownerId));
12614
13062
  const tapdId = String(record.tapdId || "").trim();
12615
13063
  const project = prdWorkflowReadProjectState(stateRoot, tapdId);
12616
13064
  const latestClient = prdWorkflowLatestClientSnapshot(stateRoot, stateRoot, tapdId);
@@ -12803,7 +13251,12 @@ export function startUiServer({
12803
13251
  json(res, 200, {
12804
13252
  ok: true,
12805
13253
  collaboration: prdWorkflowCollaborationSummaryWithUsers(record, userCtx.userId),
12806
- member: { userId: targetUser.userId, username: targetUser.username, role: "editor" },
13254
+ member: {
13255
+ userId: targetUser.userId,
13256
+ username: targetUser.username,
13257
+ role: payload?.role === "viewer" ? "viewer" : "reporter",
13258
+ source: "explicit",
13259
+ },
12807
13260
  });
12808
13261
  } catch (error) {
12809
13262
  json(res, 400, { error: (error && error.message) || String(error) });
@@ -12955,6 +13408,8 @@ export function startUiServer({
12955
13408
  }
12956
13409
 
12957
13410
  if (req.method === "POST" && url.pathname === "/api/prd-workflow/snapshot") {
13411
+ res.setHeader("Deprecation", "true");
13412
+ res.setHeader("Link", "</api/workflows/report>; rel=\"successor-version\"");
12958
13413
  if (!authUser?.userId) {
12959
13414
  json(res, 401, { error: "Authentication required" });
12960
13415
  return;
@@ -13030,7 +13485,7 @@ export function startUiServer({
13030
13485
  stageKey: reportMeta.stageKey,
13031
13486
  };
13032
13487
  const existingClientState = prdWorkflowReadClientState(scopedRoot, tapdId);
13033
- const existingClientId = prdWorkflowSafeStateId(reportMeta.clientId || "anonymous");
13488
+ const existingClientId = prdWorkflowSafeStateId(`${reportMeta.reportSource || "legacy"}:${reportMeta.clientId || "anonymous"}`);
13034
13489
  const previousClientSnapshot = existingClientState.clients?.[existingClientId]?.snapshot || null;
13035
13490
  const stampedSnapshot = prdWorkflowStampCurrentActionEntryTimes(
13036
13491
  scopedRoot,
@@ -13188,6 +13643,10 @@ export function startUiServer({
13188
13643
  json(res, 200, {
13189
13644
  ok: true,
13190
13645
  snapshot: withDiagnostic,
13646
+ compatibility: {
13647
+ deprecatedEndpoint: "/api/prd-workflow/snapshot",
13648
+ replacement: "/api/workflows/report with observation.state",
13649
+ },
13191
13650
  ...(workflowShare ? { workflowShare, shareUrl: workflowShare.shortUrl || workflowShare.url } : {}),
13192
13651
  });
13193
13652
  } catch (e) {
@@ -13701,13 +14160,14 @@ export function startUiServer({
13701
14160
  }
13702
14161
  let payload;
13703
14162
  try {
13704
- payload = JSON.parse(await readBody(req));
13705
- } catch {
13706
- json(res, 400, { error: "Invalid JSON body" });
14163
+ payload = JSON.parse(await readBody(req, 1024 * 1024));
14164
+ } catch (error) {
14165
+ json(res, error?.status === 413 ? 413 : 400, { error: error?.status === 413 ? error.message : "Invalid JSON body" });
13707
14166
  return;
13708
14167
  }
14168
+ let releaseWorkflowWriteLock = null;
13709
14169
  try {
13710
- const report = normalizeWorkflowReport(payload);
14170
+ let report = normalizeWorkflowReport(payload);
13711
14171
  if (report.error) {
13712
14172
  json(res, 400, { error: report.error });
13713
14173
  return;
@@ -13740,6 +14200,7 @@ export function startUiServer({
13740
14200
  }
13741
14201
  const scopedRoot = workflowScope.stateRoot;
13742
14202
  prdWorkflowMigrateLegacyState(workflowScope.executionRoot, scopedRoot, tapdId);
14203
+ releaseWorkflowWriteLock = await prdWorkflowAcquireWriteLock(`${scopedRoot}\t${tapdId}`);
13743
14204
  const currentSnapshot = prdWorkflowMaterializeSnapshot(
13744
14205
  workflowScope.executionRoot,
13745
14206
  scopedRoot,
@@ -13747,26 +14208,28 @@ export function startUiServer({
13747
14208
  userCtx,
13748
14209
  { flowSource, flowId },
13749
14210
  );
13750
- const acceptedRevisions = new Set([
13751
- String(currentSnapshot.runtimeRevision || "").trim(),
13752
- String(currentSnapshot.revision || "").trim(),
13753
- ].filter(Boolean));
13754
- if (report.expectedRevision && acceptedRevisions.size && !acceptedRevisions.has(report.expectedRevision)) {
13755
- json(res, 409, {
13756
- error: "Workflow state changed; refresh before reporting",
13757
- conflict: {
13758
- type: "workflow-revision-conflict",
13759
- expectedRevision: report.expectedRevision,
13760
- currentRevision: currentSnapshot.runtimeRevision || currentSnapshot.revision || "",
13761
- workflow: report.workflow,
13762
- },
13763
- snapshot: currentSnapshot,
13764
- });
13765
- return;
13766
- }
14211
+ const currentRuntimeRevision = String(currentSnapshot.runtimeRevision || "").trim();
13767
14212
  if (report.idempotencyKey) {
13768
- const existing = prdWorkflowFindCompletedIdempotencyEvent(scopedRoot, tapdId, report.idempotencyKey);
14213
+ const existing = prdWorkflowFindCompletedIdempotencyEvent(
14214
+ scopedRoot,
14215
+ tapdId,
14216
+ report.idempotencyKey,
14217
+ report.event.source,
14218
+ );
13769
14219
  if (existing) {
14220
+ const existingFingerprint = prdWorkflowIdempotencyFingerprint(existing, report.idempotencyKey);
14221
+ if (existingFingerprint && existingFingerprint !== report.event.idempotencyFingerprint) {
14222
+ json(res, 409, {
14223
+ error: "Idempotency key was already used for a different Workflow report",
14224
+ conflict: {
14225
+ type: "workflow-idempotency-conflict",
14226
+ idempotencyKey: report.idempotencyKey,
14227
+ workflow: report.workflow,
14228
+ },
14229
+ snapshot: currentSnapshot,
14230
+ });
14231
+ return;
14232
+ }
13770
14233
  json(res, 200, {
13771
14234
  ok: true,
13772
14235
  alreadyApplied: true,
@@ -13777,15 +14240,101 @@ export function startUiServer({
13777
14240
  return;
13778
14241
  }
13779
14242
  }
13780
- const event = prdWorkflowAppendRuntimeEvent(scopedRoot, tapdId, {
14243
+ const ownershipConflicts = prdWorkflowGlobalOwnershipConflicts(report, currentSnapshot);
14244
+ if (ownershipConflicts.length) {
14245
+ json(res, 409, {
14246
+ error: "Workflow globalState paths are owned by another report source",
14247
+ conflict: {
14248
+ type: "workflow-resource-ownership-conflict",
14249
+ conflicts: ownershipConflicts,
14250
+ workflow: report.workflow,
14251
+ },
14252
+ snapshot: currentSnapshot,
14253
+ });
14254
+ return;
14255
+ }
14256
+ const resourceKeys = workflowReportResourceKeys(report, currentSnapshot);
14257
+ const missingExpectedVersionKeys = Object.keys(report.expectedVersions).length
14258
+ ? resourceKeys.filter((key) => !Object.prototype.hasOwnProperty.call(report.expectedVersions, key))
14259
+ : [];
14260
+ if (missingExpectedVersionKeys.length) {
14261
+ json(res, 400, {
14262
+ error: "expectedVersions must include every resource key touched by this report",
14263
+ missingExpectedVersionKeys,
14264
+ resourceKeys,
14265
+ });
14266
+ return;
14267
+ }
14268
+ const expectedTouchedVersions = Object.fromEntries(
14269
+ resourceKeys
14270
+ .filter((key) => Object.prototype.hasOwnProperty.call(report.expectedVersions, key))
14271
+ .map((key) => [key, report.expectedVersions[key]]),
14272
+ );
14273
+ const resourceConflicts = prdWorkflowResourceVersionConflicts(
14274
+ expectedTouchedVersions,
14275
+ currentSnapshot.resourceVersions || {},
14276
+ );
14277
+ if (resourceConflicts.length) {
14278
+ json(res, 409, {
14279
+ error: "Workflow resources changed; refresh the conflicting keys before reporting",
14280
+ conflict: {
14281
+ type: "workflow-resource-conflict",
14282
+ conflicts: resourceConflicts,
14283
+ workflow: report.workflow,
14284
+ },
14285
+ snapshot: currentSnapshot,
14286
+ });
14287
+ return;
14288
+ }
14289
+ if (!Object.keys(report.expectedVersions).length && report.expectedRevision && currentRuntimeRevision && report.expectedRevision !== currentRuntimeRevision) {
14290
+ json(res, 409, {
14291
+ error: "Workflow state changed; refresh before reporting",
14292
+ conflict: {
14293
+ type: "workflow-revision-conflict",
14294
+ expectedRevision: report.expectedRevision,
14295
+ currentRevision: currentRuntimeRevision,
14296
+ workflow: report.workflow,
14297
+ },
14298
+ snapshot: currentSnapshot,
14299
+ });
14300
+ return;
14301
+ }
14302
+ report = prdWorkflowMergeProducerTimeline(report, currentSnapshot);
14303
+ if (report.error) {
14304
+ json(res, 400, { error: report.error });
14305
+ return;
14306
+ }
14307
+ let observation = null;
14308
+ if (report.observation) {
14309
+ const observationPayload = {
14310
+ ...payload,
14311
+ tapdId,
14312
+ clientId: report.observation.clientId || payload.clientId || payload.source || "workflow-reporter",
14313
+ observedAt: report.observation.observedAt || payload.observedAt || "",
14314
+ scope: report.observation.scope || payload.scope || "client",
14315
+ reportSource: report.event.source,
14316
+ };
14317
+ observation = prdWorkflowStoreClientObservation({
14318
+ scopedRoot,
14319
+ tapdId,
14320
+ rawState: report.observation.state,
14321
+ payload: observationPayload,
14322
+ req,
14323
+ userCtx,
14324
+ flowSource,
14325
+ flowId,
14326
+ });
14327
+ }
14328
+ const shouldStoreEvent = report.hasRuntimeUpdate || Boolean(report.idempotencyKey);
14329
+ const event = shouldStoreEvent ? prdWorkflowAppendRuntimeEvent(scopedRoot, tapdId, {
13781
14330
  ...report.event,
13782
14331
  tapdId,
13783
14332
  actor: {
13784
14333
  userId: String(userCtx.userId || ""),
13785
14334
  username: String(authUser.username || userCtx.userId || ""),
13786
14335
  },
13787
- });
13788
- if (!event) throw new Error("Failed to store workflow report");
14336
+ }) : null;
14337
+ if (shouldStoreEvent && !event) throw new Error("Failed to store workflow report");
13789
14338
  const snapshot = prdWorkflowWithAgentflowTokenDiagnostic(
13790
14339
  prdWorkflowMaterializeSnapshot(
13791
14340
  workflowScope.executionRoot,
@@ -13798,16 +14347,32 @@ export function startUiServer({
13798
14347
  );
13799
14348
  prdWorkflowBroadcast(
13800
14349
  prdWorkflowKey(userCtx, flowSource, flowId, tapdId),
13801
- { type: "workflow-report", tapdId, workflow: report.workflow, event, snapshot },
14350
+ { type: "workflow-report", tapdId, workflow: report.workflow, event, observation: Boolean(observation), snapshot },
13802
14351
  );
13803
- json(res, 200, { ok: true, report, event, snapshot });
14352
+ json(res, 200, {
14353
+ ok: true,
14354
+ report,
14355
+ resourceKeys,
14356
+ event,
14357
+ observation: observation ? {
14358
+ accepted: true,
14359
+ clientId: observation.reportMeta.clientId,
14360
+ observedAt: observation.reportMeta.observedAt,
14361
+ schema: report.observation.schema,
14362
+ } : null,
14363
+ snapshot,
14364
+ });
13804
14365
  } catch (e) {
13805
14366
  json(res, 500, { error: (e && e.message) || String(e) });
14367
+ } finally {
14368
+ releaseWorkflowWriteLock?.();
13806
14369
  }
13807
14370
  return;
13808
14371
  }
13809
14372
 
13810
14373
  if (req.method === "POST" && url.pathname === "/api/prd-workflow/event") {
14374
+ res.setHeader("Deprecation", "true");
14375
+ res.setHeader("Link", "</api/workflows/report>; rel=\"successor-version\"");
13811
14376
  if (!authUser?.userId) {
13812
14377
  json(res, 401, { error: "Authentication required" });
13813
14378
  return;
@@ -13858,31 +14423,49 @@ export function startUiServer({
13858
14423
  getSessionTokenFromRequest(req) || "",
13859
14424
  );
13860
14425
  prdWorkflowBroadcast(prdWorkflowKey(userCtx, flowSource, flowId, tapdId), { type: "runtime-event", tapdId, event, snapshot });
13861
- json(res, 200, { ok: true, event, snapshot });
14426
+ json(res, 200, {
14427
+ ok: true,
14428
+ event,
14429
+ snapshot,
14430
+ compatibility: {
14431
+ deprecatedEndpoint: "/api/prd-workflow/event",
14432
+ replacement: "/api/workflows/report with action/artifacts/extensions",
14433
+ },
14434
+ });
13862
14435
  } catch (e) {
13863
14436
  json(res, 500, { error: (e && e.message) || String(e) });
13864
14437
  }
13865
14438
  return;
13866
14439
  }
13867
14440
 
13868
- if (req.method === "POST" && url.pathname === "/api/prd-workflow/review-link") {
14441
+ if (req.method === "POST" && (
14442
+ url.pathname === "/api/workflow-artifacts/publish" ||
14443
+ url.pathname === "/api/prd-workflow/review-link"
14444
+ )) {
14445
+ const legacyReviewEndpoint = url.pathname === "/api/prd-workflow/review-link";
13869
14446
  if (!authUser?.userId) {
13870
14447
  json(res, 401, { error: "Authentication required" });
13871
14448
  return;
13872
14449
  }
13873
14450
  let payload;
13874
14451
  try {
13875
- payload = JSON.parse(await readBody(req));
13876
- } catch {
13877
- json(res, 400, { error: "Invalid JSON body" });
14452
+ payload = JSON.parse(await readBody(req, 600000));
14453
+ } catch (error) {
14454
+ json(res, error?.status === 413 ? 413 : 400, { error: error?.status === 413 ? error.message : "Invalid JSON body" });
13878
14455
  return;
13879
14456
  }
14457
+ let releaseWorkflowWriteLock = null;
13880
14458
  try {
13881
- const tapdId = String(payload.tapdId || payload.tapd_id || "").trim();
13882
- if (!tapdId) {
13883
- json(res, 400, { error: "Missing tapdId" });
14459
+ const workflow = normalizeWorkflowReference(payload);
14460
+ if (workflow.error) {
14461
+ json(res, 400, { error: workflow.error });
14462
+ return;
14463
+ }
14464
+ if (workflow.namespace !== "tapd") {
14465
+ json(res, 400, { error: `Unsupported workflow namespace: ${workflow.namespace}` });
13884
14466
  return;
13885
14467
  }
14468
+ const tapdId = workflow.id;
13886
14469
  const flowId = String(payload.flowId || "").trim();
13887
14470
  const flowSource = String(payload.flowSource || "user").trim() || "user";
13888
14471
  const archived = payload.archived === true || payload.flowArchived === true;
@@ -13899,6 +14482,194 @@ export function startUiServer({
13899
14482
  }
13900
14483
  const scopedRoot = workflowScope.stateRoot;
13901
14484
  prdWorkflowMigrateLegacyState(workflowScope.executionRoot, scopedRoot, tapdId);
14485
+ const producer = String(payload.source || (legacyReviewEndpoint ? "prd-flow" : "")).trim().toLowerCase();
14486
+ if (!producer) {
14487
+ json(res, 400, { error: "Workflow artifact publish requires source" });
14488
+ return;
14489
+ }
14490
+ if (!/^[a-z][a-z0-9._-]{0,119}$/.test(producer)) {
14491
+ json(res, 400, { error: "Invalid workflow report source" });
14492
+ return;
14493
+ }
14494
+ const fieldLimits = [
14495
+ [payload.title || payload.label, 160, "title"],
14496
+ [payload.stage || payload.stageKey || payload.stage_key, 240, "stage"],
14497
+ [payload.issueKey || payload.issue_key || payload.issue, 240, "issueKey"],
14498
+ [payload.platform, 80, "platform"],
14499
+ [payload.artifactLabel, 500, "artifactLabel"],
14500
+ [payload.reviewId || payload.review_id, 500, "reviewId"],
14501
+ ];
14502
+ const oversizedField = fieldLimits.find(([value, max]) => String(value || "").trim().length > max);
14503
+ if (oversizedField) {
14504
+ json(res, 400, { error: `${oversizedField[2]} exceeds ${oversizedField[1]} characters` });
14505
+ return;
14506
+ }
14507
+ const markdown = String(payload.markdown || payload.content || payload.rawOutput || "");
14508
+ if (!markdown.trim()) {
14509
+ json(res, 400, { error: "Missing review markdown" });
14510
+ return;
14511
+ }
14512
+ if (Buffer.byteLength(markdown, "utf-8") > 500000) {
14513
+ json(res, 413, { error: "Review markdown exceeds 500000 bytes" });
14514
+ return;
14515
+ }
14516
+ const requestedDurability = String(
14517
+ payload.durability || (payload.durable === true || payload.permanent === true ? "durable" : "temporary"),
14518
+ ).trim().toLowerCase() || "temporary";
14519
+ if (!["temporary", "durable"].includes(requestedDurability)) {
14520
+ json(res, 400, { error: "durability must be temporary or durable" });
14521
+ return;
14522
+ }
14523
+ const ttlInput = payload.ttlDays ?? payload.ttl_days;
14524
+ if (requestedDurability === "temporary" && ttlInput != null) {
14525
+ const ttlDays = Number(ttlInput);
14526
+ if (!Number.isInteger(ttlDays) || ttlDays < 1 || ttlDays > 30) {
14527
+ json(res, 400, { error: "ttlDays must be an integer between 1 and 30" });
14528
+ return;
14529
+ }
14530
+ }
14531
+ const explicitExpiresAt = String(payload.expiresAt || payload.expires_at || "").trim();
14532
+ if (explicitExpiresAt && (!Number.isFinite(Date.parse(explicitExpiresAt)) || Date.parse(explicitExpiresAt) <= Date.now())) {
14533
+ json(res, 400, { error: "expiresAt must be a valid future date" });
14534
+ return;
14535
+ }
14536
+ const idempotencyKey = String(
14537
+ payload.idempotencyKey || payload.idempotency_key || "",
14538
+ ).trim();
14539
+ if (idempotencyKey.length > 500) {
14540
+ json(res, 400, { error: "idempotencyKey exceeds 500 characters" });
14541
+ return;
14542
+ }
14543
+ if (String(payload.artifactKey || payload.artifact_key || "").trim().length > 500) {
14544
+ json(res, 400, { error: "artifactKey exceeds 500 characters" });
14545
+ return;
14546
+ }
14547
+ const artifactKey = prdWorkflowReviewArtifactKey(tapdId, payload);
14548
+ const idempotencyFingerprint = prdWorkflowRevisionHash({
14549
+ operation: "artifact.publish",
14550
+ workflow,
14551
+ producer,
14552
+ title: String(payload.title || payload.label || "").trim(),
14553
+ markdown,
14554
+ stage: String(payload.stage || payload.stageKey || payload.stage_key || "").trim(),
14555
+ issueKey: String(payload.issueKey || payload.issue_key || payload.issue || "").trim(),
14556
+ platform: String(payload.platform || "").trim(),
14557
+ artifactKey,
14558
+ artifactLabel: String(payload.artifactLabel || "").trim(),
14559
+ durability: requestedDurability,
14560
+ ttlDays: ttlInput ?? null,
14561
+ expiresAt: explicitExpiresAt,
14562
+ });
14563
+ releaseWorkflowWriteLock = await prdWorkflowAcquireWriteLock(`${scopedRoot}\t${tapdId}`);
14564
+ const currentSnapshot = prdWorkflowMaterializeSnapshot(
14565
+ workflowScope.executionRoot,
14566
+ scopedRoot,
14567
+ tapdId,
14568
+ userCtx,
14569
+ { flowSource, flowId },
14570
+ );
14571
+ const expectedRevision = String(payload.expectedRevision || payload.expected_revision || "").trim();
14572
+ if (expectedRevision.length > 500) {
14573
+ json(res, 400, { error: "expectedRevision exceeds 500 characters" });
14574
+ return;
14575
+ }
14576
+ const currentRuntimeRevision = String(currentSnapshot.runtimeRevision || "").trim();
14577
+ if (idempotencyKey) {
14578
+ const existing = prdWorkflowFindIdempotencyEvent(
14579
+ scopedRoot,
14580
+ tapdId,
14581
+ idempotencyKey,
14582
+ producer,
14583
+ false,
14584
+ "artifact.publish",
14585
+ );
14586
+ if (existing) {
14587
+ const existingFingerprint = prdWorkflowIdempotencyFingerprint(existing, idempotencyKey);
14588
+ if (existingFingerprint && existingFingerprint !== idempotencyFingerprint) {
14589
+ json(res, 409, {
14590
+ error: "Idempotency key was already used for different Artifact content",
14591
+ conflict: { type: "workflow-idempotency-conflict", idempotencyKey, workflow },
14592
+ snapshot: currentSnapshot,
14593
+ });
14594
+ return;
14595
+ }
14596
+ const artifact = Array.isArray(existing.artifacts) ? existing.artifacts[0] : null;
14597
+ json(res, 200, {
14598
+ ok: true,
14599
+ alreadyApplied: true,
14600
+ workflow,
14601
+ artifact,
14602
+ review: artifact ? {
14603
+ id: existing.reviewId || "",
14604
+ url: artifact.canonicalUrl || artifact.url || "",
14605
+ shortUrl: artifact.shortUrl || "",
14606
+ shortCode: existing.reviewShortCode || "",
14607
+ durability: existing.durability || artifact.durability || "",
14608
+ expiresAt: existing.expiresAt || artifact.expiresAt || "",
14609
+ } : null,
14610
+ event: existing,
14611
+ snapshot: currentSnapshot,
14612
+ });
14613
+ return;
14614
+ }
14615
+ }
14616
+ const resourceKey = `artifact:${producer}:${artifactKey}`;
14617
+ const hasExpectedVersionsField = Object.prototype.hasOwnProperty.call(payload, "expectedVersions")
14618
+ || Object.prototype.hasOwnProperty.call(payload, "expected_versions");
14619
+ const rawExpectedVersionsInput = Object.prototype.hasOwnProperty.call(payload, "expectedVersions")
14620
+ ? payload.expectedVersions
14621
+ : payload.expected_versions;
14622
+ if (hasExpectedVersionsField && (!rawExpectedVersionsInput || typeof rawExpectedVersionsInput !== "object" || Array.isArray(rawExpectedVersionsInput))) {
14623
+ json(res, 400, { error: "expectedVersions must be an object" });
14624
+ return;
14625
+ }
14626
+ const rawExpectedVersions = hasExpectedVersionsField ? rawExpectedVersionsInput : {};
14627
+ const invalidExpectedVersionEntry = Object.entries(rawExpectedVersions).find(([key, value]) => (
14628
+ !String(key || "").trim() || String(key).length > 800 || /[\0\r\n]/.test(String(key)) ||
14629
+ String(value == null || value === "" ? "absent" : value).trim().length > 160
14630
+ ));
14631
+ if (invalidExpectedVersionEntry) {
14632
+ json(res, 400, { error: "expectedVersions contains an invalid resource key or version" });
14633
+ return;
14634
+ }
14635
+ if (Object.keys(rawExpectedVersions).length && !Object.prototype.hasOwnProperty.call(rawExpectedVersions, resourceKey)) {
14636
+ json(res, 400, {
14637
+ error: "expectedVersions must include the Artifact resource key touched by this publish",
14638
+ missingExpectedVersionKeys: [resourceKey],
14639
+ resourceKeys: [resourceKey],
14640
+ });
14641
+ return;
14642
+ }
14643
+ const expectedArtifactVersion = Object.prototype.hasOwnProperty.call(rawExpectedVersions, resourceKey)
14644
+ ? String(rawExpectedVersions[resourceKey] || "absent")
14645
+ : null;
14646
+ const resourceConflicts = expectedArtifactVersion == null
14647
+ ? []
14648
+ : prdWorkflowResourceVersionConflicts(
14649
+ { [resourceKey]: expectedArtifactVersion },
14650
+ currentSnapshot.resourceVersions || {},
14651
+ );
14652
+ if (resourceConflicts.length) {
14653
+ json(res, 409, {
14654
+ error: "Workflow artifact changed; refresh before publishing",
14655
+ conflict: { type: "workflow-resource-conflict", conflicts: resourceConflicts, workflow },
14656
+ snapshot: currentSnapshot,
14657
+ });
14658
+ return;
14659
+ }
14660
+ if (!Object.keys(rawExpectedVersions).length && expectedRevision && currentRuntimeRevision && expectedRevision !== currentRuntimeRevision) {
14661
+ json(res, 409, {
14662
+ error: "Workflow state changed; refresh before publishing",
14663
+ conflict: {
14664
+ type: "workflow-revision-conflict",
14665
+ expectedRevision,
14666
+ currentRevision: currentRuntimeRevision,
14667
+ workflow,
14668
+ },
14669
+ snapshot: currentSnapshot,
14670
+ });
14671
+ return;
14672
+ }
13902
14673
  const review = prdWorkflowCreateReview(
13903
14674
  scopedRoot,
13904
14675
  tapdId,
@@ -13920,7 +14691,6 @@ export function startUiServer({
13920
14691
  const shortUrl = shortLink?.shortUrl || "";
13921
14692
  const displayUrl = shortUrl || reviewUrl;
13922
14693
  const durability = review.durability || "temporary";
13923
- const artifactKey = prdWorkflowReviewArtifactKey(tapdId, payload);
13924
14694
  const reviewStageKey = prdWorkflowRuntimeEventCanonicalStage(payload)
13925
14695
  || payload.stageKey
13926
14696
  || payload.stage_key
@@ -13932,9 +14702,6 @@ export function startUiServer({
13932
14702
  const reviewSource = review.source && typeof review.source === "object" && !Array.isArray(review.source)
13933
14703
  ? review.source
13934
14704
  : { kind: durability === "durable" ? "ai-doc" : "local-draft", durability };
13935
- const idempotencyKey = String(
13936
- payload.idempotencyKey || payload.idempotency_key || "",
13937
- ).trim();
13938
14705
  const artifact = {
13939
14706
  key: artifactKey,
13940
14707
  label: payload.artifactLabel || "Markdown Review",
@@ -13950,6 +14717,7 @@ export function startUiServer({
13950
14717
  issueKey: payload.issueKey || payload.issue_key || "",
13951
14718
  platform: payload.platform || "",
13952
14719
  stageKey: reviewStageKey,
14720
+ producer,
13953
14721
  ...(reviewMrUrl ? { mrUrl: reviewMrUrl } : {}),
13954
14722
  ...(reviewMrIid ? { mrIid: reviewMrIid } : {}),
13955
14723
  ...(reviewCommitSha ? { commitSha: reviewCommitSha } : {}),
@@ -13957,6 +14725,8 @@ export function startUiServer({
13957
14725
  const event = prdWorkflowAppendRuntimeEvent(scopedRoot, tapdId, {
13958
14726
  id: `review-link:${artifactKey}`,
13959
14727
  type: "review-link",
14728
+ operation: "artifact.publish",
14729
+ source: producer,
13960
14730
  auxiliary: true,
13961
14731
  aggregateByStage: false,
13962
14732
  conflictOnArtifact: false,
@@ -13974,6 +14744,8 @@ export function startUiServer({
13974
14744
  ...(reviewMrIid ? { mrIid: reviewMrIid } : {}),
13975
14745
  ...(reviewCommitSha ? { commitSha: reviewCommitSha } : {}),
13976
14746
  idempotencyKey,
14747
+ idempotencyFingerprint,
14748
+ idempotencyFingerprints: idempotencyKey ? { [idempotencyKey]: idempotencyFingerprint } : {},
13977
14749
  durability,
13978
14750
  sourceArtifact: reviewSource,
13979
14751
  expiresAt: review.expiresAt || "",
@@ -13988,6 +14760,7 @@ export function startUiServer({
13988
14760
  persistence: "runtime",
13989
14761
  durability,
13990
14762
  source: reviewSource,
14763
+ producer,
13991
14764
  expiresAt: review.expiresAt || "",
13992
14765
  issueKey: artifact.issueKey,
13993
14766
  platform: artifact.platform,
@@ -14004,8 +14777,15 @@ export function startUiServer({
14004
14777
  getSessionTokenFromRequest(req) || "",
14005
14778
  );
14006
14779
  prdWorkflowBroadcast(prdWorkflowKey(userCtx, flowSource, flowId, tapdId), { type: "review-link", tapdId, event, snapshot });
14780
+ if (legacyReviewEndpoint) {
14781
+ res.setHeader("Deprecation", "true");
14782
+ res.setHeader("Link", "</api/workflow-artifacts/publish>; rel=\"successor-version\"");
14783
+ }
14007
14784
  json(res, 200, {
14008
14785
  ok: true,
14786
+ workflow,
14787
+ artifact,
14788
+ resourceKeys: [resourceKey],
14009
14789
  review: {
14010
14790
  ...review,
14011
14791
  url: reviewUrl,
@@ -14014,9 +14794,18 @@ export function startUiServer({
14014
14794
  },
14015
14795
  event,
14016
14796
  snapshot,
14797
+ ...(legacyReviewEndpoint ? {
14798
+ compatibility: {
14799
+ deprecatedEndpoint: "/api/prd-workflow/review-link",
14800
+ replacement: "/api/workflow-artifacts/publish",
14801
+ },
14802
+ } : {}),
14017
14803
  });
14018
14804
  } catch (e) {
14019
- json(res, 500, { error: (e && e.message) || String(e) });
14805
+ const status = Number(e?.status);
14806
+ json(res, status >= 400 && status < 500 ? status : 500, { error: (e && e.message) || String(e) });
14807
+ } finally {
14808
+ releaseWorkflowWriteLock?.();
14020
14809
  }
14021
14810
  return;
14022
14811
  }