@fieldwangai/agentflow 0.1.135 → 0.1.136

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.
@@ -176,6 +176,7 @@ import {
176
176
  } from "./teams.mjs";
177
177
  import {
178
178
  legacyOverallToGlobalState,
179
+ materializeWorkflowExtensions,
179
180
  materializeWorkflowGlobalState,
180
181
  materializeWorkflowProjections,
181
182
  mergeWorkflowArtifactLists,
@@ -7997,7 +7998,17 @@ function prdWorkflowSafeStateId(value) {
7997
7998
  }
7998
7999
 
7999
8000
  function prdWorkflowReviewIdFromRequest(tapdId, payload = {}, durability = "temporary") {
8000
- if (durability === "temporary") return `r-${crypto.randomBytes(5).toString("hex")}`;
8001
+ if (durability === "temporary") {
8002
+ const idempotencyKey = String(payload.idempotencyKey || payload.idempotency_key || "").trim();
8003
+ if (!idempotencyKey) return `r-${crypto.randomBytes(5).toString("hex")}`;
8004
+ const source = String(payload.source || "agentflow-cli").trim().toLowerCase() || "agentflow-cli";
8005
+ const digest = crypto
8006
+ .createHash("sha256")
8007
+ .update(JSON.stringify({ tapdId: String(tapdId || ""), source, idempotencyKey }))
8008
+ .digest("hex")
8009
+ .slice(0, 12);
8010
+ return `r-${digest}`;
8011
+ }
8001
8012
  const requested = String(payload.reviewId || payload.review_id || "").trim();
8002
8013
  if (!requested) return `review_${Date.now().toString(36)}_${crypto.randomBytes(4).toString("hex")}`;
8003
8014
  const safeRequested = prdWorkflowSafeStateId(requested);
@@ -10171,6 +10182,89 @@ function prdWorkflowSnapshotMetaFromReport(payload = {}, rawSnapshot = {}, req =
10171
10182
  };
10172
10183
  }
10173
10184
 
10185
+ function prdWorkflowStoreClientObservation({
10186
+ scopedRoot,
10187
+ tapdId,
10188
+ rawState,
10189
+ payload = {},
10190
+ req = null,
10191
+ userCtx = {},
10192
+ flowSource = "user",
10193
+ flowId = "",
10194
+ }) {
10195
+ const normalizedSnapshot = {
10196
+ ...prdWorkflowSnapshotFromParsed(scopedRoot, tapdId, rawState, userCtx, { flowSource, flowId }),
10197
+ clientReportedAt: new Date().toISOString(),
10198
+ sources: {
10199
+ ...(rawState.sources && typeof rawState.sources === "object" ? rawState.sources : {}),
10200
+ executionMode: "workflow-report",
10201
+ },
10202
+ };
10203
+ const reportMeta = prdWorkflowSnapshotMetaFromReport(payload, rawState, req, userCtx);
10204
+ const reportSource = {
10205
+ ...(normalizedSnapshot.sources && typeof normalizedSnapshot.sources === "object" ? normalizedSnapshot.sources : {}),
10206
+ executionMode: "workflow-report",
10207
+ truth: "observation",
10208
+ authority: "client",
10209
+ persistence: "runtime",
10210
+ clientId: reportMeta.clientId,
10211
+ clientUserId: reportMeta.userId,
10212
+ clientReportedAt: reportMeta.reportedAt,
10213
+ clientObservedAt: reportMeta.observedAt,
10214
+ baseRevision: reportMeta.baseRevision,
10215
+ scope: reportMeta.scope,
10216
+ platform: reportMeta.platform,
10217
+ issueKey: reportMeta.issueKey,
10218
+ stageKey: reportMeta.stageKey,
10219
+ };
10220
+ const existingClientState = prdWorkflowReadClientState(scopedRoot, tapdId);
10221
+ const existingClientId = prdWorkflowSafeStateId(reportMeta.clientId || "anonymous");
10222
+ const previousClientSnapshot = existingClientState.clients?.[existingClientId]?.snapshot || null;
10223
+ const stampedSnapshot = prdWorkflowStampCurrentActionEntryTimes(
10224
+ scopedRoot,
10225
+ tapdId,
10226
+ normalizedSnapshot,
10227
+ existingClientState,
10228
+ reportMeta,
10229
+ );
10230
+ const storedObservationSnapshot = prdWorkflowStoredObservationSnapshot(stampedSnapshot, reportSource);
10231
+ const actionChanges = prdWorkflowSnapshotActionChanges(previousClientSnapshot || {}, storedObservationSnapshot);
10232
+ prdWorkflowWriteClientObservation(scopedRoot, tapdId, reportMeta, storedObservationSnapshot);
10233
+ prdWorkflowAppendAudit(scopedRoot, tapdId, {
10234
+ type: "workflow-report-observation-stored",
10235
+ flowSource,
10236
+ flowId,
10237
+ clientId: reportMeta.clientId,
10238
+ userId: reportMeta.userId,
10239
+ observedAt: reportMeta.observedAt,
10240
+ reportedAt: reportMeta.reportedAt,
10241
+ phase: String(storedObservationSnapshot?.phase || ""),
10242
+ pointer: String(storedObservationSnapshot?.pointer || ""),
10243
+ revision: String(storedObservationSnapshot?.revision || ""),
10244
+ actionCount: prdWorkflowSnapshotActionCount(storedObservationSnapshot),
10245
+ truth: "observation",
10246
+ authority: "client",
10247
+ persistence: "runtime",
10248
+ note: "producer observation accepted through the canonical Workflow Report endpoint",
10249
+ });
10250
+ for (const change of actionChanges) {
10251
+ prdWorkflowAppendAudit(scopedRoot, tapdId, {
10252
+ type: "snapshot-action-change",
10253
+ source: "workflow-report",
10254
+ clientId: reportMeta.clientId,
10255
+ userId: reportMeta.userId,
10256
+ observedAt: reportMeta.observedAt,
10257
+ reportedAt: reportMeta.reportedAt,
10258
+ revision: String(storedObservationSnapshot.revision || ""),
10259
+ previousRevision: String(previousClientSnapshot?.revision || ""),
10260
+ pointer: String(storedObservationSnapshot.pointer || ""),
10261
+ previousPointer: String(previousClientSnapshot?.pointer || ""),
10262
+ ...change,
10263
+ });
10264
+ }
10265
+ return { reportMeta, storedObservationSnapshot, previousClientSnapshot };
10266
+ }
10267
+
10174
10268
  function prdWorkflowSnapshotReportConflict(existingRecord, incomingSnapshot, meta) {
10175
10269
  if (!existingRecord?.snapshot || meta.force) return null;
10176
10270
  const current = existingRecord.snapshot;
@@ -10565,6 +10659,13 @@ function prdWorkflowRuntimeEventCanonicalAction(stage = "") {
10565
10659
  : "";
10566
10660
  }
10567
10661
 
10662
+ function prdWorkflowRuntimeEventProducer(event = {}) {
10663
+ return String(event.source || event.producer || "agentflow")
10664
+ .trim()
10665
+ .toLowerCase()
10666
+ .slice(0, 120) || "agentflow";
10667
+ }
10668
+
10568
10669
  function prdWorkflowRuntimeOwnedArtifacts(values, stage = "") {
10569
10670
  if (!Array.isArray(values)) return values;
10570
10671
  const ownsOnlyChangedArtifact = /^(?:issue-plan|implementation|bugfix|integration):/.test(String(stage || ""));
@@ -10588,14 +10689,14 @@ function prdWorkflowRuntimeEventId(event = {}) {
10588
10689
  const aggregateByStage = event.aggregateByStage !== false && event.aggregate_by_stage !== false;
10589
10690
  if ((stage || action) && aggregateByStage) {
10590
10691
  const issue = String(event.issueKey || event.issue_key || event.issue || "").trim();
10591
- const key = [scope, issue, platform, stage || action].filter(Boolean).join(":");
10692
+ const key = [prdWorkflowRuntimeEventProducer(event), scope, issue, platform, stage || action].filter(Boolean).join(":");
10592
10693
  return `stage_${prdWorkflowSafeStateId(key)}`;
10593
10694
  }
10594
10695
  const existing = String(event.id || event.eventId || event.event_id || "").trim();
10595
10696
  if (existing) return existing.slice(0, 160);
10596
10697
  if (stage || action) {
10597
10698
  const issue = String(event.issueKey || event.issue_key || event.issue || "").trim();
10598
- const key = [scope, issue, platform, stage || action].filter(Boolean).join(":");
10699
+ const key = [prdWorkflowRuntimeEventProducer(event), scope, issue, platform, stage || action].filter(Boolean).join(":");
10599
10700
  return `stage_${prdWorkflowSafeStateId(key)}`;
10600
10701
  }
10601
10702
  return `evt_${Date.now().toString(36)}_${crypto.randomBytes(4).toString("hex")}`;
@@ -10659,8 +10760,13 @@ function prdWorkflowNormalizeRuntimeEvent(tapdId, event = {}) {
10659
10760
  entry.stage = stage;
10660
10761
  entry.stageKey = stage;
10661
10762
  }
10662
- entry.artifacts = prdWorkflowRuntimeOwnedArtifacts(entry.artifacts, stage);
10663
- entry.links = prdWorkflowRuntimeOwnedArtifacts(entry.links, stage);
10763
+ const attachProducer = (values) => (Array.isArray(values) ? values.map((item) => (
10764
+ item && typeof item === "object" && !Array.isArray(item)
10765
+ ? { ...item, producer: String(item.producer || source).trim().toLowerCase() || source }
10766
+ : item
10767
+ )) : values);
10768
+ entry.artifacts = attachProducer(prdWorkflowRuntimeOwnedArtifacts(entry.artifacts, stage));
10769
+ entry.links = attachProducer(prdWorkflowRuntimeOwnedArtifacts(entry.links, stage));
10664
10770
  if (scope) entry.scope = scope;
10665
10771
  if (platform) entry.platform = platform;
10666
10772
  if (!entry.createdAt) entry.createdAt = event.startedAt || now;
@@ -10759,8 +10865,12 @@ function prdWorkflowAppendRuntimeEvent(scopedRoot, tapdId, event = {}) {
10759
10865
  const current = prdWorkflowReadRuntimeEvents(scopedRoot, tapdId);
10760
10866
  const entry = prdWorkflowNormalizeRuntimeEvent(tapdId, event);
10761
10867
  const entryIdem = String(entry.idempotencyKey || "").trim();
10868
+ const entryProducer = prdWorkflowRuntimeEventProducer(entry);
10869
+ const entryDedupeKey = prdWorkflowRuntimeEventDedupeKey(entry);
10762
10870
  const index = current.events.findIndex((item) => {
10871
+ if (prdWorkflowRuntimeEventProducer(item) !== entryProducer) return false;
10763
10872
  if (String(item?.id || "") === entry.id) return true;
10873
+ if (prdWorkflowRuntimeEventDedupeKey(item) === entryDedupeKey) return true;
10764
10874
  if (!entryIdem) return false;
10765
10875
  if (String(item?.idempotencyKey || "").trim() === entryIdem) return true;
10766
10876
  return Array.isArray(item?.idempotencyHistory) && item.idempotencyHistory.includes(entryIdem);
@@ -10871,36 +10981,43 @@ function prdWorkflowAppendRuntimeEvent(scopedRoot, tapdId, event = {}) {
10871
10981
  }
10872
10982
  }
10873
10983
 
10874
- function prdWorkflowFindCompletedIdempotencyEvent(scopedRoot, tapdId, idempotencyKey) {
10984
+ function prdWorkflowFindIdempotencyEvent(scopedRoot, tapdId, idempotencyKey, source = "", completedOnly = true) {
10875
10985
  const key = String(idempotencyKey || "").trim();
10876
10986
  if (!key) return null;
10987
+ const producer = String(source || "").trim().toLowerCase();
10877
10988
  const events = prdWorkflowReadRuntimeEvents(scopedRoot, tapdId).events;
10878
10989
  return [...events].reverse().find((event) => (
10990
+ (!producer || prdWorkflowRuntimeEventProducer(event) === producer) &&
10879
10991
  (String(event?.idempotencyKey || "") === key || (Array.isArray(event?.idempotencyHistory) && event.idempotencyHistory.includes(key))) &&
10880
- ["done", "success", "completed"].includes(String(event?.status || "").toLowerCase())
10992
+ (!completedOnly || ["done", "success", "completed"].includes(String(event?.status || "").toLowerCase()))
10881
10993
  )) || null;
10882
10994
  }
10883
10995
 
10996
+ function prdWorkflowFindCompletedIdempotencyEvent(scopedRoot, tapdId, idempotencyKey, source = "") {
10997
+ return prdWorkflowFindIdempotencyEvent(scopedRoot, tapdId, idempotencyKey, source, true);
10998
+ }
10999
+
10884
11000
  function prdWorkflowRuntimeEventDedupeKey(event = {}, index = 0) {
11001
+ const producer = prdWorkflowRuntimeEventProducer(event);
10885
11002
  const stage = prdWorkflowRuntimeEventCanonicalStage(event);
10886
11003
  const aggregateByStage = event.aggregateByStage !== false && event.aggregate_by_stage !== false;
10887
11004
  if (stage) {
10888
11005
  const issue = event?.issueKey || event?.issue_key || event?.issue;
10889
11006
  const platform = event?.platform;
10890
11007
  if (aggregateByStage || issue || platform) {
10891
- return ["stage", event?.scope, issue, platform, stage]
11008
+ return ["producer", producer, "stage", event?.scope, issue, platform, stage]
10892
11009
  .map((value) => String(value || "").trim())
10893
11010
  .join(":");
10894
11011
  }
10895
11012
  }
10896
11013
  const id = String(event?.id || event?.eventId || event?.event_id || "").trim();
10897
- if (id) return `id:${id}`;
11014
+ if (id) return `producer:${producer}:id:${id}`;
10898
11015
  if (stage) {
10899
- return ["stage", event?.scope, event?.issueKey || event?.issue_key || event?.issue, event?.platform, stage]
11016
+ return ["producer", producer, "stage", event?.scope, event?.issueKey || event?.issue_key || event?.issue, event?.platform, stage]
10900
11017
  .map((value) => String(value || "").trim())
10901
11018
  .join(":");
10902
11019
  }
10903
- return `idx:${index}`;
11020
+ return `producer:${producer}:idx:${index}`;
10904
11021
  }
10905
11022
 
10906
11023
  function prdWorkflowMergeRuntimeEventList(snapshotEvents = [], runtimeEvents = []) {
@@ -11169,14 +11286,24 @@ function prdWorkflowMergeRuntimeEvents(scopedRoot, tapdId, snapshot) {
11169
11286
  const globalState = prdWorkflowGlobalStateFromEvents(tapdId, snapshot, runtimeEvents);
11170
11287
  const artifacts = mergeWorkflowArtifacts(snapshot?.artifacts, runtimeEvents);
11171
11288
  const projections = materializeWorkflowProjections(snapshot, runtimeEvents);
11289
+ const extensions = materializeWorkflowExtensions(snapshot, runtimeEvents);
11290
+ const prdFlowExtension = extensions["prd-flow"] && typeof extensions["prd-flow"] === "object"
11291
+ ? extensions["prd-flow"]
11292
+ : {};
11293
+ const prdFlowExtensionView = {};
11294
+ for (const key of ["issues", "issueGroups", "issue_groups", "epics", "epicGroups", "epic_groups", "aiDocs", "ai_docs"]) {
11295
+ if (Object.prototype.hasOwnProperty.call(prdFlowExtension, key)) prdFlowExtensionView[key] = prdFlowExtension[key];
11296
+ }
11172
11297
  return {
11173
11298
  ...snapshot,
11299
+ ...prdFlowExtensionView,
11174
11300
  workflow: globalState.workflow,
11175
11301
  overall,
11176
11302
  globalState,
11177
11303
  artifacts,
11178
11304
  projections,
11179
- runtimeRevision: workflowRuntimeRevision(globalState, artifacts, runtimeEvents, projections),
11305
+ extensions,
11306
+ runtimeRevision: workflowRuntimeRevision(globalState, artifacts, runtimeEvents, projections, extensions),
11180
11307
  runtimeEvents,
11181
11308
  events,
11182
11309
  sources: {
@@ -12955,6 +13082,8 @@ export function startUiServer({
12955
13082
  }
12956
13083
 
12957
13084
  if (req.method === "POST" && url.pathname === "/api/prd-workflow/snapshot") {
13085
+ res.setHeader("Deprecation", "true");
13086
+ res.setHeader("Link", "</api/workflows/report>; rel=\"successor-version\"");
12958
13087
  if (!authUser?.userId) {
12959
13088
  json(res, 401, { error: "Authentication required" });
12960
13089
  return;
@@ -13188,6 +13317,10 @@ export function startUiServer({
13188
13317
  json(res, 200, {
13189
13318
  ok: true,
13190
13319
  snapshot: withDiagnostic,
13320
+ compatibility: {
13321
+ deprecatedEndpoint: "/api/prd-workflow/snapshot",
13322
+ replacement: "/api/workflows/report with observation.state",
13323
+ },
13191
13324
  ...(workflowShare ? { workflowShare, shareUrl: workflowShare.shortUrl || workflowShare.url } : {}),
13192
13325
  });
13193
13326
  } catch (e) {
@@ -13751,6 +13884,24 @@ export function startUiServer({
13751
13884
  String(currentSnapshot.runtimeRevision || "").trim(),
13752
13885
  String(currentSnapshot.revision || "").trim(),
13753
13886
  ].filter(Boolean));
13887
+ if (report.idempotencyKey) {
13888
+ const existing = prdWorkflowFindCompletedIdempotencyEvent(
13889
+ scopedRoot,
13890
+ tapdId,
13891
+ report.idempotencyKey,
13892
+ report.event.source,
13893
+ );
13894
+ if (existing) {
13895
+ json(res, 200, {
13896
+ ok: true,
13897
+ alreadyApplied: true,
13898
+ report,
13899
+ event: existing,
13900
+ snapshot: currentSnapshot,
13901
+ });
13902
+ return;
13903
+ }
13904
+ }
13754
13905
  if (report.expectedRevision && acceptedRevisions.size && !acceptedRevisions.has(report.expectedRevision)) {
13755
13906
  json(res, 409, {
13756
13907
  error: "Workflow state changed; refresh before reporting",
@@ -13764,28 +13915,36 @@ export function startUiServer({
13764
13915
  });
13765
13916
  return;
13766
13917
  }
13767
- if (report.idempotencyKey) {
13768
- const existing = prdWorkflowFindCompletedIdempotencyEvent(scopedRoot, tapdId, report.idempotencyKey);
13769
- if (existing) {
13770
- json(res, 200, {
13771
- ok: true,
13772
- alreadyApplied: true,
13773
- report,
13774
- event: existing,
13775
- snapshot: currentSnapshot,
13776
- });
13777
- return;
13778
- }
13918
+ let observation = null;
13919
+ if (report.observation) {
13920
+ const observationPayload = {
13921
+ ...payload,
13922
+ tapdId,
13923
+ clientId: report.observation.clientId || payload.clientId || payload.source || "workflow-reporter",
13924
+ observedAt: report.observation.observedAt || payload.observedAt || "",
13925
+ scope: report.observation.scope || payload.scope || "client",
13926
+ };
13927
+ observation = prdWorkflowStoreClientObservation({
13928
+ scopedRoot,
13929
+ tapdId,
13930
+ rawState: report.observation.state,
13931
+ payload: observationPayload,
13932
+ req,
13933
+ userCtx,
13934
+ flowSource,
13935
+ flowId,
13936
+ });
13779
13937
  }
13780
- const event = prdWorkflowAppendRuntimeEvent(scopedRoot, tapdId, {
13938
+ const shouldStoreEvent = report.hasRuntimeUpdate || Boolean(report.idempotencyKey);
13939
+ const event = shouldStoreEvent ? prdWorkflowAppendRuntimeEvent(scopedRoot, tapdId, {
13781
13940
  ...report.event,
13782
13941
  tapdId,
13783
13942
  actor: {
13784
13943
  userId: String(userCtx.userId || ""),
13785
13944
  username: String(authUser.username || userCtx.userId || ""),
13786
13945
  },
13787
- });
13788
- if (!event) throw new Error("Failed to store workflow report");
13946
+ }) : null;
13947
+ if (shouldStoreEvent && !event) throw new Error("Failed to store workflow report");
13789
13948
  const snapshot = prdWorkflowWithAgentflowTokenDiagnostic(
13790
13949
  prdWorkflowMaterializeSnapshot(
13791
13950
  workflowScope.executionRoot,
@@ -13798,9 +13957,20 @@ export function startUiServer({
13798
13957
  );
13799
13958
  prdWorkflowBroadcast(
13800
13959
  prdWorkflowKey(userCtx, flowSource, flowId, tapdId),
13801
- { type: "workflow-report", tapdId, workflow: report.workflow, event, snapshot },
13960
+ { type: "workflow-report", tapdId, workflow: report.workflow, event, observation: Boolean(observation), snapshot },
13802
13961
  );
13803
- json(res, 200, { ok: true, report, event, snapshot });
13962
+ json(res, 200, {
13963
+ ok: true,
13964
+ report,
13965
+ event,
13966
+ observation: observation ? {
13967
+ accepted: true,
13968
+ clientId: observation.reportMeta.clientId,
13969
+ observedAt: observation.reportMeta.observedAt,
13970
+ schema: report.observation.schema,
13971
+ } : null,
13972
+ snapshot,
13973
+ });
13804
13974
  } catch (e) {
13805
13975
  json(res, 500, { error: (e && e.message) || String(e) });
13806
13976
  }
@@ -13808,6 +13978,8 @@ export function startUiServer({
13808
13978
  }
13809
13979
 
13810
13980
  if (req.method === "POST" && url.pathname === "/api/prd-workflow/event") {
13981
+ res.setHeader("Deprecation", "true");
13982
+ res.setHeader("Link", "</api/workflows/report>; rel=\"successor-version\"");
13811
13983
  if (!authUser?.userId) {
13812
13984
  json(res, 401, { error: "Authentication required" });
13813
13985
  return;
@@ -13858,14 +14030,26 @@ export function startUiServer({
13858
14030
  getSessionTokenFromRequest(req) || "",
13859
14031
  );
13860
14032
  prdWorkflowBroadcast(prdWorkflowKey(userCtx, flowSource, flowId, tapdId), { type: "runtime-event", tapdId, event, snapshot });
13861
- json(res, 200, { ok: true, event, snapshot });
14033
+ json(res, 200, {
14034
+ ok: true,
14035
+ event,
14036
+ snapshot,
14037
+ compatibility: {
14038
+ deprecatedEndpoint: "/api/prd-workflow/event",
14039
+ replacement: "/api/workflows/report with action/artifacts/extensions",
14040
+ },
14041
+ });
13862
14042
  } catch (e) {
13863
14043
  json(res, 500, { error: (e && e.message) || String(e) });
13864
14044
  }
13865
14045
  return;
13866
14046
  }
13867
14047
 
13868
- if (req.method === "POST" && url.pathname === "/api/prd-workflow/review-link") {
14048
+ if (req.method === "POST" && (
14049
+ url.pathname === "/api/workflow-artifacts/publish" ||
14050
+ url.pathname === "/api/prd-workflow/review-link"
14051
+ )) {
14052
+ const legacyReviewEndpoint = url.pathname === "/api/prd-workflow/review-link";
13869
14053
  if (!authUser?.userId) {
13870
14054
  json(res, 401, { error: "Authentication required" });
13871
14055
  return;
@@ -13878,11 +14062,16 @@ export function startUiServer({
13878
14062
  return;
13879
14063
  }
13880
14064
  try {
13881
- const tapdId = String(payload.tapdId || payload.tapd_id || "").trim();
13882
- if (!tapdId) {
13883
- json(res, 400, { error: "Missing tapdId" });
14065
+ const workflow = normalizeWorkflowReference(payload);
14066
+ if (workflow.error) {
14067
+ json(res, 400, { error: workflow.error });
14068
+ return;
14069
+ }
14070
+ if (workflow.namespace !== "tapd") {
14071
+ json(res, 400, { error: `Unsupported workflow namespace: ${workflow.namespace}` });
13884
14072
  return;
13885
14073
  }
14074
+ const tapdId = workflow.id;
13886
14075
  const flowId = String(payload.flowId || "").trim();
13887
14076
  const flowSource = String(payload.flowSource || "user").trim() || "user";
13888
14077
  const archived = payload.archived === true || payload.flowArchived === true;
@@ -13899,6 +14088,68 @@ export function startUiServer({
13899
14088
  }
13900
14089
  const scopedRoot = workflowScope.stateRoot;
13901
14090
  prdWorkflowMigrateLegacyState(workflowScope.executionRoot, scopedRoot, tapdId);
14091
+ const producer = String(payload.source || "agentflow-cli").trim().toLowerCase() || "agentflow-cli";
14092
+ if (!/^[a-z][a-z0-9._-]{0,119}$/.test(producer)) {
14093
+ json(res, 400, { error: "Invalid workflow report source" });
14094
+ return;
14095
+ }
14096
+ const idempotencyKey = String(
14097
+ payload.idempotencyKey || payload.idempotency_key || "",
14098
+ ).trim();
14099
+ const currentSnapshot = prdWorkflowMaterializeSnapshot(
14100
+ workflowScope.executionRoot,
14101
+ scopedRoot,
14102
+ tapdId,
14103
+ userCtx,
14104
+ { flowSource, flowId },
14105
+ );
14106
+ const expectedRevision = String(payload.expectedRevision || payload.expected_revision || "").trim();
14107
+ const acceptedRevisions = new Set([
14108
+ String(currentSnapshot.runtimeRevision || "").trim(),
14109
+ String(currentSnapshot.revision || "").trim(),
14110
+ ].filter(Boolean));
14111
+ if (idempotencyKey) {
14112
+ const existing = prdWorkflowFindIdempotencyEvent(
14113
+ scopedRoot,
14114
+ tapdId,
14115
+ idempotencyKey,
14116
+ producer,
14117
+ false,
14118
+ );
14119
+ if (existing) {
14120
+ const artifact = Array.isArray(existing.artifacts) ? existing.artifacts[0] : null;
14121
+ json(res, 200, {
14122
+ ok: true,
14123
+ alreadyApplied: true,
14124
+ workflow,
14125
+ artifact,
14126
+ review: artifact ? {
14127
+ id: existing.reviewId || "",
14128
+ url: artifact.canonicalUrl || artifact.url || "",
14129
+ shortUrl: artifact.shortUrl || "",
14130
+ shortCode: existing.reviewShortCode || "",
14131
+ durability: existing.durability || artifact.durability || "",
14132
+ expiresAt: existing.expiresAt || artifact.expiresAt || "",
14133
+ } : null,
14134
+ event: existing,
14135
+ snapshot: currentSnapshot,
14136
+ });
14137
+ return;
14138
+ }
14139
+ }
14140
+ if (expectedRevision && acceptedRevisions.size && !acceptedRevisions.has(expectedRevision)) {
14141
+ json(res, 409, {
14142
+ error: "Workflow state changed; refresh before publishing",
14143
+ conflict: {
14144
+ type: "workflow-revision-conflict",
14145
+ expectedRevision,
14146
+ currentRevision: currentSnapshot.runtimeRevision || currentSnapshot.revision || "",
14147
+ workflow,
14148
+ },
14149
+ snapshot: currentSnapshot,
14150
+ });
14151
+ return;
14152
+ }
13902
14153
  const review = prdWorkflowCreateReview(
13903
14154
  scopedRoot,
13904
14155
  tapdId,
@@ -13932,9 +14183,6 @@ export function startUiServer({
13932
14183
  const reviewSource = review.source && typeof review.source === "object" && !Array.isArray(review.source)
13933
14184
  ? review.source
13934
14185
  : { kind: durability === "durable" ? "ai-doc" : "local-draft", durability };
13935
- const idempotencyKey = String(
13936
- payload.idempotencyKey || payload.idempotency_key || "",
13937
- ).trim();
13938
14186
  const artifact = {
13939
14187
  key: artifactKey,
13940
14188
  label: payload.artifactLabel || "Markdown Review",
@@ -13950,6 +14198,7 @@ export function startUiServer({
13950
14198
  issueKey: payload.issueKey || payload.issue_key || "",
13951
14199
  platform: payload.platform || "",
13952
14200
  stageKey: reviewStageKey,
14201
+ producer,
13953
14202
  ...(reviewMrUrl ? { mrUrl: reviewMrUrl } : {}),
13954
14203
  ...(reviewMrIid ? { mrIid: reviewMrIid } : {}),
13955
14204
  ...(reviewCommitSha ? { commitSha: reviewCommitSha } : {}),
@@ -13957,6 +14206,7 @@ export function startUiServer({
13957
14206
  const event = prdWorkflowAppendRuntimeEvent(scopedRoot, tapdId, {
13958
14207
  id: `review-link:${artifactKey}`,
13959
14208
  type: "review-link",
14209
+ source: producer,
13960
14210
  auxiliary: true,
13961
14211
  aggregateByStage: false,
13962
14212
  conflictOnArtifact: false,
@@ -13988,6 +14238,7 @@ export function startUiServer({
13988
14238
  persistence: "runtime",
13989
14239
  durability,
13990
14240
  source: reviewSource,
14241
+ producer,
13991
14242
  expiresAt: review.expiresAt || "",
13992
14243
  issueKey: artifact.issueKey,
13993
14244
  platform: artifact.platform,
@@ -14004,8 +14255,14 @@ export function startUiServer({
14004
14255
  getSessionTokenFromRequest(req) || "",
14005
14256
  );
14006
14257
  prdWorkflowBroadcast(prdWorkflowKey(userCtx, flowSource, flowId, tapdId), { type: "review-link", tapdId, event, snapshot });
14258
+ if (legacyReviewEndpoint) {
14259
+ res.setHeader("Deprecation", "true");
14260
+ res.setHeader("Link", "</api/workflow-artifacts/publish>; rel=\"successor-version\"");
14261
+ }
14007
14262
  json(res, 200, {
14008
14263
  ok: true,
14264
+ workflow,
14265
+ artifact,
14009
14266
  review: {
14010
14267
  ...review,
14011
14268
  url: reviewUrl,
@@ -14014,6 +14271,12 @@ export function startUiServer({
14014
14271
  },
14015
14272
  event,
14016
14273
  snapshot,
14274
+ ...(legacyReviewEndpoint ? {
14275
+ compatibility: {
14276
+ deprecatedEndpoint: "/api/prd-workflow/review-link",
14277
+ replacement: "/api/workflow-artifacts/publish",
14278
+ },
14279
+ } : {}),
14017
14280
  });
14018
14281
  } catch (e) {
14019
14282
  json(res, 500, { error: (e && e.message) || String(e) });