@fieldwangai/agentflow 0.1.134 → 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,7 +176,9 @@ import {
176
176
  } from "./teams.mjs";
177
177
  import {
178
178
  legacyOverallToGlobalState,
179
+ materializeWorkflowExtensions,
179
180
  materializeWorkflowGlobalState,
181
+ materializeWorkflowProjections,
180
182
  mergeWorkflowArtifactLists,
181
183
  mergeWorkflowArtifacts,
182
184
  mergeWorkflowGlobalState,
@@ -3464,6 +3466,23 @@ function prdWorkflowDashboardSummary(record, snapshot = {}, userCtx = {}) {
3464
3466
  || snapshot?.title
3465
3467
  || "",
3466
3468
  ).trim();
3469
+ const timeline = Array.isArray(snapshot?.projections?.timeline)
3470
+ ? snapshot.projections.timeline
3471
+ .filter((entry) => entry && typeof entry === "object" && !Array.isArray(entry))
3472
+ .map((entry) => ({
3473
+ key: String(entry.key || [entry.source, entry.kind, entry.id].filter(Boolean).join(":")),
3474
+ kind: String(entry.kind || ""),
3475
+ id: String(entry.id || ""),
3476
+ title: String(entry.title || entry.label || entry.id || ""),
3477
+ date: String(entry.date || ""),
3478
+ source: String(entry.source || ""),
3479
+ dimensions: entry.dimensions && typeof entry.dimensions === "object" && !Array.isArray(entry.dimensions)
3480
+ ? entry.dimensions
3481
+ : {},
3482
+ order: Number.isFinite(Number(entry.order)) ? Number(entry.order) : 0,
3483
+ }))
3484
+ .filter((entry) => entry.kind && entry.id)
3485
+ : [];
3467
3486
  const updatedAtTimestamp = Math.max(
3468
3487
  prdWorkflowDashboardTimestamp(record),
3469
3488
  prdWorkflowDashboardTimestamp(snapshot),
@@ -3481,6 +3500,7 @@ function prdWorkflowDashboardSummary(record, snapshot = {}, userCtx = {}) {
3481
3500
  platforms,
3482
3501
  actionCount: actions.length,
3483
3502
  completedActionCount: completedActions,
3503
+ timeline,
3484
3504
  latestAction: latestAction
3485
3505
  ? {
3486
3506
  title: String(latestAction.title || latestAction.label || latestAction.action || latestAction.id || "").trim(),
@@ -3500,6 +3520,72 @@ function prdWorkflowDashboardSummary(record, snapshot = {}, userCtx = {}) {
3500
3520
  };
3501
3521
  }
3502
3522
 
3523
+ function prdWorkflowDashboardTimeline(workflows = []) {
3524
+ const buckets = new Map();
3525
+ const assignedWorkflowIds = new Set();
3526
+ const rows = [...(Array.isArray(workflows) ? workflows : [])].sort((left, right) => {
3527
+ const leftAt = Date.parse(String(left?.updatedAt || ""));
3528
+ const rightAt = Date.parse(String(right?.updatedAt || ""));
3529
+ if (Number.isFinite(leftAt) && Number.isFinite(rightAt)) return leftAt - rightAt;
3530
+ if (Number.isFinite(leftAt) !== Number.isFinite(rightAt)) return Number.isFinite(leftAt) ? 1 : -1;
3531
+ return String(left?.id || left?.tapdId || "").localeCompare(String(right?.id || right?.tapdId || ""));
3532
+ });
3533
+ for (const workflow of rows) {
3534
+ const workflowId = String(workflow?.id || workflow?.tapdId || "");
3535
+ const seen = new Set();
3536
+ for (const entry of Array.isArray(workflow?.timeline) ? workflow.timeline : []) {
3537
+ const key = String(entry?.key || [entry?.source, entry?.kind, entry?.id].filter(Boolean).join(":"));
3538
+ if (!key || seen.has(key)) continue;
3539
+ seen.add(key);
3540
+ assignedWorkflowIds.add(workflowId);
3541
+ const current = buckets.get(key) || {
3542
+ key,
3543
+ kind: String(entry.kind || ""),
3544
+ id: String(entry.id || ""),
3545
+ title: String(entry.title || entry.id || ""),
3546
+ date: String(entry.date || ""),
3547
+ source: String(entry.source || ""),
3548
+ dimensions: entry.dimensions && typeof entry.dimensions === "object" && !Array.isArray(entry.dimensions)
3549
+ ? entry.dimensions
3550
+ : {},
3551
+ order: Number.isFinite(Number(entry.order)) ? Number(entry.order) : 0,
3552
+ workflowCount: 0,
3553
+ completedCount: 0,
3554
+ blockedCount: 0,
3555
+ workflowIds: [],
3556
+ };
3557
+ current.kind = String(entry.kind || current.kind);
3558
+ current.id = String(entry.id || current.id);
3559
+ current.title = String(entry.title || current.title);
3560
+ current.date = String(entry.date || current.date);
3561
+ current.source = String(entry.source || current.source);
3562
+ current.dimensions = entry.dimensions && typeof entry.dimensions === "object" && !Array.isArray(entry.dimensions)
3563
+ ? entry.dimensions
3564
+ : current.dimensions;
3565
+ current.order = Number.isFinite(Number(entry.order)) ? Number(entry.order) : current.order;
3566
+ current.workflowCount += 1;
3567
+ if (workflow?.state === "completed") current.completedCount += 1;
3568
+ if (workflow?.state === "blocked") current.blockedCount += 1;
3569
+ current.workflowIds.push(workflowId);
3570
+ buckets.set(key, current);
3571
+ }
3572
+ }
3573
+ const timeline = Array.from(buckets.values()).sort((left, right) => {
3574
+ const leftAt = Date.parse(String(left.date || ""));
3575
+ const rightAt = Date.parse(String(right.date || ""));
3576
+ const leftValid = Number.isFinite(leftAt);
3577
+ const rightValid = Number.isFinite(rightAt);
3578
+ if (leftValid && rightValid && leftAt !== rightAt) return leftAt - rightAt;
3579
+ if (leftValid !== rightValid) return leftValid ? -1 : 1;
3580
+ if (left.order !== right.order) return left.order - right.order;
3581
+ return left.title.localeCompare(right.title, undefined, { numeric: true, sensitivity: "base" });
3582
+ });
3583
+ return {
3584
+ timeline,
3585
+ unassignedCount: rows.filter((workflow) => !assignedWorkflowIds.has(String(workflow?.id || workflow?.tapdId || ""))).length,
3586
+ };
3587
+ }
3588
+
3503
3589
  function workspaceConversationsPath(scopedRoot) {
3504
3590
  return path.join(path.resolve(scopedRoot), ".workspace", "agentflow", "conversations.json");
3505
3591
  }
@@ -7912,7 +7998,17 @@ function prdWorkflowSafeStateId(value) {
7912
7998
  }
7913
7999
 
7914
8000
  function prdWorkflowReviewIdFromRequest(tapdId, payload = {}, durability = "temporary") {
7915
- 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
+ }
7916
8012
  const requested = String(payload.reviewId || payload.review_id || "").trim();
7917
8013
  if (!requested) return `review_${Date.now().toString(36)}_${crypto.randomBytes(4).toString("hex")}`;
7918
8014
  const safeRequested = prdWorkflowSafeStateId(requested);
@@ -10086,6 +10182,89 @@ function prdWorkflowSnapshotMetaFromReport(payload = {}, rawSnapshot = {}, req =
10086
10182
  };
10087
10183
  }
10088
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
+
10089
10268
  function prdWorkflowSnapshotReportConflict(existingRecord, incomingSnapshot, meta) {
10090
10269
  if (!existingRecord?.snapshot || meta.force) return null;
10091
10270
  const current = existingRecord.snapshot;
@@ -10480,6 +10659,13 @@ function prdWorkflowRuntimeEventCanonicalAction(stage = "") {
10480
10659
  : "";
10481
10660
  }
10482
10661
 
10662
+ function prdWorkflowRuntimeEventProducer(event = {}) {
10663
+ return String(event.source || event.producer || "agentflow")
10664
+ .trim()
10665
+ .toLowerCase()
10666
+ .slice(0, 120) || "agentflow";
10667
+ }
10668
+
10483
10669
  function prdWorkflowRuntimeOwnedArtifacts(values, stage = "") {
10484
10670
  if (!Array.isArray(values)) return values;
10485
10671
  const ownsOnlyChangedArtifact = /^(?:issue-plan|implementation|bugfix|integration):/.test(String(stage || ""));
@@ -10503,14 +10689,14 @@ function prdWorkflowRuntimeEventId(event = {}) {
10503
10689
  const aggregateByStage = event.aggregateByStage !== false && event.aggregate_by_stage !== false;
10504
10690
  if ((stage || action) && aggregateByStage) {
10505
10691
  const issue = String(event.issueKey || event.issue_key || event.issue || "").trim();
10506
- const key = [scope, issue, platform, stage || action].filter(Boolean).join(":");
10692
+ const key = [prdWorkflowRuntimeEventProducer(event), scope, issue, platform, stage || action].filter(Boolean).join(":");
10507
10693
  return `stage_${prdWorkflowSafeStateId(key)}`;
10508
10694
  }
10509
10695
  const existing = String(event.id || event.eventId || event.event_id || "").trim();
10510
10696
  if (existing) return existing.slice(0, 160);
10511
10697
  if (stage || action) {
10512
10698
  const issue = String(event.issueKey || event.issue_key || event.issue || "").trim();
10513
- const key = [scope, issue, platform, stage || action].filter(Boolean).join(":");
10699
+ const key = [prdWorkflowRuntimeEventProducer(event), scope, issue, platform, stage || action].filter(Boolean).join(":");
10514
10700
  return `stage_${prdWorkflowSafeStateId(key)}`;
10515
10701
  }
10516
10702
  return `evt_${Date.now().toString(36)}_${crypto.randomBytes(4).toString("hex")}`;
@@ -10574,8 +10760,13 @@ function prdWorkflowNormalizeRuntimeEvent(tapdId, event = {}) {
10574
10760
  entry.stage = stage;
10575
10761
  entry.stageKey = stage;
10576
10762
  }
10577
- entry.artifacts = prdWorkflowRuntimeOwnedArtifacts(entry.artifacts, stage);
10578
- 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));
10579
10770
  if (scope) entry.scope = scope;
10580
10771
  if (platform) entry.platform = platform;
10581
10772
  if (!entry.createdAt) entry.createdAt = event.startedAt || now;
@@ -10674,8 +10865,12 @@ function prdWorkflowAppendRuntimeEvent(scopedRoot, tapdId, event = {}) {
10674
10865
  const current = prdWorkflowReadRuntimeEvents(scopedRoot, tapdId);
10675
10866
  const entry = prdWorkflowNormalizeRuntimeEvent(tapdId, event);
10676
10867
  const entryIdem = String(entry.idempotencyKey || "").trim();
10868
+ const entryProducer = prdWorkflowRuntimeEventProducer(entry);
10869
+ const entryDedupeKey = prdWorkflowRuntimeEventDedupeKey(entry);
10677
10870
  const index = current.events.findIndex((item) => {
10871
+ if (prdWorkflowRuntimeEventProducer(item) !== entryProducer) return false;
10678
10872
  if (String(item?.id || "") === entry.id) return true;
10873
+ if (prdWorkflowRuntimeEventDedupeKey(item) === entryDedupeKey) return true;
10679
10874
  if (!entryIdem) return false;
10680
10875
  if (String(item?.idempotencyKey || "").trim() === entryIdem) return true;
10681
10876
  return Array.isArray(item?.idempotencyHistory) && item.idempotencyHistory.includes(entryIdem);
@@ -10786,36 +10981,43 @@ function prdWorkflowAppendRuntimeEvent(scopedRoot, tapdId, event = {}) {
10786
10981
  }
10787
10982
  }
10788
10983
 
10789
- function prdWorkflowFindCompletedIdempotencyEvent(scopedRoot, tapdId, idempotencyKey) {
10984
+ function prdWorkflowFindIdempotencyEvent(scopedRoot, tapdId, idempotencyKey, source = "", completedOnly = true) {
10790
10985
  const key = String(idempotencyKey || "").trim();
10791
10986
  if (!key) return null;
10987
+ const producer = String(source || "").trim().toLowerCase();
10792
10988
  const events = prdWorkflowReadRuntimeEvents(scopedRoot, tapdId).events;
10793
10989
  return [...events].reverse().find((event) => (
10990
+ (!producer || prdWorkflowRuntimeEventProducer(event) === producer) &&
10794
10991
  (String(event?.idempotencyKey || "") === key || (Array.isArray(event?.idempotencyHistory) && event.idempotencyHistory.includes(key))) &&
10795
- ["done", "success", "completed"].includes(String(event?.status || "").toLowerCase())
10992
+ (!completedOnly || ["done", "success", "completed"].includes(String(event?.status || "").toLowerCase()))
10796
10993
  )) || null;
10797
10994
  }
10798
10995
 
10996
+ function prdWorkflowFindCompletedIdempotencyEvent(scopedRoot, tapdId, idempotencyKey, source = "") {
10997
+ return prdWorkflowFindIdempotencyEvent(scopedRoot, tapdId, idempotencyKey, source, true);
10998
+ }
10999
+
10799
11000
  function prdWorkflowRuntimeEventDedupeKey(event = {}, index = 0) {
11001
+ const producer = prdWorkflowRuntimeEventProducer(event);
10800
11002
  const stage = prdWorkflowRuntimeEventCanonicalStage(event);
10801
11003
  const aggregateByStage = event.aggregateByStage !== false && event.aggregate_by_stage !== false;
10802
11004
  if (stage) {
10803
11005
  const issue = event?.issueKey || event?.issue_key || event?.issue;
10804
11006
  const platform = event?.platform;
10805
11007
  if (aggregateByStage || issue || platform) {
10806
- return ["stage", event?.scope, issue, platform, stage]
11008
+ return ["producer", producer, "stage", event?.scope, issue, platform, stage]
10807
11009
  .map((value) => String(value || "").trim())
10808
11010
  .join(":");
10809
11011
  }
10810
11012
  }
10811
11013
  const id = String(event?.id || event?.eventId || event?.event_id || "").trim();
10812
- if (id) return `id:${id}`;
11014
+ if (id) return `producer:${producer}:id:${id}`;
10813
11015
  if (stage) {
10814
- 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]
10815
11017
  .map((value) => String(value || "").trim())
10816
11018
  .join(":");
10817
11019
  }
10818
- return `idx:${index}`;
11020
+ return `producer:${producer}:idx:${index}`;
10819
11021
  }
10820
11022
 
10821
11023
  function prdWorkflowMergeRuntimeEventList(snapshotEvents = [], runtimeEvents = []) {
@@ -11083,13 +11285,25 @@ function prdWorkflowMergeRuntimeEvents(scopedRoot, tapdId, snapshot) {
11083
11285
  const overall = prdWorkflowOverallFromEvents(tapdId, snapshot, runtimeEvents);
11084
11286
  const globalState = prdWorkflowGlobalStateFromEvents(tapdId, snapshot, runtimeEvents);
11085
11287
  const artifacts = mergeWorkflowArtifacts(snapshot?.artifacts, runtimeEvents);
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
+ }
11086
11297
  return {
11087
11298
  ...snapshot,
11299
+ ...prdFlowExtensionView,
11088
11300
  workflow: globalState.workflow,
11089
11301
  overall,
11090
11302
  globalState,
11091
11303
  artifacts,
11092
- runtimeRevision: workflowRuntimeRevision(globalState, artifacts, runtimeEvents),
11304
+ projections,
11305
+ extensions,
11306
+ runtimeRevision: workflowRuntimeRevision(globalState, artifacts, runtimeEvents, projections, extensions),
11093
11307
  runtimeEvents,
11094
11308
  events,
11095
11309
  sources: {
@@ -12515,7 +12729,7 @@ export function startUiServer({
12515
12729
  ? getTeamById(requestedTeamId)
12516
12730
  : getTeamForUser(userCtx.userId);
12517
12731
  if (!team || team.status !== "active") {
12518
- json(res, 200, { ok: true, view: "team", team: null, workflows: [] });
12732
+ json(res, 200, { ok: true, view: "team", team: null, workflows: [], timeline: [], unassignedCount: 0 });
12519
12733
  return;
12520
12734
  }
12521
12735
  records = listPrdWorkflowCollaborationsForTeam(team.id);
@@ -12529,9 +12743,17 @@ export function startUiServer({
12529
12743
  const latestClient = prdWorkflowLatestClientSnapshot(stateRoot, stateRoot, tapdId);
12530
12744
  const legacy = prdWorkflowReadCachedSnapshot(stateRoot, tapdId);
12531
12745
  const snapshot = project?.snapshot || latestClient || legacy?.snapshot || {};
12532
- return prdWorkflowDashboardSummary(record, snapshot, userCtx);
12746
+ const materialized = prdWorkflowMergeRuntimeEvents(stateRoot, tapdId, snapshot);
12747
+ return prdWorkflowDashboardSummary(record, materialized, userCtx);
12748
+ });
12749
+ const dashboardTimeline = prdWorkflowDashboardTimeline(workflows);
12750
+ json(res, 200, {
12751
+ ok: true,
12752
+ view: view === "team" ? "team" : "personal",
12753
+ team: teamSummaryWithUsers(team),
12754
+ workflows,
12755
+ ...dashboardTimeline,
12533
12756
  });
12534
- json(res, 200, { ok: true, view: view === "team" ? "team" : "personal", team: teamSummaryWithUsers(team), workflows });
12535
12757
  } catch (error) {
12536
12758
  json(res, 500, { error: (error && error.message) || String(error) });
12537
12759
  }
@@ -12860,6 +13082,8 @@ export function startUiServer({
12860
13082
  }
12861
13083
 
12862
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\"");
12863
13087
  if (!authUser?.userId) {
12864
13088
  json(res, 401, { error: "Authentication required" });
12865
13089
  return;
@@ -13093,6 +13317,10 @@ export function startUiServer({
13093
13317
  json(res, 200, {
13094
13318
  ok: true,
13095
13319
  snapshot: withDiagnostic,
13320
+ compatibility: {
13321
+ deprecatedEndpoint: "/api/prd-workflow/snapshot",
13322
+ replacement: "/api/workflows/report with observation.state",
13323
+ },
13096
13324
  ...(workflowShare ? { workflowShare, shareUrl: workflowShare.shortUrl || workflowShare.url } : {}),
13097
13325
  });
13098
13326
  } catch (e) {
@@ -13636,6 +13864,13 @@ export function startUiServer({
13636
13864
  json(res, workflowScope.status || 400, { error: workflowScope.error });
13637
13865
  return;
13638
13866
  }
13867
+ if (!workflowScope.collaboration) {
13868
+ const ensured = ensurePrdWorkflowCollaboration({ tapdId, userId: userCtx.userId });
13869
+ if (ensured.error) {
13870
+ json(res, ensured.status || 400, { error: ensured.error });
13871
+ return;
13872
+ }
13873
+ }
13639
13874
  const scopedRoot = workflowScope.stateRoot;
13640
13875
  prdWorkflowMigrateLegacyState(workflowScope.executionRoot, scopedRoot, tapdId);
13641
13876
  const currentSnapshot = prdWorkflowMaterializeSnapshot(
@@ -13649,6 +13884,24 @@ export function startUiServer({
13649
13884
  String(currentSnapshot.runtimeRevision || "").trim(),
13650
13885
  String(currentSnapshot.revision || "").trim(),
13651
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
+ }
13652
13905
  if (report.expectedRevision && acceptedRevisions.size && !acceptedRevisions.has(report.expectedRevision)) {
13653
13906
  json(res, 409, {
13654
13907
  error: "Workflow state changed; refresh before reporting",
@@ -13662,28 +13915,36 @@ export function startUiServer({
13662
13915
  });
13663
13916
  return;
13664
13917
  }
13665
- if (report.idempotencyKey) {
13666
- const existing = prdWorkflowFindCompletedIdempotencyEvent(scopedRoot, tapdId, report.idempotencyKey);
13667
- if (existing) {
13668
- json(res, 200, {
13669
- ok: true,
13670
- alreadyApplied: true,
13671
- report,
13672
- event: existing,
13673
- snapshot: currentSnapshot,
13674
- });
13675
- return;
13676
- }
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
+ });
13677
13937
  }
13678
- const event = prdWorkflowAppendRuntimeEvent(scopedRoot, tapdId, {
13938
+ const shouldStoreEvent = report.hasRuntimeUpdate || Boolean(report.idempotencyKey);
13939
+ const event = shouldStoreEvent ? prdWorkflowAppendRuntimeEvent(scopedRoot, tapdId, {
13679
13940
  ...report.event,
13680
13941
  tapdId,
13681
13942
  actor: {
13682
13943
  userId: String(userCtx.userId || ""),
13683
13944
  username: String(authUser.username || userCtx.userId || ""),
13684
13945
  },
13685
- });
13686
- if (!event) throw new Error("Failed to store workflow report");
13946
+ }) : null;
13947
+ if (shouldStoreEvent && !event) throw new Error("Failed to store workflow report");
13687
13948
  const snapshot = prdWorkflowWithAgentflowTokenDiagnostic(
13688
13949
  prdWorkflowMaterializeSnapshot(
13689
13950
  workflowScope.executionRoot,
@@ -13696,9 +13957,20 @@ export function startUiServer({
13696
13957
  );
13697
13958
  prdWorkflowBroadcast(
13698
13959
  prdWorkflowKey(userCtx, flowSource, flowId, tapdId),
13699
- { type: "workflow-report", tapdId, workflow: report.workflow, event, snapshot },
13960
+ { type: "workflow-report", tapdId, workflow: report.workflow, event, observation: Boolean(observation), snapshot },
13700
13961
  );
13701
- 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
+ });
13702
13974
  } catch (e) {
13703
13975
  json(res, 500, { error: (e && e.message) || String(e) });
13704
13976
  }
@@ -13706,6 +13978,8 @@ export function startUiServer({
13706
13978
  }
13707
13979
 
13708
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\"");
13709
13983
  if (!authUser?.userId) {
13710
13984
  json(res, 401, { error: "Authentication required" });
13711
13985
  return;
@@ -13756,14 +14030,26 @@ export function startUiServer({
13756
14030
  getSessionTokenFromRequest(req) || "",
13757
14031
  );
13758
14032
  prdWorkflowBroadcast(prdWorkflowKey(userCtx, flowSource, flowId, tapdId), { type: "runtime-event", tapdId, event, snapshot });
13759
- 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
+ });
13760
14042
  } catch (e) {
13761
14043
  json(res, 500, { error: (e && e.message) || String(e) });
13762
14044
  }
13763
14045
  return;
13764
14046
  }
13765
14047
 
13766
- 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";
13767
14053
  if (!authUser?.userId) {
13768
14054
  json(res, 401, { error: "Authentication required" });
13769
14055
  return;
@@ -13776,11 +14062,16 @@ export function startUiServer({
13776
14062
  return;
13777
14063
  }
13778
14064
  try {
13779
- const tapdId = String(payload.tapdId || payload.tapd_id || "").trim();
13780
- if (!tapdId) {
13781
- 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}` });
13782
14072
  return;
13783
14073
  }
14074
+ const tapdId = workflow.id;
13784
14075
  const flowId = String(payload.flowId || "").trim();
13785
14076
  const flowSource = String(payload.flowSource || "user").trim() || "user";
13786
14077
  const archived = payload.archived === true || payload.flowArchived === true;
@@ -13797,6 +14088,68 @@ export function startUiServer({
13797
14088
  }
13798
14089
  const scopedRoot = workflowScope.stateRoot;
13799
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
+ }
13800
14153
  const review = prdWorkflowCreateReview(
13801
14154
  scopedRoot,
13802
14155
  tapdId,
@@ -13830,9 +14183,6 @@ export function startUiServer({
13830
14183
  const reviewSource = review.source && typeof review.source === "object" && !Array.isArray(review.source)
13831
14184
  ? review.source
13832
14185
  : { kind: durability === "durable" ? "ai-doc" : "local-draft", durability };
13833
- const idempotencyKey = String(
13834
- payload.idempotencyKey || payload.idempotency_key || "",
13835
- ).trim();
13836
14186
  const artifact = {
13837
14187
  key: artifactKey,
13838
14188
  label: payload.artifactLabel || "Markdown Review",
@@ -13848,6 +14198,7 @@ export function startUiServer({
13848
14198
  issueKey: payload.issueKey || payload.issue_key || "",
13849
14199
  platform: payload.platform || "",
13850
14200
  stageKey: reviewStageKey,
14201
+ producer,
13851
14202
  ...(reviewMrUrl ? { mrUrl: reviewMrUrl } : {}),
13852
14203
  ...(reviewMrIid ? { mrIid: reviewMrIid } : {}),
13853
14204
  ...(reviewCommitSha ? { commitSha: reviewCommitSha } : {}),
@@ -13855,6 +14206,7 @@ export function startUiServer({
13855
14206
  const event = prdWorkflowAppendRuntimeEvent(scopedRoot, tapdId, {
13856
14207
  id: `review-link:${artifactKey}`,
13857
14208
  type: "review-link",
14209
+ source: producer,
13858
14210
  auxiliary: true,
13859
14211
  aggregateByStage: false,
13860
14212
  conflictOnArtifact: false,
@@ -13886,6 +14238,7 @@ export function startUiServer({
13886
14238
  persistence: "runtime",
13887
14239
  durability,
13888
14240
  source: reviewSource,
14241
+ producer,
13889
14242
  expiresAt: review.expiresAt || "",
13890
14243
  issueKey: artifact.issueKey,
13891
14244
  platform: artifact.platform,
@@ -13902,8 +14255,14 @@ export function startUiServer({
13902
14255
  getSessionTokenFromRequest(req) || "",
13903
14256
  );
13904
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
+ }
13905
14262
  json(res, 200, {
13906
14263
  ok: true,
14264
+ workflow,
14265
+ artifact,
13907
14266
  review: {
13908
14267
  ...review,
13909
14268
  url: reviewUrl,
@@ -13912,6 +14271,12 @@ export function startUiServer({
13912
14271
  },
13913
14272
  event,
13914
14273
  snapshot,
14274
+ ...(legacyReviewEndpoint ? {
14275
+ compatibility: {
14276
+ deprecatedEndpoint: "/api/prd-workflow/review-link",
14277
+ replacement: "/api/workflow-artifacts/publish",
14278
+ },
14279
+ } : {}),
13915
14280
  });
13916
14281
  } catch (e) {
13917
14282
  json(res, 500, { error: (e && e.message) || String(e) });