@fieldwangai/agentflow 0.1.138 → 0.1.142

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.
@@ -83,7 +83,7 @@ import {
83
83
  } from "./composer-log.mjs";
84
84
  import { runNodeScript } from "./pipeline-scripts.mjs";
85
85
  import { computeNextRunAt, readFlowSchedule, writeFlowSchedule } from "./schedule-config.mjs";
86
- import { listScheduleStatuses } from "./scheduler.mjs";
86
+ import { cancelScheduledRun, listScheduleStatuses } from "./scheduler.mjs";
87
87
  import {
88
88
  mergeWorkspaceGraphs,
89
89
  workspaceDesignRevision,
@@ -153,6 +153,7 @@ import {
153
153
  } from "./workspace-collaboration.mjs";
154
154
  import {
155
155
  addPrdWorkflowCollaborationMember,
156
+ bindPrdWorkflowProject,
156
157
  ensurePrdWorkflowCollaboration,
157
158
  getPrdWorkflowCollaborationById,
158
159
  getPrdWorkflowCollaborationByShareToken,
@@ -161,12 +162,14 @@ import {
161
162
  ensurePrdWorkflowShareLink,
162
163
  listPrdWorkflowCollaborationsForUser,
163
164
  listPrdWorkflowCollaborationsForTeam,
165
+ listPrdWorkflowProjectBindings,
164
166
  prdWorkflowCollaborationAccess,
165
167
  prdWorkflowCollaborationSummary,
166
168
  removePrdWorkflowCollaborationMember,
167
169
  revokePrdWorkflowShareLink,
168
170
  setPrdWorkflowKnowledgeBindings,
169
171
  syncPrdWorkflowAuthority,
172
+ unbindPrdWorkflowProject,
170
173
  } from "./prd-workflow-collaboration.mjs";
171
174
  import {
172
175
  createTeam,
@@ -185,6 +188,8 @@ import {
185
188
  mergeWorkflowArtifactLists,
186
189
  mergeWorkflowArtifacts,
187
190
  mergeWorkflowGlobalState,
191
+ isSafeWorkflowUrl,
192
+ normalizeWorkflowChecklistItemStatus,
188
193
  normalizeWorkflowReference,
189
194
  normalizeWorkflowReport,
190
195
  removeWorkflowGlobalStatePath,
@@ -3592,6 +3597,95 @@ function prdWorkflowShareLinkSummary(record, shareToken, publicBaseUrl, userId =
3592
3597
  };
3593
3598
  }
3594
3599
 
3600
+ function listAccessibleProjectFlows(root, userCtx = {}) {
3601
+ const flows = listFlowsJson(root, { ...userCtx, includeWorkspaceFlows: true })
3602
+ .filter((flow) => (
3603
+ !workspaceFlowCollaborationGuard(
3604
+ flow.id,
3605
+ flow.source || "user",
3606
+ flow.archived === true,
3607
+ userCtx,
3608
+ "read",
3609
+ )
3610
+ ))
3611
+ .map((flow) => {
3612
+ const source = flow.source || "user";
3613
+ const collaboration = source === "workspace"
3614
+ ? getWorkspaceCollaborationByFlow(flow.id, flow.archived === true)
3615
+ : getWorkspaceCollaborationForProject({
3616
+ flowId: flow.id,
3617
+ flowSource: source,
3618
+ archived: flow.archived === true,
3619
+ ownerId: userCtx.userId,
3620
+ });
3621
+ return collaboration
3622
+ ? { ...flow, collaboration: workspaceCollaborationSummaryWithUsers(collaboration, userCtx.userId) }
3623
+ : flow;
3624
+ });
3625
+ const existingCollaborationIds = new Set(flows.map((flow) => flow.collaboration?.id).filter(Boolean));
3626
+ for (const record of listWorkspaceCollaborationsForUser(userCtx.userId)) {
3627
+ const source = record.projectSource || record.flowSource || "workspace";
3628
+ if (source !== "user" || record.ownerId === userCtx.userId) continue;
3629
+ if (existingCollaborationIds.has(record.id)) continue;
3630
+ const ownerFlow = listFlowsJson(root, { userId: record.ownerId })
3631
+ .find((flow) => (
3632
+ flow.id === record.flowId
3633
+ && (flow.source || "user") === "user"
3634
+ && Boolean(flow.archived) === Boolean(record.archived)
3635
+ ));
3636
+ if (!ownerFlow) continue;
3637
+ flows.push({
3638
+ ...ownerFlow,
3639
+ collaboration: workspaceCollaborationSummaryWithUsers(record, userCtx.userId),
3640
+ });
3641
+ existingCollaborationIds.add(record.id);
3642
+ }
3643
+ return flows;
3644
+ }
3645
+
3646
+ function workflowProjectBindingRows(bindings = [], accessibleProjects = [], userCtx = {}) {
3647
+ return (Array.isArray(bindings) ? bindings : []).flatMap((binding) => {
3648
+ const workspaceId = String(binding?.workspaceId || "").trim();
3649
+ if (!workspaceId) return [];
3650
+ const project = accessibleProjects.find((flow) => String(flow?.collaboration?.id || "") === workspaceId);
3651
+ if (!project) return [];
3652
+ const role = String(project.collaboration?.role || "");
3653
+ return [{
3654
+ workspaceId,
3655
+ flowId: String(project.id || binding.flowId || ""),
3656
+ flowSource: String(project.source || binding.flowSource || "user"),
3657
+ archived: project.archived === true,
3658
+ label: String(project.id || binding.flowId || "Project"),
3659
+ description: String(project.description || ""),
3660
+ role: role || ((project.source || "user") === "user" ? "owner" : "editor"),
3661
+ canManage: role === "owner" || role === "editor" || (!role && (project.source || "user") === "user"),
3662
+ boundBy: String(binding.boundBy || ""),
3663
+ boundAt: String(binding.boundAt || ""),
3664
+ }];
3665
+ });
3666
+ }
3667
+
3668
+ function availableWorkflowBindingProjects(accessibleProjects = [], bindings = []) {
3669
+ const bound = new Set((Array.isArray(bindings) ? bindings : []).map((item) => String(item?.workspaceId || "")).filter(Boolean));
3670
+ return accessibleProjects
3671
+ .filter((project) => {
3672
+ const source = String(project?.source || "user");
3673
+ const role = String(project?.collaboration?.role || "");
3674
+ const workspaceId = String(project?.collaboration?.id || "");
3675
+ return !project?.archived
3676
+ && (source === "user" || source === "workspace")
3677
+ && !bound.has(workspaceId)
3678
+ && (!role || role === "owner" || role === "editor");
3679
+ })
3680
+ .map((project) => ({
3681
+ flowId: String(project.id || ""),
3682
+ flowSource: String(project.source || "user"),
3683
+ workspaceId: String(project.collaboration?.id || ""),
3684
+ label: String(project.id || "Project"),
3685
+ description: String(project.description || ""),
3686
+ }));
3687
+ }
3688
+
3595
3689
  function prdWorkflowDashboardActions(snapshot = {}) {
3596
3690
  const rows = new Map();
3597
3691
  for (const field of ["actions", "workflowActions", "workflow_actions", "timeline", "history"]) {
@@ -3639,7 +3733,7 @@ function prdWorkflowDashboardTimestamp(item = {}) {
3639
3733
  return 0;
3640
3734
  }
3641
3735
 
3642
- function prdWorkflowDashboardSummary(record, snapshot = {}, userCtx = {}) {
3736
+ function prdWorkflowDashboardSummary(record, snapshot = {}, userCtx = {}, projectBindings = []) {
3643
3737
  const tapdId = String(record?.tapdId || snapshot?.tapdId || snapshot?.tapd_id || "").trim();
3644
3738
  const collaboration = prdWorkflowCollaborationSummaryWithUsers(record, userCtx?.userId) || {};
3645
3739
  const actions = prdWorkflowDashboardActions(snapshot);
@@ -3669,11 +3763,11 @@ function prdWorkflowDashboardSummary(record, snapshot = {}, userCtx = {}) {
3669
3763
  ? snapshot.overall.requirement
3670
3764
  : {};
3671
3765
  const title = String(
3672
- requirement.title
3766
+ snapshot?.globalState?.title
3767
+ || requirement.title
3673
3768
  || requirement.name
3674
3769
  || snapshot?.prd?.title
3675
3770
  || snapshot?.raw?.prd?.title
3676
- || snapshot?.title
3677
3771
  || "",
3678
3772
  ).trim();
3679
3773
  const timeline = Array.isArray(snapshot?.projections?.timeline)
@@ -3685,6 +3779,8 @@ function prdWorkflowDashboardSummary(record, snapshot = {}, userCtx = {}) {
3685
3779
  id: String(entry.id || ""),
3686
3780
  title: String(entry.title || entry.label || entry.id || ""),
3687
3781
  date: String(entry.date || ""),
3782
+ startDate: String(entry.startDate || entry.start_date || entry.start || ""),
3783
+ endDate: String(entry.endDate || entry.end_date || entry.end || entry.date || ""),
3688
3784
  source: String(entry.source || ""),
3689
3785
  dimensions: entry.dimensions && typeof entry.dimensions === "object" && !Array.isArray(entry.dimensions)
3690
3786
  ? entry.dimensions
@@ -3727,10 +3823,73 @@ function prdWorkflowDashboardSummary(record, snapshot = {}, userCtx = {}) {
3727
3823
  teamName: String(getTeamById(collaboration.teamId)?.name || ""),
3728
3824
  shareActive: collaboration.shareActive === true,
3729
3825
  updatedAt: updatedAtTimestamp ? new Date(updatedAtTimestamp).toISOString() : String(record?.updatedAt || ""),
3826
+ projectBindings,
3730
3827
  };
3731
3828
  }
3732
3829
 
3733
- function prdWorkflowDashboardTimeline(workflows = []) {
3830
+ function prdWorkflowDashboardTimelineDimensionValues(value) {
3831
+ return (Array.isArray(value) ? value : [value])
3832
+ .flatMap((item) => Array.isArray(item) ? item : [item])
3833
+ .map((item) => String(item ?? "").trim())
3834
+ .filter(Boolean);
3835
+ }
3836
+
3837
+ function prdWorkflowDashboardTimelineIdentity(entry = {}) {
3838
+ const id = String(entry?.id || "").trim();
3839
+ const source = String(entry?.source || "").trim().toLowerCase();
3840
+ const kind = String(entry?.kind || "").trim().toLowerCase();
3841
+ const dimensions = entry?.dimensions && typeof entry.dimensions === "object" && !Array.isArray(entry.dimensions)
3842
+ ? entry.dimensions
3843
+ : {};
3844
+ const legacyPlatformIdentity = id.match(/^(android|ios|all)[:_-](.+)$/i);
3845
+ const declaredPlatforms = [
3846
+ ...prdWorkflowDashboardTimelineDimensionValues(dimensions.platform),
3847
+ ...prdWorkflowDashboardTimelineDimensionValues(dimensions.platforms),
3848
+ ].map((value) => value.toLowerCase());
3849
+ if (
3850
+ source === "prd-flow"
3851
+ && ["version", "iteration"].includes(kind)
3852
+ && legacyPlatformIdentity
3853
+ && declaredPlatforms.includes(legacyPlatformIdentity[1].toLowerCase())
3854
+ ) {
3855
+ return legacyPlatformIdentity[2].trim().toLowerCase();
3856
+ }
3857
+ return id.toLowerCase();
3858
+ }
3859
+
3860
+ function prdWorkflowDashboardTimelineGroupKey(entry = {}) {
3861
+ const source = String(entry?.source || "").trim().toLowerCase();
3862
+ const kind = String(entry?.kind || "").trim().toLowerCase();
3863
+ const identity = prdWorkflowDashboardTimelineIdentity(entry);
3864
+ const dimensions = entry?.dimensions && typeof entry.dimensions === "object" && !Array.isArray(entry.dimensions)
3865
+ ? entry.dimensions
3866
+ : {};
3867
+ const nonPlatformDimensions = Object.entries(dimensions)
3868
+ .filter(([key]) => !["platform", "platforms", "client", "clients", "os"].includes(String(key).trim().toLowerCase()))
3869
+ .map(([key, value]) => [
3870
+ String(key).trim().toLowerCase(),
3871
+ prdWorkflowDashboardTimelineDimensionValues(value).map((item) => item.toLowerCase()).sort(),
3872
+ ])
3873
+ .sort(([left], [right]) => left.localeCompare(right));
3874
+ if (identity) return JSON.stringify([source, kind, identity, nonPlatformDimensions]);
3875
+ return String(entry?.key || [entry?.source, entry?.kind, entry?.id].filter(Boolean).join(":"));
3876
+ }
3877
+
3878
+ function prdWorkflowDashboardMergeTimelineDimensions(current = {}, incoming = {}) {
3879
+ const out = {};
3880
+ for (const key of new Set([...Object.keys(current || {}), ...Object.keys(incoming || {})])) {
3881
+ const values = [
3882
+ ...prdWorkflowDashboardTimelineDimensionValues(current?.[key]),
3883
+ ...prdWorkflowDashboardTimelineDimensionValues(incoming?.[key]),
3884
+ ];
3885
+ const unique = Array.from(new Map(values.map((value) => [value.toLowerCase(), value])).values());
3886
+ if (unique.length === 1) out[key] = unique[0];
3887
+ else if (unique.length > 1) out[key] = unique;
3888
+ }
3889
+ return out;
3890
+ }
3891
+
3892
+ export function prdWorkflowDashboardTimeline(workflows = []) {
3734
3893
  const buckets = new Map();
3735
3894
  const assignedWorkflowIds = new Set();
3736
3895
  const rows = [...(Array.isArray(workflows) ? workflows : [])].sort((left, right) => {
@@ -3744,16 +3903,19 @@ function prdWorkflowDashboardTimeline(workflows = []) {
3744
3903
  const workflowId = String(workflow?.id || workflow?.tapdId || "");
3745
3904
  const seen = new Set();
3746
3905
  for (const entry of Array.isArray(workflow?.timeline) ? workflow.timeline : []) {
3747
- const key = String(entry?.key || [entry?.source, entry?.kind, entry?.id].filter(Boolean).join(":"));
3748
- if (!key || seen.has(key)) continue;
3749
- seen.add(key);
3906
+ const memberKey = String(entry?.key || [entry?.source, entry?.kind, entry?.id].filter(Boolean).join(":"));
3907
+ const identity = prdWorkflowDashboardTimelineIdentity(entry);
3908
+ const groupKey = prdWorkflowDashboardTimelineGroupKey(entry);
3909
+ if (!memberKey || !groupKey) continue;
3750
3910
  assignedWorkflowIds.add(workflowId);
3751
- const current = buckets.get(key) || {
3752
- key,
3911
+ const current = buckets.get(groupKey) || {
3912
+ key: memberKey,
3753
3913
  kind: String(entry.kind || ""),
3754
- id: String(entry.id || ""),
3914
+ id: identity || String(entry.id || ""),
3755
3915
  title: String(entry.title || entry.id || ""),
3756
3916
  date: String(entry.date || ""),
3917
+ startDate: String(entry.startDate || ""),
3918
+ endDate: String(entry.endDate || entry.date || ""),
3757
3919
  source: String(entry.source || ""),
3758
3920
  dimensions: entry.dimensions && typeof entry.dimensions === "object" && !Array.isArray(entry.dimensions)
3759
3921
  ? entry.dimensions
@@ -3763,21 +3925,27 @@ function prdWorkflowDashboardTimeline(workflows = []) {
3763
3925
  completedCount: 0,
3764
3926
  blockedCount: 0,
3765
3927
  workflowIds: [],
3928
+ memberKeys: [],
3766
3929
  };
3767
3930
  current.kind = String(entry.kind || current.kind);
3768
- current.id = String(entry.id || current.id);
3769
3931
  current.title = String(entry.title || current.title);
3770
3932
  current.date = String(entry.date || current.date);
3933
+ current.startDate = String(entry.startDate || current.startDate);
3934
+ current.endDate = String(entry.endDate || entry.date || current.endDate);
3771
3935
  current.source = String(entry.source || current.source);
3772
- current.dimensions = entry.dimensions && typeof entry.dimensions === "object" && !Array.isArray(entry.dimensions)
3773
- ? entry.dimensions
3774
- : current.dimensions;
3936
+ current.dimensions = prdWorkflowDashboardMergeTimelineDimensions(current.dimensions, entry.dimensions);
3775
3937
  current.order = Number.isFinite(Number(entry.order)) ? Number(entry.order) : current.order;
3776
- current.workflowCount += 1;
3777
- if (workflow?.state === "completed") current.completedCount += 1;
3778
- if (workflow?.state === "blocked") current.blockedCount += 1;
3779
- current.workflowIds.push(workflowId);
3780
- buckets.set(key, current);
3938
+ if (!current.memberKeys.includes(memberKey)) current.memberKeys.push(memberKey);
3939
+ current.memberKeys.sort();
3940
+ current.key = current.memberKeys[0] || memberKey;
3941
+ if (!seen.has(groupKey)) {
3942
+ seen.add(groupKey);
3943
+ current.workflowCount += 1;
3944
+ if (workflow?.state === "completed") current.completedCount += 1;
3945
+ if (workflow?.state === "blocked") current.blockedCount += 1;
3946
+ if (!current.workflowIds.includes(workflowId)) current.workflowIds.push(workflowId);
3947
+ }
3948
+ buckets.set(groupKey, current);
3781
3949
  }
3782
3950
  }
3783
3951
  const timeline = Array.from(buckets.values()).sort((left, right) => {
@@ -3796,6 +3964,112 @@ function prdWorkflowDashboardTimeline(workflows = []) {
3796
3964
  };
3797
3965
  }
3798
3966
 
3967
+ function prdWorkflowTimelineTimestamp(value, endOfDay = false) {
3968
+ const text = String(value || "").trim();
3969
+ if (!text) return Number.NaN;
3970
+ const timestamp = Date.parse(text);
3971
+ if (!Number.isFinite(timestamp)) return Number.NaN;
3972
+ return endOfDay && /^\d{4}-\d{2}-\d{2}$/.test(text)
3973
+ ? timestamp + (24 * 60 * 60 * 1000) - 1
3974
+ : timestamp;
3975
+ }
3976
+
3977
+ export function prdWorkflowDefaultTimelineKey(timeline = [], now = Date.now()) {
3978
+ const rows = (Array.isArray(timeline) ? timeline : [])
3979
+ .map((entry) => ({
3980
+ entry,
3981
+ startAt: prdWorkflowTimelineTimestamp(entry?.startDate || entry?.start),
3982
+ endAt: prdWorkflowTimelineTimestamp(entry?.endDate || entry?.end || entry?.date, true),
3983
+ }))
3984
+ .filter(({ entry, endAt }) => String(entry?.key || "").trim() && Number.isFinite(endAt));
3985
+ const active = rows
3986
+ .filter(({ startAt, endAt }) => Number.isFinite(startAt) && startAt <= now && now <= endAt)
3987
+ .sort((left, right) => left.endAt - right.endAt);
3988
+ if (active.length > 0) return String(active[0].entry.key);
3989
+ const upcoming = rows
3990
+ .filter(({ endAt }) => endAt >= now)
3991
+ .sort((left, right) => left.endAt - right.endAt);
3992
+ if (upcoming.length > 0) return String(upcoming[0].entry.key);
3993
+ const latestPast = rows.sort((left, right) => right.endAt - left.endAt)[0];
3994
+ return latestPast ? String(latestPast.entry.key) : "all";
3995
+ }
3996
+
3997
+ function prdWorkflowDashboardSearchValues(workflow = {}) {
3998
+ return [
3999
+ workflow.tapdId,
4000
+ workflow.title,
4001
+ workflow.pointer,
4002
+ workflow.phase,
4003
+ workflow.ownerUsername,
4004
+ workflow.latestAction?.title,
4005
+ ...(Array.isArray(workflow.timeline) ? workflow.timeline : []).flatMap((entry) => [
4006
+ entry?.title,
4007
+ entry?.kind,
4008
+ entry?.date,
4009
+ ...Object.values(entry?.dimensions || {}).flatMap((value) => Array.isArray(value) ? value : [value]),
4010
+ ]),
4011
+ ];
4012
+ }
4013
+
4014
+ export function prdWorkflowDashboardPage(workflows = [], dashboardTimeline = {}, options = {}) {
4015
+ const allWorkflows = Array.isArray(workflows) ? workflows : [];
4016
+ const timeline = Array.isArray(dashboardTimeline?.timeline) ? dashboardTimeline.timeline : [];
4017
+ const unassignedCount = Number(dashboardTimeline?.unassignedCount || 0);
4018
+ const defaultTimelineKey = prdWorkflowDefaultTimelineKey(timeline, options.now);
4019
+ const requestedTimelineKey = String(options.timelineKey || "").trim();
4020
+ const matchedTimeline = timeline.find((entry) => (
4021
+ entry.key === requestedTimelineKey
4022
+ || (Array.isArray(entry.memberKeys) && entry.memberKeys.includes(requestedTimelineKey))
4023
+ ));
4024
+ const selectedTimelineKey = requestedTimelineKey === "all"
4025
+ ? "all"
4026
+ : requestedTimelineKey === "unassigned" && unassignedCount > 0
4027
+ ? "unassigned"
4028
+ : matchedTimeline?.key || defaultTimelineKey;
4029
+ const selectedTimeline = timeline.find((entry) => entry.key === selectedTimelineKey) || null;
4030
+ const selectedWorkflowIds = new Set(
4031
+ Array.isArray(selectedTimeline?.workflowIds)
4032
+ ? selectedTimeline.workflowIds.map((value) => String(value))
4033
+ : [],
4034
+ );
4035
+ const query = String(options.query || "").trim().toLowerCase();
4036
+ const scope = ["owned", "collaborating"].includes(options.scope) ? options.scope : "all";
4037
+ const state = ["active", "completed", "blocked"].includes(options.state) ? options.state : "all";
4038
+ const filtered = allWorkflows.filter((workflow) => {
4039
+ const workflowId = String(workflow?.id || workflow?.tapdId || "");
4040
+ if (selectedTimelineKey === "unassigned" && (workflow.timeline || []).length > 0) return false;
4041
+ if (selectedTimeline && !selectedWorkflowIds.has(workflowId)) return false;
4042
+ if (scope === "owned" && workflow.role !== "owner") return false;
4043
+ if (scope === "collaborating" && workflow.role === "owner") return false;
4044
+ if (state !== "all" && workflow.state !== state) return false;
4045
+ if (query && !prdWorkflowDashboardSearchValues(workflow).some(
4046
+ (value) => String(value || "").toLowerCase().includes(query),
4047
+ )) return false;
4048
+ return true;
4049
+ });
4050
+ const requestedPageSize = Number.parseInt(String(options.pageSize || "20"), 10);
4051
+ const pageSize = [20, 50, 100].includes(requestedPageSize) ? requestedPageSize : 20;
4052
+ const total = filtered.length;
4053
+ const totalPages = Math.max(1, Math.ceil(total / pageSize));
4054
+ const requestedPage = Number.parseInt(String(options.page || "1"), 10);
4055
+ const page = Math.min(totalPages, Math.max(1, Number.isFinite(requestedPage) ? requestedPage : 1));
4056
+ const start = (page - 1) * pageSize;
4057
+ return {
4058
+ workflows: filtered.slice(start, start + pageSize),
4059
+ availableCount: allWorkflows.length,
4060
+ selectedTimelineKey,
4061
+ defaultTimelineKey,
4062
+ pagination: {
4063
+ page,
4064
+ pageSize,
4065
+ total,
4066
+ totalPages,
4067
+ hasPrevious: page > 1,
4068
+ hasNext: page < totalPages,
4069
+ },
4070
+ };
4071
+ }
4072
+
3799
4073
  function workspaceConversationsPath(scopedRoot) {
3800
4074
  return path.join(path.resolve(scopedRoot), ".workspace", "agentflow", "conversations.json");
3801
4075
  }
@@ -8025,6 +8299,10 @@ function resolvePrdWorkflowScope(workspaceRoot, params = {}, userCtx = {}, capab
8025
8299
  return { error: "Admin permission required", status: 403 };
8026
8300
  }
8027
8301
  const adminOwner = adminOwnerId ? adminWorkspaceOwnerSummary(adminOwnerId) : null;
8302
+ const adminVersionRepair = capability === "admin-version-repair";
8303
+ if (adminVersionRepair && userCtx.isAdmin !== true) {
8304
+ return { error: "Admin permission required", status: 403 };
8305
+ }
8028
8306
  if (adminOwnerId && !adminOwner) {
8029
8307
  return { error: "Workspace owner not found", status: 404 };
8030
8308
  }
@@ -8040,19 +8318,24 @@ function resolvePrdWorkflowScope(workspaceRoot, params = {}, userCtx = {}, capab
8040
8318
  ? getPrdWorkflowCollaborationForUser(tapdId, userCtx?.userId)
8041
8319
  : null;
8042
8320
  const existingCollaboration = tapdId ? getPrdWorkflowCollaborationByTapdId(tapdId) : null;
8043
- if (!adminOwner && !linkCollaboration && existingCollaboration && !memberCollaboration) {
8321
+ if (!adminOwner && !linkCollaboration && existingCollaboration && !memberCollaboration && !adminVersionRepair) {
8044
8322
  return { error: "PRD Workflow collaboration permission denied", status: 403 };
8045
8323
  }
8046
- const collaboration = adminOwner ? null : (linkCollaboration || memberCollaboration);
8324
+ if (adminVersionRepair && !existingCollaboration) {
8325
+ return { error: "PRD Workflow collaboration not found", status: 404 };
8326
+ }
8327
+ const collaboration = adminOwner ? null : (adminVersionRepair ? existingCollaboration : (linkCollaboration || memberCollaboration));
8047
8328
  const access = adminOwner
8048
8329
  ? { allowed: true, writable: false, role: "admin-viewer", via: "admin-review" }
8330
+ : adminVersionRepair
8331
+ ? { allowed: true, writable: true, role: "admin-version-repair", via: "admin-version-repair" }
8049
8332
  : linkCollaboration
8050
8333
  ? { allowed: true, writable: false, role: "viewer", via: "share-link" }
8051
8334
  : prdWorkflowCollaborationAccess(collaboration, userCtx?.userId);
8052
8335
  if (collaboration && !access.allowed) {
8053
8336
  return { error: "PRD Workflow collaboration permission denied", status: 403 };
8054
8337
  }
8055
- if (capability === "write" && (linkCollaboration || (collaboration && !access.writable))) {
8338
+ if ((capability === "write" || adminVersionRepair) && (linkCollaboration || (collaboration && !access.writable))) {
8056
8339
  return { error: "PRD Workflow collaboration edit permission denied", status: 403 };
8057
8340
  }
8058
8341
  const ownerId = String(adminOwner?.userId || collaboration?.ownerId || userCtx?.userId || "").trim();
@@ -8085,6 +8368,7 @@ function resolvePrdWorkflowScope(workspaceRoot, params = {}, userCtx = {}, capab
8085
8368
  shareToken,
8086
8369
  sharedByLink: Boolean(linkCollaboration),
8087
8370
  adminReadonly: Boolean(adminOwner),
8371
+ adminVersionRepair,
8088
8372
  flowId,
8089
8373
  flowSource,
8090
8374
  archived,
@@ -11392,6 +11676,72 @@ function prdWorkflowMergeProducerTimeline(report, currentSnapshot = {}) {
11392
11676
  };
11393
11677
  }
11394
11678
 
11679
+ function prdWorkflowAdminVersionRepairIntent(payload = {}, report = {}, userCtx = {}) {
11680
+ const operation = String(
11681
+ payload.adminOperation || payload.admin_operation || payload.administrativeOperation || payload.administrative_operation || "",
11682
+ ).trim().toLowerCase();
11683
+ if (!operation) return { requested: false };
11684
+ if (operation !== "repair-version-membership") {
11685
+ return { requested: true, status: 400, error: `Unsupported admin Workflow operation: ${operation}` };
11686
+ }
11687
+ if (userCtx.isAdmin !== true) {
11688
+ return { requested: true, status: 403, error: "Admin permission required" };
11689
+ }
11690
+ const forbiddenKeys = ["action", "artifacts", "observation", "globalState", "global_state", "extensions", "extension"]
11691
+ .filter((key) => Object.prototype.hasOwnProperty.call(payload, key));
11692
+ if (forbiddenKeys.length) {
11693
+ return {
11694
+ requested: true,
11695
+ status: 400,
11696
+ error: `Admin version repair may only update projections.timeline; remove: ${forbiddenKeys.join(", ")}`,
11697
+ };
11698
+ }
11699
+ const projections = payload.projections;
11700
+ if (!projections || typeof projections !== "object" || Array.isArray(projections) || !Array.isArray(projections.timeline)) {
11701
+ return { requested: true, status: 400, error: "Admin version repair requires projections.timeline" };
11702
+ }
11703
+ const extraProjectionKeys = Object.keys(projections).filter((key) => key !== "timeline");
11704
+ if (extraProjectionKeys.length) {
11705
+ return { requested: true, status: 400, error: "Admin version repair may only update projections.timeline" };
11706
+ }
11707
+ const nonVersionEntry = report?.projections?.timeline?.find((item) => String(item?.kind || "").trim().toLowerCase() !== "version");
11708
+ if (nonVersionEntry) {
11709
+ return { requested: true, status: 400, error: "Admin version repair only accepts timeline entries with kind=version" };
11710
+ }
11711
+ if (!report.idempotencyKey) {
11712
+ return { requested: true, status: 400, error: "Admin version repair requires idempotencyKey" };
11713
+ }
11714
+ if (!report.expectedRevision) {
11715
+ return { requested: true, status: 400, error: "Admin version repair requires expectedRevision" };
11716
+ }
11717
+ return { requested: true, operation };
11718
+ }
11719
+
11720
+ function prdWorkflowMergeAdminVersionTimeline(report, currentSnapshot = {}) {
11721
+ const source = prdWorkflowRuntimeEventProducer(report?.event || {});
11722
+ const current = Array.isArray(currentSnapshot?.projections?.timeline) ? currentSnapshot.projections.timeline : [];
11723
+ const incoming = Array.isArray(report?.projections?.timeline) ? report.projections.timeline : [];
11724
+ const retained = current.filter((item) => (
11725
+ prdWorkflowRuntimeEventProducer(item) !== source
11726
+ || String(item?.kind || "").trim().toLowerCase() !== "version"
11727
+ ));
11728
+ const timeline = [...retained, ...incoming];
11729
+ const administrativeRepair = {
11730
+ kind: "version-attribution",
11731
+ operation: "repair-version-membership",
11732
+ };
11733
+ return {
11734
+ ...report,
11735
+ projections: { ...report.projections, timeline },
11736
+ event: {
11737
+ ...report.event,
11738
+ projections: { ...report.event.projections, timeline },
11739
+ administrativeRepair,
11740
+ administrative_repair: administrativeRepair,
11741
+ },
11742
+ };
11743
+ }
11744
+
11395
11745
  function prdWorkflowRuntimeEventDedupeKey(event = {}, index = 0) {
11396
11746
  const producer = prdWorkflowRuntimeEventProducer(event);
11397
11747
  const operation = prdWorkflowRuntimeEventOperation(event);
@@ -11678,6 +12028,136 @@ function prdWorkflowGlobalStateFromEvents(tapdId, snapshot = {}, runtimeEvents =
11678
12028
  return state;
11679
12029
  }
11680
12030
 
12031
+ function prdWorkflowChecklistActionKey(action = {}) {
12032
+ return String(
12033
+ action?.actionModel?.key ||
12034
+ action?.key ||
12035
+ action?.actionKey ||
12036
+ action?.action_key ||
12037
+ action?.action ||
12038
+ action?.actionId ||
12039
+ action?.action_id ||
12040
+ action?.stageKey ||
12041
+ action?.stage_key ||
12042
+ "",
12043
+ ).trim();
12044
+ }
12045
+
12046
+ function prdWorkflowChecklistResourceKey(producer, actionKey, itemKey) {
12047
+ return `checklist:${String(producer || "").trim().toLowerCase()}:${String(actionKey || "").trim()}:${String(itemKey || "").trim()}`;
12048
+ }
12049
+
12050
+ function prdWorkflowChecklistStateEntries(runtimeEvents = []) {
12051
+ const states = new Map();
12052
+ for (const event of Array.isArray(runtimeEvents) ? runtimeEvents : []) {
12053
+ const state = event?.checklistState || event?.checklist_state;
12054
+ if (!state || typeof state !== "object" || Array.isArray(state)) continue;
12055
+ const producer = String(state.producer || state.source || "").trim().toLowerCase();
12056
+ const actionKey = String(state.actionKey || state.action_key || "").trim();
12057
+ const itemKey = String(state.itemKey || state.item_key || "").trim();
12058
+ if (!producer || !actionKey || !itemKey) continue;
12059
+ const resourceKey = prdWorkflowChecklistResourceKey(producer, actionKey, itemKey);
12060
+ const version = workflowSnapshotResourceVersions({ runtimeEvents: [event] })[resourceKey] || "absent";
12061
+ states.set(resourceKey, { ...state, producer, actionKey, itemKey, resourceKey, version });
12062
+ }
12063
+ return states;
12064
+ }
12065
+
12066
+ function prdWorkflowMaterializeChecklists(snapshot = {}, runtimeEvents = []) {
12067
+ const actionSources = new Map();
12068
+ for (const event of Array.isArray(runtimeEvents) ? runtimeEvents : []) {
12069
+ const actionKey = prdWorkflowChecklistActionKey(event);
12070
+ const producer = String(event?.source || event?.producer || "").trim().toLowerCase();
12071
+ if (!actionKey || !producer || (!event?.checklist && !event?.actionModel?.checklist)) continue;
12072
+ const existing = actionSources.get(actionKey);
12073
+ actionSources.set(actionKey, existing && existing !== producer ? "" : producer);
12074
+ }
12075
+ const stateEntries = prdWorkflowChecklistStateEntries(runtimeEvents);
12076
+ const terminalStatuses = new Set(["passed", "skipped"]);
12077
+ const decorate = (action) => {
12078
+ if (!action || typeof action !== "object" || Array.isArray(action)) return action;
12079
+ const definition = action.checklist || action.actionModel?.checklist;
12080
+ if (!definition || typeof definition !== "object" || Array.isArray(definition)) return action;
12081
+ const actionKey = prdWorkflowChecklistActionKey(action);
12082
+ const producer = String(
12083
+ definition.source || action.source || action.producer || action.actionModel?.source || actionSources.get(actionKey) || "",
12084
+ ).trim().toLowerCase();
12085
+ const items = (Array.isArray(definition.items) ? definition.items : []).map((item) => {
12086
+ const itemKey = String(item?.key || item?.id || "").trim();
12087
+ const resourceKey = producer && actionKey && itemKey
12088
+ ? prdWorkflowChecklistResourceKey(producer, actionKey, itemKey)
12089
+ : "";
12090
+ const stored = resourceKey ? stateEntries.get(resourceKey) : null;
12091
+ const state = stored || {
12092
+ producer,
12093
+ actionKey,
12094
+ itemKey,
12095
+ status: "pending",
12096
+ note: "",
12097
+ evidence: [],
12098
+ resourceKey,
12099
+ version: "absent",
12100
+ };
12101
+ return { ...item, state };
12102
+ });
12103
+ const required = items.filter((item) => item.required !== false);
12104
+ const completed = items.filter((item) => terminalStatuses.has(String(item?.state?.status || "pending"))).length;
12105
+ const requiredCompleted = required.filter((item) => terminalStatuses.has(String(item?.state?.status || "pending"))).length;
12106
+ const completionPolicy = String(definition.completionPolicy || definition.completion_policy || "all_required").trim().toLowerCase();
12107
+ const completionCandidates = required.length ? required : items;
12108
+ const ready = completionPolicy === "manual"
12109
+ ? false
12110
+ : completionPolicy === "any_required"
12111
+ ? completionCandidates.some((item) => terminalStatuses.has(String(item?.state?.status || "pending")))
12112
+ : completionCandidates.length > 0 && completionCandidates.every((item) => terminalStatuses.has(String(item?.state?.status || "pending")));
12113
+ const checklist = {
12114
+ ...definition,
12115
+ source: producer,
12116
+ items,
12117
+ progress: {
12118
+ total: items.length,
12119
+ completed,
12120
+ required: required.length,
12121
+ requiredCompleted,
12122
+ percent: items.length ? Math.round((completed / items.length) * 100) : 0,
12123
+ ready,
12124
+ },
12125
+ };
12126
+ return {
12127
+ ...action,
12128
+ checklist,
12129
+ ...(action.actionModel && typeof action.actionModel === "object" && !Array.isArray(action.actionModel)
12130
+ ? { actionModel: { ...action.actionModel, checklist } }
12131
+ : {}),
12132
+ };
12133
+ };
12134
+ const out = { ...snapshot };
12135
+ for (const key of ["actions", "workflowActions", "workflow_actions", "timeline", "history", "events", "runtimeEvents", "runtime_events"]) {
12136
+ if (Array.isArray(snapshot?.[key])) out[key] = snapshot[key].map(decorate);
12137
+ }
12138
+ out.checklistStates = [...stateEntries.values()];
12139
+ return out;
12140
+ }
12141
+
12142
+ function prdWorkflowFindChecklistAction(snapshot = {}, producer = "", actionKey = "") {
12143
+ const wantedProducer = String(producer || "").trim().toLowerCase();
12144
+ const wantedActionKey = String(actionKey || "").trim();
12145
+ const matches = [];
12146
+ for (const key of ["actions", "workflowActions", "workflow_actions", "timeline", "history", "events", "runtimeEvents", "runtime_events"]) {
12147
+ for (const action of Array.isArray(snapshot?.[key]) ? snapshot[key] : []) {
12148
+ if (!action || typeof action !== "object" || Array.isArray(action)) continue;
12149
+ const checklist = action.checklist || action.actionModel?.checklist;
12150
+ if (!checklist || typeof checklist !== "object" || Array.isArray(checklist)) continue;
12151
+ const resolvedActionKey = prdWorkflowChecklistActionKey(action);
12152
+ const resolvedProducer = String(checklist.source || action.source || action.producer || "").trim().toLowerCase();
12153
+ if (resolvedActionKey !== wantedActionKey) continue;
12154
+ if (wantedProducer && resolvedProducer !== wantedProducer) continue;
12155
+ matches.push({ ...action, checklist, source: resolvedProducer || wantedProducer });
12156
+ }
12157
+ }
12158
+ return matches.at(-1) || null;
12159
+ }
12160
+
11681
12161
  function prdWorkflowMergeRuntimeEvents(scopedRoot, tapdId, snapshot) {
11682
12162
  const runtime = prdWorkflowReadRuntimeEvents(scopedRoot, tapdId);
11683
12163
  const runtimeEvents = runtime.events;
@@ -11694,7 +12174,7 @@ function prdWorkflowMergeRuntimeEvents(scopedRoot, tapdId, snapshot) {
11694
12174
  for (const key of ["issues", "issueGroups", "issue_groups", "epics", "epicGroups", "epic_groups", "aiDocs", "ai_docs"]) {
11695
12175
  if (Object.prototype.hasOwnProperty.call(prdFlowExtension, key)) prdFlowExtensionView[key] = prdFlowExtension[key];
11696
12176
  }
11697
- const materialized = {
12177
+ let materialized = {
11698
12178
  ...snapshot,
11699
12179
  ...prdFlowExtensionView,
11700
12180
  workflow: globalState.workflow,
@@ -11711,6 +12191,7 @@ function prdWorkflowMergeRuntimeEvents(scopedRoot, tapdId, snapshot) {
11711
12191
  runtimeEventsUpdatedAt: runtime.updatedAt || "",
11712
12192
  },
11713
12193
  };
12194
+ materialized = prdWorkflowMaterializeChecklists(materialized, runtimeEvents);
11714
12195
  materialized.resourceVersions = workflowSnapshotResourceVersions(materialized);
11715
12196
  return materialized;
11716
12197
  }
@@ -13220,13 +13701,25 @@ export function startUiServer({
13220
13701
  ? getTeamById(requestedTeamId)
13221
13702
  : getTeamForUser(userCtx.userId);
13222
13703
  if (!team || team.status !== "active") {
13223
- json(res, 200, { ok: true, view: "team", team: null, workflows: [], timeline: [], unassignedCount: 0 });
13704
+ json(res, 200, {
13705
+ ok: true,
13706
+ view: "team",
13707
+ team: null,
13708
+ workflows: [],
13709
+ timeline: [],
13710
+ unassignedCount: 0,
13711
+ availableCount: 0,
13712
+ selectedTimelineKey: "all",
13713
+ defaultTimelineKey: "all",
13714
+ pagination: { page: 1, pageSize: 20, total: 0, totalPages: 1, hasPrevious: false, hasNext: false },
13715
+ });
13224
13716
  return;
13225
13717
  }
13226
13718
  records = listPrdWorkflowCollaborationsForTeam(team.id);
13227
13719
  } else {
13228
13720
  records = listPrdWorkflowCollaborationsForUser(userCtx.userId);
13229
13721
  }
13722
+ const accessibleProjects = listAccessibleProjectFlows(root, userCtx);
13230
13723
  const workflows = records.map((record) => {
13231
13724
  const stateRoot = path.resolve(getAgentflowUserDataRoot(record.stateOwnerId || record.ownerId));
13232
13725
  const tapdId = String(record.tapdId || "").trim();
@@ -13235,14 +13728,23 @@ export function startUiServer({
13235
13728
  const legacy = prdWorkflowReadCachedSnapshot(stateRoot, tapdId);
13236
13729
  const snapshot = project?.snapshot || latestClient || legacy?.snapshot || {};
13237
13730
  const materialized = prdWorkflowMergeRuntimeEvents(stateRoot, tapdId, snapshot);
13238
- return prdWorkflowDashboardSummary(record, materialized, userCtx);
13731
+ const projectBindings = workflowProjectBindingRows(record.projectBindings, accessibleProjects, userCtx);
13732
+ return prdWorkflowDashboardSummary(record, materialized, userCtx, projectBindings);
13239
13733
  });
13240
13734
  const dashboardTimeline = prdWorkflowDashboardTimeline(workflows);
13735
+ const dashboardPage = prdWorkflowDashboardPage(workflows, dashboardTimeline, {
13736
+ timelineKey: url.searchParams.has("timelineKey") ? url.searchParams.get("timelineKey") : "",
13737
+ query: url.searchParams.get("q"),
13738
+ scope: url.searchParams.get("scope"),
13739
+ state: url.searchParams.get("state"),
13740
+ page: url.searchParams.get("page"),
13741
+ pageSize: url.searchParams.get("pageSize"),
13742
+ });
13241
13743
  json(res, 200, {
13242
13744
  ok: true,
13243
13745
  view: view === "team" ? "team" : "personal",
13244
13746
  team: teamSummaryWithUsers(team),
13245
- workflows,
13747
+ ...dashboardPage,
13246
13748
  ...dashboardTimeline,
13247
13749
  });
13248
13750
  } catch (error) {
@@ -13373,6 +13875,177 @@ export function startUiServer({
13373
13875
  });
13374
13876
  return;
13375
13877
  }
13878
+ if (req.method === "GET" && url.pathname === "/api/workflows/project-bindings") {
13879
+ if (!authUser?.userId) {
13880
+ json(res, 401, { error: "Authentication required" });
13881
+ return;
13882
+ }
13883
+ const tapdId = String(url.searchParams.get("tapdId") || url.searchParams.get("id") || "").trim();
13884
+ if (!tapdId) {
13885
+ json(res, 400, { error: "Missing tapdId" });
13886
+ return;
13887
+ }
13888
+ const existing = getPrdWorkflowCollaborationByTapdId(tapdId);
13889
+ const result = listPrdWorkflowProjectBindings({ tapdId, userId: userCtx.userId });
13890
+ if (existing && result.error) {
13891
+ json(res, 403, { error: "PRD Workflow collaboration permission denied" });
13892
+ return;
13893
+ }
13894
+ const accessibleProjects = listAccessibleProjectFlows(root, userCtx);
13895
+ const bindings = workflowProjectBindingRows(result.projectBindings || [], accessibleProjects, userCtx);
13896
+ json(res, 200, {
13897
+ ok: true,
13898
+ bindings,
13899
+ availableProjects: availableWorkflowBindingProjects(accessibleProjects, bindings),
13900
+ });
13901
+ return;
13902
+ }
13903
+ if (req.method === "POST" && url.pathname === "/api/workflows/project-bindings") {
13904
+ if (!authUser?.userId) {
13905
+ json(res, 401, { error: "Authentication required" });
13906
+ return;
13907
+ }
13908
+ let payload;
13909
+ try {
13910
+ payload = JSON.parse(await readBody(req, 128 * 1024));
13911
+ } catch {
13912
+ json(res, 400, { error: "Invalid JSON body" });
13913
+ return;
13914
+ }
13915
+ const tapdId = String(payload?.tapdId || payload?.tapd_id || "").trim();
13916
+ const flowId = String(payload?.flowId || "").trim();
13917
+ const flowSource = String(payload?.flowSource || "user").trim() || "user";
13918
+ const workspaceId = String(payload?.workspaceId || "").trim();
13919
+ if (!tapdId || !flowId) {
13920
+ json(res, 400, { error: "Project binding requires tapdId and flowId" });
13921
+ return;
13922
+ }
13923
+ if (flowSource !== "user" && flowSource !== "workspace") {
13924
+ json(res, 400, { error: "Only editable Projects can be bound" });
13925
+ return;
13926
+ }
13927
+ const existingWorkflow = getPrdWorkflowCollaborationByTapdId(tapdId);
13928
+ if (existingWorkflow && !getPrdWorkflowCollaborationForUser(tapdId, userCtx.userId)) {
13929
+ json(res, 403, { error: "PRD Workflow collaboration permission denied" });
13930
+ return;
13931
+ }
13932
+ const scoped = resolveWorkspaceScopeRoot(root, {
13933
+ flowId,
13934
+ flowSource,
13935
+ workspaceId,
13936
+ archived: false,
13937
+ }, userCtx);
13938
+ if (scoped.error) {
13939
+ json(res, scoped.status || 400, { error: scoped.error });
13940
+ return;
13941
+ }
13942
+ if (scoped.archived || (scoped.collaboration && !scoped.collaborationAccess?.writable)) {
13943
+ json(res, 403, { error: "Only Project owners and editors can bind an iteration" });
13944
+ return;
13945
+ }
13946
+ const projectCollaboration = scoped.collaboration
13947
+ ? { record: scoped.collaboration, workspace: workspaceCollaborationSummary(scoped.collaboration, userCtx.userId) }
13948
+ : ensureWorkspaceCollaboration({
13949
+ flowId: scoped.flowId,
13950
+ flowSource: scoped.flowSource,
13951
+ archived: false,
13952
+ userId: userCtx.userId,
13953
+ });
13954
+ if (projectCollaboration.error || !projectCollaboration.record?.id) {
13955
+ json(res, projectCollaboration.status || 400, { error: projectCollaboration.error || "Project collaboration is unavailable" });
13956
+ return;
13957
+ }
13958
+ const projectAccess = workspaceCollaborationAccess(projectCollaboration.record, userCtx.userId);
13959
+ if (!projectAccess.writable) {
13960
+ json(res, 403, { error: "Only Project owners and editors can bind an iteration" });
13961
+ return;
13962
+ }
13963
+ const ensuredWorkflow = ensurePrdWorkflowCollaboration({ tapdId, userId: userCtx.userId });
13964
+ if (ensuredWorkflow.error) {
13965
+ json(res, ensuredWorkflow.status || 400, { error: ensuredWorkflow.error });
13966
+ return;
13967
+ }
13968
+ const result = bindPrdWorkflowProject({
13969
+ tapdId,
13970
+ userId: userCtx.userId,
13971
+ project: {
13972
+ workspaceId: projectCollaboration.record.id,
13973
+ flowId: scoped.flowId,
13974
+ flowSource: scoped.flowSource,
13975
+ archived: false,
13976
+ ownerId: projectCollaboration.record.ownerId,
13977
+ },
13978
+ });
13979
+ if (result.error) {
13980
+ json(res, result.status || 400, { error: result.error });
13981
+ return;
13982
+ }
13983
+ const accessibleProjects = listAccessibleProjectFlows(root, userCtx);
13984
+ const bindings = workflowProjectBindingRows(result.projectBindings, accessibleProjects, userCtx);
13985
+ json(res, 200, {
13986
+ ok: true,
13987
+ created: result.created === true,
13988
+ bindings,
13989
+ availableProjects: availableWorkflowBindingProjects(accessibleProjects, bindings),
13990
+ });
13991
+ return;
13992
+ }
13993
+ if (req.method === "DELETE" && url.pathname === "/api/workflows/project-bindings") {
13994
+ if (!authUser?.userId) {
13995
+ json(res, 401, { error: "Authentication required" });
13996
+ return;
13997
+ }
13998
+ let payload;
13999
+ try {
14000
+ payload = JSON.parse(await readBody(req, 128 * 1024));
14001
+ } catch {
14002
+ json(res, 400, { error: "Invalid JSON body" });
14003
+ return;
14004
+ }
14005
+ const tapdId = String(payload?.tapdId || payload?.tapd_id || "").trim();
14006
+ const workspaceId = String(payload?.workspaceId || "").trim();
14007
+ if (!tapdId || !workspaceId) {
14008
+ json(res, 400, { error: "Unbinding requires tapdId and workspaceId" });
14009
+ return;
14010
+ }
14011
+ const listed = listPrdWorkflowProjectBindings({ tapdId, userId: userCtx.userId });
14012
+ if (listed.error) {
14013
+ json(res, listed.status || 400, { error: listed.error });
14014
+ return;
14015
+ }
14016
+ const binding = listed.projectBindings.find((item) => item.workspaceId === workspaceId);
14017
+ if (!binding) {
14018
+ json(res, 404, { error: "Project binding not found" });
14019
+ return;
14020
+ }
14021
+ const scoped = resolveWorkspaceScopeRoot(root, {
14022
+ flowId: binding.flowId,
14023
+ flowSource: binding.flowSource,
14024
+ workspaceId,
14025
+ archived: binding.archived === true,
14026
+ }, userCtx);
14027
+ if (scoped.error) {
14028
+ json(res, scoped.status || 400, { error: scoped.error });
14029
+ return;
14030
+ }
14031
+ if (!scoped.collaborationAccess?.writable) {
14032
+ json(res, 403, { error: "Only Project owners and editors can unbind an iteration" });
14033
+ return;
14034
+ }
14035
+ const result = unbindPrdWorkflowProject({ tapdId, userId: userCtx.userId, workspaceId });
14036
+ if (result.error) {
14037
+ json(res, result.status || 400, { error: result.error });
14038
+ return;
14039
+ }
14040
+ const accessibleProjects = listAccessibleProjectFlows(root, userCtx);
14041
+ const bindings = workflowProjectBindingRows(result.projectBindings, accessibleProjects, userCtx);
14042
+ json(res, 200, {
14043
+ ok: true,
14044
+ bindings,
14045
+ availableProjects: availableWorkflowBindingProjects(accessibleProjects, bindings),
14046
+ });
14047
+ return;
14048
+ }
13376
14049
  if (req.method === "GET" && url.pathname === "/api/workflows/knowledge-bindings") {
13377
14050
  if (!authUser?.userId) {
13378
14051
  json(res, 401, { error: "Authentication required" });
@@ -13713,6 +14386,276 @@ export function startUiServer({
13713
14386
  }
13714
14387
  return;
13715
14388
  }
14389
+ if (req.method === "GET" && url.pathname === "/api/workflows/checklist") {
14390
+ try {
14391
+ const workflow = normalizeWorkflowReference({ workflow: url.searchParams.get("workflow") || "" });
14392
+ if (workflow.error) {
14393
+ json(res, 400, { error: workflow.error });
14394
+ return;
14395
+ }
14396
+ if (workflow.namespace !== "tapd") {
14397
+ json(res, 400, { error: `Unsupported workflow namespace: ${workflow.namespace}` });
14398
+ return;
14399
+ }
14400
+ const source = String(url.searchParams.get("source") || "").trim().toLowerCase();
14401
+ const actionKey = String(url.searchParams.get("actionKey") || url.searchParams.get("action_key") || "").trim();
14402
+ if (!/^[a-z][a-z0-9._-]{0,119}$/.test(source)) {
14403
+ json(res, 400, { error: "Invalid checklist source" });
14404
+ return;
14405
+ }
14406
+ if (!actionKey || actionKey.length > 240 || /[\0\r\n]/.test(actionKey)) {
14407
+ json(res, 400, { error: "Invalid checklist actionKey" });
14408
+ return;
14409
+ }
14410
+ const flowId = String(url.searchParams.get("flowId") || "").trim();
14411
+ const flowSource = String(url.searchParams.get("flowSource") || "user").trim() || "user";
14412
+ const workflowScope = resolvePrdWorkflowScope(root, {
14413
+ tapdId: workflow.id,
14414
+ flowId,
14415
+ flowSource,
14416
+ archived: url.searchParams.get("archived") === "1",
14417
+ workspaceId: url.searchParams.get("workspaceId") || "",
14418
+ workflowShare: url.searchParams.get("workflowShare") || "",
14419
+ }, userCtx, "read");
14420
+ if (workflowScope.error) {
14421
+ json(res, workflowScope.status || 400, { error: workflowScope.error });
14422
+ return;
14423
+ }
14424
+ prdWorkflowMigrateLegacyState(workflowScope.executionRoot, workflowScope.stateRoot, workflow.id);
14425
+ const snapshot = prdWorkflowMaterializeSnapshot(
14426
+ workflowScope.executionRoot,
14427
+ workflowScope.stateRoot,
14428
+ workflow.id,
14429
+ userCtx,
14430
+ { flowSource, flowId },
14431
+ );
14432
+ const action = prdWorkflowFindChecklistAction(snapshot, source, actionKey);
14433
+ if (!action) {
14434
+ json(res, 404, { error: "Workflow Action checklist not found" });
14435
+ return;
14436
+ }
14437
+ const access = workflowScope.collaborationAccess || {};
14438
+ const canWrite = Boolean(authUser?.userId) && !workflowScope.sharedByLink && !workflowScope.adminReadonly && (
14439
+ workflowScope.collaboration ? access.writable === true : true
14440
+ );
14441
+ json(res, 200, {
14442
+ ok: true,
14443
+ workflow,
14444
+ action: {
14445
+ key: actionKey,
14446
+ source,
14447
+ title: String(action.title || action.label || actionKey),
14448
+ status: String(action.status || "pending"),
14449
+ checklist: action.checklist,
14450
+ },
14451
+ canWrite,
14452
+ });
14453
+ } catch (error) {
14454
+ json(res, 500, { error: (error && error.message) || String(error) });
14455
+ }
14456
+ return;
14457
+ }
14458
+ if (req.method === "PATCH" && url.pathname === "/api/workflows/checklist") {
14459
+ if (!authUser?.userId) {
14460
+ json(res, 401, { error: "Authentication required" });
14461
+ return;
14462
+ }
14463
+ let payload;
14464
+ try {
14465
+ payload = JSON.parse(await readBody(req, 1024 * 1024));
14466
+ } catch (error) {
14467
+ json(res, error?.status === 413 ? 413 : 400, { error: error?.status === 413 ? error.message : "Invalid JSON body" });
14468
+ return;
14469
+ }
14470
+ let releaseWorkflowWriteLock = null;
14471
+ try {
14472
+ const workflow = normalizeWorkflowReference(payload);
14473
+ if (workflow.error) {
14474
+ json(res, 400, { error: workflow.error });
14475
+ return;
14476
+ }
14477
+ if (workflow.namespace !== "tapd") {
14478
+ json(res, 400, { error: `Unsupported workflow namespace: ${workflow.namespace}` });
14479
+ return;
14480
+ }
14481
+ const source = String(payload.source || "").trim().toLowerCase();
14482
+ const actionKey = String(payload.actionKey || payload.action_key || "").trim();
14483
+ const itemKey = String(payload.itemKey || payload.item_key || "").trim();
14484
+ if (!/^[a-z][a-z0-9._-]{0,119}$/.test(source)) {
14485
+ json(res, 400, { error: "Invalid checklist source" });
14486
+ return;
14487
+ }
14488
+ if (!actionKey || actionKey.length > 240 || /[\0\r\n]/.test(actionKey)) {
14489
+ json(res, 400, { error: "Invalid checklist actionKey" });
14490
+ return;
14491
+ }
14492
+ if (!itemKey || itemKey.length > 240 || /[\0\r\n]/.test(itemKey)) {
14493
+ json(res, 400, { error: "Invalid checklist itemKey" });
14494
+ return;
14495
+ }
14496
+ const rawStatus = String(payload.status || "pending").trim().toLowerCase();
14497
+ if (!["pending", "passed", "failed", "blocked", "skipped", "done", "complete", "completed", "success", "error", "cancelled", "canceled"].includes(rawStatus)) {
14498
+ json(res, 400, { error: `Invalid checklist item status: ${rawStatus}` });
14499
+ return;
14500
+ }
14501
+ const status = normalizeWorkflowChecklistItemStatus(rawStatus);
14502
+ const note = String(payload.note || "").trim();
14503
+ if (note.length > 4000) {
14504
+ json(res, 400, { error: "Checklist note exceeds 4000 characters" });
14505
+ return;
14506
+ }
14507
+ const rawEvidence = Array.isArray(payload.evidence) ? payload.evidence : [];
14508
+ if (rawEvidence.length > 20) {
14509
+ json(res, 400, { error: "Checklist evidence supports at most 20 entries" });
14510
+ return;
14511
+ }
14512
+ const evidence = [];
14513
+ for (let index = 0; index < rawEvidence.length; index += 1) {
14514
+ const item = rawEvidence[index];
14515
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
14516
+ json(res, 400, { error: `evidence[${index}] must be an object` });
14517
+ return;
14518
+ }
14519
+ const evidenceUrl = String(item.url || item.href || "").trim();
14520
+ if (!evidenceUrl || evidenceUrl.length > 4000 || !isSafeWorkflowUrl(evidenceUrl)) {
14521
+ json(res, 400, { error: `evidence[${index}].url must use http, https, or an absolute application path` });
14522
+ return;
14523
+ }
14524
+ evidence.push({
14525
+ title: String(item.title || item.label || `证据 ${index + 1}`).trim().slice(0, 500),
14526
+ url: evidenceUrl,
14527
+ });
14528
+ }
14529
+ const expectedVersion = String(payload.expectedVersion || payload.expected_version || "").trim();
14530
+ if (!expectedVersion) {
14531
+ json(res, 400, { error: "Checklist update requires expectedVersion" });
14532
+ return;
14533
+ }
14534
+ const flowId = String(payload.flowId || payload.flow_id || "").trim();
14535
+ const flowSource = String(payload.flowSource || payload.flow_source || "user").trim() || "user";
14536
+ const workflowScope = resolvePrdWorkflowScope(root, {
14537
+ ...payload,
14538
+ tapdId: workflow.id,
14539
+ flowId,
14540
+ flowSource,
14541
+ }, userCtx, "write");
14542
+ if (workflowScope.error) {
14543
+ json(res, workflowScope.status || 400, { error: workflowScope.error });
14544
+ return;
14545
+ }
14546
+ if (!workflowScope.collaboration) {
14547
+ const ensured = ensurePrdWorkflowCollaboration({ tapdId: workflow.id, userId: userCtx.userId });
14548
+ if (ensured.error) {
14549
+ json(res, ensured.status || 400, { error: ensured.error });
14550
+ return;
14551
+ }
14552
+ }
14553
+ const scopedRoot = workflowScope.stateRoot;
14554
+ prdWorkflowMigrateLegacyState(workflowScope.executionRoot, scopedRoot, workflow.id);
14555
+ releaseWorkflowWriteLock = await prdWorkflowAcquireWriteLock(`${scopedRoot}\t${workflow.id}`);
14556
+ let snapshot = prdWorkflowMaterializeSnapshot(
14557
+ workflowScope.executionRoot,
14558
+ scopedRoot,
14559
+ workflow.id,
14560
+ userCtx,
14561
+ { flowSource, flowId },
14562
+ );
14563
+ const idempotencyKey = String(payload.idempotencyKey || payload.idempotency_key || "").trim().slice(0, 500);
14564
+ if (idempotencyKey) {
14565
+ const existing = prdWorkflowFindIdempotencyEvent(scopedRoot, workflow.id, idempotencyKey, "agentflow-checklist", false, "checklist.update");
14566
+ if (existing) {
14567
+ json(res, 200, { ok: true, alreadyApplied: true, workflow, checklistState: existing.checklistState, snapshot });
14568
+ return;
14569
+ }
14570
+ }
14571
+ const action = prdWorkflowFindChecklistAction(snapshot, source, actionKey);
14572
+ const checklistItem = action?.checklist?.items?.find((item) => String(item?.key || "") === itemKey);
14573
+ if (!action || !checklistItem) {
14574
+ json(res, 404, { error: "Workflow Action checklist item not found" });
14575
+ return;
14576
+ }
14577
+ if (status === "passed" && checklistItem.evidenceRequired === true && evidence.length === 0) {
14578
+ json(res, 400, { error: "Checklist item requires evidence before it can pass" });
14579
+ return;
14580
+ }
14581
+ const resourceKey = prdWorkflowChecklistResourceKey(source, actionKey, itemKey);
14582
+ const currentVersion = String(snapshot.resourceVersions?.[resourceKey] || "absent");
14583
+ if (expectedVersion !== currentVersion) {
14584
+ json(res, 409, {
14585
+ error: "Checklist item changed; refresh it before saving",
14586
+ conflict: { type: "workflow-resource-conflict", conflicts: [{ resourceKey, expectedVersion, currentVersion }], workflow },
14587
+ snapshot,
14588
+ });
14589
+ return;
14590
+ }
14591
+ const now = new Date().toISOString();
14592
+ const checklistState = {
14593
+ producer: source,
14594
+ actionKey,
14595
+ itemKey,
14596
+ status,
14597
+ note,
14598
+ evidence,
14599
+ updatedAt: now,
14600
+ updatedBy: {
14601
+ userId: String(userCtx.userId || ""),
14602
+ username: String(authUser.username || userCtx.userId || ""),
14603
+ },
14604
+ };
14605
+ const event = prdWorkflowAppendRuntimeEvent(scopedRoot, workflow.id, {
14606
+ id: `checklist_state_${prdWorkflowSafeStateId([source, actionKey, itemKey].join(":"))}`,
14607
+ type: "workflow-checklist-update",
14608
+ operation: "checklist.update",
14609
+ source: "agentflow-checklist",
14610
+ auxiliary: true,
14611
+ aggregateByStage: false,
14612
+ status: "done",
14613
+ checklistState,
14614
+ ...(idempotencyKey ? { idempotencyKey } : {}),
14615
+ });
14616
+ if (!event) throw new Error("Failed to store checklist state");
14617
+ snapshot = prdWorkflowMaterializeSnapshot(
14618
+ workflowScope.executionRoot,
14619
+ scopedRoot,
14620
+ workflow.id,
14621
+ userCtx,
14622
+ { flowSource, flowId },
14623
+ );
14624
+ const updatedAction = prdWorkflowFindChecklistAction(snapshot, source, actionKey);
14625
+ const updatedItem = updatedAction?.checklist?.items?.find((item) => String(item?.key || "") === itemKey);
14626
+ prdWorkflowAppendAudit(scopedRoot, workflow.id, {
14627
+ type: "checklist-item-updated",
14628
+ source,
14629
+ actionKey,
14630
+ itemKey,
14631
+ status,
14632
+ resourceKey,
14633
+ actorUserId: String(userCtx.userId || ""),
14634
+ });
14635
+ prdWorkflowBroadcast(prdWorkflowKey(userCtx, flowSource, flowId, workflow.id), {
14636
+ type: "workflow-checklist-updated",
14637
+ tapdId: workflow.id,
14638
+ source,
14639
+ actionKey,
14640
+ itemKey,
14641
+ checklistState: updatedItem?.state || checklistState,
14642
+ snapshot,
14643
+ });
14644
+ json(res, 200, {
14645
+ ok: true,
14646
+ alreadyApplied: false,
14647
+ workflow,
14648
+ checklistState: updatedItem?.state || checklistState,
14649
+ checklist: updatedAction?.checklist || null,
14650
+ snapshot,
14651
+ });
14652
+ } catch (error) {
14653
+ json(res, 500, { error: (error && error.message) || String(error) });
14654
+ } finally {
14655
+ releaseWorkflowWriteLock?.();
14656
+ }
14657
+ return;
14658
+ }
13716
14659
  if (req.method === "GET" && url.pathname === "/api/prd-workflow/snapshot") {
13717
14660
  try {
13718
14661
  const tapdId = String(url.searchParams.get("tapdId") || "").trim();
@@ -14534,6 +15477,11 @@ export function startUiServer({
14534
15477
  json(res, 400, { error: `Unsupported workflow namespace: ${report.workflow.namespace}` });
14535
15478
  return;
14536
15479
  }
15480
+ const adminVersionRepair = prdWorkflowAdminVersionRepairIntent(payload, report, userCtx);
15481
+ if (adminVersionRepair.error) {
15482
+ json(res, adminVersionRepair.status || 400, { error: adminVersionRepair.error });
15483
+ return;
15484
+ }
14537
15485
  const tapdId = report.workflow.id;
14538
15486
  const flowId = report.flowId;
14539
15487
  const flowSource = report.flowSource || "user";
@@ -14544,12 +15492,12 @@ export function startUiServer({
14544
15492
  flowId,
14545
15493
  flowSource,
14546
15494
  archived,
14547
- }, userCtx, "write");
15495
+ }, userCtx, adminVersionRepair.requested ? "admin-version-repair" : "write");
14548
15496
  if (workflowScope.error) {
14549
15497
  json(res, workflowScope.status || 400, { error: workflowScope.error });
14550
15498
  return;
14551
15499
  }
14552
- if (!workflowScope.collaboration) {
15500
+ if (!workflowScope.collaboration && !adminVersionRepair.requested) {
14553
15501
  const ensured = ensurePrdWorkflowCollaboration({ tapdId, userId: userCtx.userId });
14554
15502
  if (ensured.error) {
14555
15503
  json(res, ensured.status || 400, { error: ensured.error });
@@ -14657,7 +15605,9 @@ export function startUiServer({
14657
15605
  });
14658
15606
  return;
14659
15607
  }
14660
- report = prdWorkflowMergeProducerTimeline(report, currentSnapshot);
15608
+ report = adminVersionRepair.requested
15609
+ ? prdWorkflowMergeAdminVersionTimeline(report, currentSnapshot)
15610
+ : prdWorkflowMergeProducerTimeline(report, currentSnapshot);
14661
15611
  if (report.error) {
14662
15612
  json(res, 400, { error: report.error });
14663
15613
  return;
@@ -14704,11 +15654,17 @@ export function startUiServer({
14704
15654
  getSessionTokenFromRequest(req) || "",
14705
15655
  );
14706
15656
  prdWorkflowBroadcast(
14707
- prdWorkflowKey(userCtx, flowSource, flowId, tapdId),
15657
+ prdWorkflowKey(
15658
+ adminVersionRepair.requested ? { userId: workflowScope.stateOwnerId } : userCtx,
15659
+ flowSource,
15660
+ flowId,
15661
+ tapdId,
15662
+ ),
14708
15663
  { type: "workflow-report", tapdId, workflow: report.workflow, event, observation: Boolean(observation), snapshot },
14709
15664
  );
14710
15665
  json(res, 200, {
14711
15666
  ok: true,
15667
+ ...(adminVersionRepair.requested ? { administrativeRepair: report.event.administrativeRepair } : {}),
14712
15668
  report,
14713
15669
  resourceKeys,
14714
15670
  event,
@@ -15701,48 +16657,7 @@ export function startUiServer({
15701
16657
  try {
15702
16658
  const projectView = String(url.searchParams.get("view") || "all").trim().toLowerCase();
15703
16659
  const currentTeam = getTeamForUser(userCtx.userId);
15704
- const flows = listFlowsJson(root, { ...userCtx, includeWorkspaceFlows: true })
15705
- .filter((flow) => (
15706
- !workspaceFlowCollaborationGuard(
15707
- flow.id,
15708
- flow.source || "user",
15709
- flow.archived === true,
15710
- userCtx,
15711
- "read",
15712
- )
15713
- ))
15714
- .map((flow) => {
15715
- const source = flow.source || "user";
15716
- const collaboration = source === "workspace"
15717
- ? getWorkspaceCollaborationByFlow(flow.id, flow.archived === true)
15718
- : getWorkspaceCollaborationForProject({
15719
- flowId: flow.id,
15720
- flowSource: source,
15721
- archived: flow.archived === true,
15722
- ownerId: userCtx.userId,
15723
- });
15724
- return collaboration
15725
- ? { ...flow, collaboration: workspaceCollaborationSummaryWithUsers(collaboration, userCtx.userId) }
15726
- : flow;
15727
- });
15728
- const existingCollaborationIds = new Set(flows.map((flow) => flow.collaboration?.id).filter(Boolean));
15729
- for (const record of listWorkspaceCollaborationsForUser(userCtx.userId)) {
15730
- const source = record.projectSource || record.flowSource || "workspace";
15731
- if (source !== "user" || record.ownerId === userCtx.userId) continue;
15732
- if (existingCollaborationIds.has(record.id)) continue;
15733
- const ownerFlow = listFlowsJson(root, { userId: record.ownerId })
15734
- .find((flow) => (
15735
- flow.id === record.flowId
15736
- && (flow.source || "user") === "user"
15737
- && Boolean(flow.archived) === Boolean(record.archived)
15738
- ));
15739
- if (!ownerFlow) continue;
15740
- flows.push({
15741
- ...ownerFlow,
15742
- collaboration: workspaceCollaborationSummaryWithUsers(record, userCtx.userId),
15743
- });
15744
- existingCollaborationIds.add(record.id);
15745
- }
16660
+ const flows = listAccessibleProjectFlows(root, userCtx);
15746
16661
  const visibleFlows = projectView === "team"
15747
16662
  ? flows.filter((flow) => (
15748
16663
  currentTeam
@@ -19585,6 +20500,7 @@ finishedAt: "${new Date().toISOString()}"
19585
20500
  return;
19586
20501
  }
19587
20502
  const flowSource = payload.flowSource || "user";
20503
+ const requestedRunId = typeof payload.runId === "string" ? payload.runId.trim() : "";
19588
20504
  const collaborationDenied = workspaceFlowCollaborationGuard(
19589
20505
  flowId,
19590
20506
  flowSource,
@@ -19599,6 +20515,13 @@ finishedAt: "${new Date().toISOString()}"
19599
20515
  const runKey = workspaceRunKey(userCtx, flowSource, flowId);
19600
20516
  const entry = activeFlowRuns.get(runKey);
19601
20517
  if (!entry || !entry.child) {
20518
+ if (requestedRunId) {
20519
+ const cancelled = cancelScheduledRun(root, flowId, requestedRunId, userCtx);
20520
+ if (cancelled.ok && cancelled.updatedWaits > 0) {
20521
+ json(res, 200, { ok: true, cancelledWaitingRun: true, ...cancelled });
20522
+ return;
20523
+ }
20524
+ }
19602
20525
  json(res, 404, { error: "该流水线未在运行" });
19603
20526
  return;
19604
20527
  }