@fieldwangai/agentflow 0.1.141 → 0.1.143
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.
- package/bin/lib/ui-server.mjs +651 -12
- package/bin/lib/workflow-report.mjs +127 -1
- package/builtin/web-ui/dist/assets/{WorkflowAssistantThread-ubxHcM7p.js → WorkflowAssistantThread-BttvNPJr.js} +1 -1
- package/builtin/web-ui/dist/assets/index-CtG65J3e.css +1 -0
- package/builtin/web-ui/dist/assets/index-c8AUUsSF.js +839 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/skills/agentflow-cli/SKILL.md +5 -0
- package/skills/agentflow-cli/scripts/agentflow-cli.mjs +6 -2
- package/skills/agentflow-cli/scripts/workflow-report-client.mjs +2 -1
- package/skills/agentflow-workflow-report/SKILL.md +8 -1
- package/skills/agentflow-workflow-report/references/protocol.md +138 -7
- package/builtin/pipelines/jenkins-build-notify/flow.yaml +0 -217
- package/builtin/web-ui/dist/assets/index-B6TWUomI.css +0 -1
- package/builtin/web-ui/dist/assets/index-DQzcZp7S.js +0 -590
package/bin/lib/ui-server.mjs
CHANGED
|
@@ -188,6 +188,8 @@ import {
|
|
|
188
188
|
mergeWorkflowArtifactLists,
|
|
189
189
|
mergeWorkflowArtifacts,
|
|
190
190
|
mergeWorkflowGlobalState,
|
|
191
|
+
isSafeWorkflowUrl,
|
|
192
|
+
normalizeWorkflowChecklistItemStatus,
|
|
191
193
|
normalizeWorkflowReference,
|
|
192
194
|
normalizeWorkflowReport,
|
|
193
195
|
removeWorkflowGlobalStatePath,
|
|
@@ -3777,6 +3779,8 @@ function prdWorkflowDashboardSummary(record, snapshot = {}, userCtx = {}, projec
|
|
|
3777
3779
|
id: String(entry.id || ""),
|
|
3778
3780
|
title: String(entry.title || entry.label || entry.id || ""),
|
|
3779
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 || ""),
|
|
3780
3784
|
source: String(entry.source || ""),
|
|
3781
3785
|
dimensions: entry.dimensions && typeof entry.dimensions === "object" && !Array.isArray(entry.dimensions)
|
|
3782
3786
|
? entry.dimensions
|
|
@@ -3910,6 +3914,8 @@ export function prdWorkflowDashboardTimeline(workflows = []) {
|
|
|
3910
3914
|
id: identity || String(entry.id || ""),
|
|
3911
3915
|
title: String(entry.title || entry.id || ""),
|
|
3912
3916
|
date: String(entry.date || ""),
|
|
3917
|
+
startDate: String(entry.startDate || ""),
|
|
3918
|
+
endDate: String(entry.endDate || entry.date || ""),
|
|
3913
3919
|
source: String(entry.source || ""),
|
|
3914
3920
|
dimensions: entry.dimensions && typeof entry.dimensions === "object" && !Array.isArray(entry.dimensions)
|
|
3915
3921
|
? entry.dimensions
|
|
@@ -3924,6 +3930,8 @@ export function prdWorkflowDashboardTimeline(workflows = []) {
|
|
|
3924
3930
|
current.kind = String(entry.kind || current.kind);
|
|
3925
3931
|
current.title = String(entry.title || current.title);
|
|
3926
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);
|
|
3927
3935
|
current.source = String(entry.source || current.source);
|
|
3928
3936
|
current.dimensions = prdWorkflowDashboardMergeTimelineDimensions(current.dimensions, entry.dimensions);
|
|
3929
3937
|
current.order = Number.isFinite(Number(entry.order)) ? Number(entry.order) : current.order;
|
|
@@ -3956,6 +3964,112 @@ export function prdWorkflowDashboardTimeline(workflows = []) {
|
|
|
3956
3964
|
};
|
|
3957
3965
|
}
|
|
3958
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
|
+
|
|
3959
4073
|
function workspaceConversationsPath(scopedRoot) {
|
|
3960
4074
|
return path.join(path.resolve(scopedRoot), ".workspace", "agentflow", "conversations.json");
|
|
3961
4075
|
}
|
|
@@ -8185,6 +8299,10 @@ function resolvePrdWorkflowScope(workspaceRoot, params = {}, userCtx = {}, capab
|
|
|
8185
8299
|
return { error: "Admin permission required", status: 403 };
|
|
8186
8300
|
}
|
|
8187
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
|
+
}
|
|
8188
8306
|
if (adminOwnerId && !adminOwner) {
|
|
8189
8307
|
return { error: "Workspace owner not found", status: 404 };
|
|
8190
8308
|
}
|
|
@@ -8200,19 +8318,24 @@ function resolvePrdWorkflowScope(workspaceRoot, params = {}, userCtx = {}, capab
|
|
|
8200
8318
|
? getPrdWorkflowCollaborationForUser(tapdId, userCtx?.userId)
|
|
8201
8319
|
: null;
|
|
8202
8320
|
const existingCollaboration = tapdId ? getPrdWorkflowCollaborationByTapdId(tapdId) : null;
|
|
8203
|
-
if (!adminOwner && !linkCollaboration && existingCollaboration && !memberCollaboration) {
|
|
8321
|
+
if (!adminOwner && !linkCollaboration && existingCollaboration && !memberCollaboration && !adminVersionRepair) {
|
|
8204
8322
|
return { error: "PRD Workflow collaboration permission denied", status: 403 };
|
|
8205
8323
|
}
|
|
8206
|
-
|
|
8324
|
+
if (adminVersionRepair && !existingCollaboration) {
|
|
8325
|
+
return { error: "PRD Workflow collaboration not found", status: 404 };
|
|
8326
|
+
}
|
|
8327
|
+
const collaboration = adminOwner ? null : (adminVersionRepair ? existingCollaboration : (linkCollaboration || memberCollaboration));
|
|
8207
8328
|
const access = adminOwner
|
|
8208
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" }
|
|
8209
8332
|
: linkCollaboration
|
|
8210
8333
|
? { allowed: true, writable: false, role: "viewer", via: "share-link" }
|
|
8211
8334
|
: prdWorkflowCollaborationAccess(collaboration, userCtx?.userId);
|
|
8212
8335
|
if (collaboration && !access.allowed) {
|
|
8213
8336
|
return { error: "PRD Workflow collaboration permission denied", status: 403 };
|
|
8214
8337
|
}
|
|
8215
|
-
if (capability === "write" && (linkCollaboration || (collaboration && !access.writable))) {
|
|
8338
|
+
if ((capability === "write" || adminVersionRepair) && (linkCollaboration || (collaboration && !access.writable))) {
|
|
8216
8339
|
return { error: "PRD Workflow collaboration edit permission denied", status: 403 };
|
|
8217
8340
|
}
|
|
8218
8341
|
const ownerId = String(adminOwner?.userId || collaboration?.ownerId || userCtx?.userId || "").trim();
|
|
@@ -8245,6 +8368,7 @@ function resolvePrdWorkflowScope(workspaceRoot, params = {}, userCtx = {}, capab
|
|
|
8245
8368
|
shareToken,
|
|
8246
8369
|
sharedByLink: Boolean(linkCollaboration),
|
|
8247
8370
|
adminReadonly: Boolean(adminOwner),
|
|
8371
|
+
adminVersionRepair,
|
|
8248
8372
|
flowId,
|
|
8249
8373
|
flowSource,
|
|
8250
8374
|
archived,
|
|
@@ -11552,6 +11676,79 @@ function prdWorkflowMergeProducerTimeline(report, currentSnapshot = {}) {
|
|
|
11552
11676
|
};
|
|
11553
11677
|
}
|
|
11554
11678
|
|
|
11679
|
+
function prdWorkflowAdminVersionRepairOperation(value = "", userCtx = {}) {
|
|
11680
|
+
const operation = String(value || "").trim().toLowerCase();
|
|
11681
|
+
if (!operation) return { requested: false };
|
|
11682
|
+
if (operation !== "repair-version-membership") {
|
|
11683
|
+
return { requested: true, status: 400, error: `Unsupported admin Workflow operation: ${operation}` };
|
|
11684
|
+
}
|
|
11685
|
+
if (userCtx.isAdmin !== true) {
|
|
11686
|
+
return { requested: true, status: 403, error: "Admin permission required" };
|
|
11687
|
+
}
|
|
11688
|
+
return { requested: true, operation };
|
|
11689
|
+
}
|
|
11690
|
+
|
|
11691
|
+
function prdWorkflowAdminVersionRepairIntent(payload = {}, report = {}, userCtx = {}) {
|
|
11692
|
+
const intent = prdWorkflowAdminVersionRepairOperation(
|
|
11693
|
+
payload.adminOperation || payload.admin_operation || payload.administrativeOperation || payload.administrative_operation || "",
|
|
11694
|
+
userCtx,
|
|
11695
|
+
);
|
|
11696
|
+
if (!intent.requested || intent.error) return intent;
|
|
11697
|
+
const forbiddenKeys = ["action", "artifacts", "observation", "globalState", "global_state", "extensions", "extension"]
|
|
11698
|
+
.filter((key) => Object.prototype.hasOwnProperty.call(payload, key));
|
|
11699
|
+
if (forbiddenKeys.length) {
|
|
11700
|
+
return {
|
|
11701
|
+
requested: true,
|
|
11702
|
+
status: 400,
|
|
11703
|
+
error: `Admin version repair may only update projections.timeline; remove: ${forbiddenKeys.join(", ")}`,
|
|
11704
|
+
};
|
|
11705
|
+
}
|
|
11706
|
+
const projections = payload.projections;
|
|
11707
|
+
if (!projections || typeof projections !== "object" || Array.isArray(projections) || !Array.isArray(projections.timeline)) {
|
|
11708
|
+
return { requested: true, status: 400, error: "Admin version repair requires projections.timeline" };
|
|
11709
|
+
}
|
|
11710
|
+
const extraProjectionKeys = Object.keys(projections).filter((key) => key !== "timeline");
|
|
11711
|
+
if (extraProjectionKeys.length) {
|
|
11712
|
+
return { requested: true, status: 400, error: "Admin version repair may only update projections.timeline" };
|
|
11713
|
+
}
|
|
11714
|
+
const nonVersionEntry = report?.projections?.timeline?.find((item) => String(item?.kind || "").trim().toLowerCase() !== "version");
|
|
11715
|
+
if (nonVersionEntry) {
|
|
11716
|
+
return { requested: true, status: 400, error: "Admin version repair only accepts timeline entries with kind=version" };
|
|
11717
|
+
}
|
|
11718
|
+
if (!report.idempotencyKey) {
|
|
11719
|
+
return { requested: true, status: 400, error: "Admin version repair requires idempotencyKey" };
|
|
11720
|
+
}
|
|
11721
|
+
if (!report.expectedRevision) {
|
|
11722
|
+
return { requested: true, status: 400, error: "Admin version repair requires expectedRevision" };
|
|
11723
|
+
}
|
|
11724
|
+
return intent;
|
|
11725
|
+
}
|
|
11726
|
+
|
|
11727
|
+
function prdWorkflowMergeAdminVersionTimeline(report, currentSnapshot = {}) {
|
|
11728
|
+
const source = prdWorkflowRuntimeEventProducer(report?.event || {});
|
|
11729
|
+
const current = Array.isArray(currentSnapshot?.projections?.timeline) ? currentSnapshot.projections.timeline : [];
|
|
11730
|
+
const incoming = Array.isArray(report?.projections?.timeline) ? report.projections.timeline : [];
|
|
11731
|
+
const retained = current.filter((item) => (
|
|
11732
|
+
prdWorkflowRuntimeEventProducer(item) !== source
|
|
11733
|
+
|| String(item?.kind || "").trim().toLowerCase() !== "version"
|
|
11734
|
+
));
|
|
11735
|
+
const timeline = [...retained, ...incoming];
|
|
11736
|
+
const administrativeRepair = {
|
|
11737
|
+
kind: "version-attribution",
|
|
11738
|
+
operation: "repair-version-membership",
|
|
11739
|
+
};
|
|
11740
|
+
return {
|
|
11741
|
+
...report,
|
|
11742
|
+
projections: { ...report.projections, timeline },
|
|
11743
|
+
event: {
|
|
11744
|
+
...report.event,
|
|
11745
|
+
projections: { ...report.event.projections, timeline },
|
|
11746
|
+
administrativeRepair,
|
|
11747
|
+
administrative_repair: administrativeRepair,
|
|
11748
|
+
},
|
|
11749
|
+
};
|
|
11750
|
+
}
|
|
11751
|
+
|
|
11555
11752
|
function prdWorkflowRuntimeEventDedupeKey(event = {}, index = 0) {
|
|
11556
11753
|
const producer = prdWorkflowRuntimeEventProducer(event);
|
|
11557
11754
|
const operation = prdWorkflowRuntimeEventOperation(event);
|
|
@@ -11838,6 +12035,136 @@ function prdWorkflowGlobalStateFromEvents(tapdId, snapshot = {}, runtimeEvents =
|
|
|
11838
12035
|
return state;
|
|
11839
12036
|
}
|
|
11840
12037
|
|
|
12038
|
+
function prdWorkflowChecklistActionKey(action = {}) {
|
|
12039
|
+
return String(
|
|
12040
|
+
action?.actionModel?.key ||
|
|
12041
|
+
action?.key ||
|
|
12042
|
+
action?.actionKey ||
|
|
12043
|
+
action?.action_key ||
|
|
12044
|
+
action?.action ||
|
|
12045
|
+
action?.actionId ||
|
|
12046
|
+
action?.action_id ||
|
|
12047
|
+
action?.stageKey ||
|
|
12048
|
+
action?.stage_key ||
|
|
12049
|
+
"",
|
|
12050
|
+
).trim();
|
|
12051
|
+
}
|
|
12052
|
+
|
|
12053
|
+
function prdWorkflowChecklistResourceKey(producer, actionKey, itemKey) {
|
|
12054
|
+
return `checklist:${String(producer || "").trim().toLowerCase()}:${String(actionKey || "").trim()}:${String(itemKey || "").trim()}`;
|
|
12055
|
+
}
|
|
12056
|
+
|
|
12057
|
+
function prdWorkflowChecklistStateEntries(runtimeEvents = []) {
|
|
12058
|
+
const states = new Map();
|
|
12059
|
+
for (const event of Array.isArray(runtimeEvents) ? runtimeEvents : []) {
|
|
12060
|
+
const state = event?.checklistState || event?.checklist_state;
|
|
12061
|
+
if (!state || typeof state !== "object" || Array.isArray(state)) continue;
|
|
12062
|
+
const producer = String(state.producer || state.source || "").trim().toLowerCase();
|
|
12063
|
+
const actionKey = String(state.actionKey || state.action_key || "").trim();
|
|
12064
|
+
const itemKey = String(state.itemKey || state.item_key || "").trim();
|
|
12065
|
+
if (!producer || !actionKey || !itemKey) continue;
|
|
12066
|
+
const resourceKey = prdWorkflowChecklistResourceKey(producer, actionKey, itemKey);
|
|
12067
|
+
const version = workflowSnapshotResourceVersions({ runtimeEvents: [event] })[resourceKey] || "absent";
|
|
12068
|
+
states.set(resourceKey, { ...state, producer, actionKey, itemKey, resourceKey, version });
|
|
12069
|
+
}
|
|
12070
|
+
return states;
|
|
12071
|
+
}
|
|
12072
|
+
|
|
12073
|
+
function prdWorkflowMaterializeChecklists(snapshot = {}, runtimeEvents = []) {
|
|
12074
|
+
const actionSources = new Map();
|
|
12075
|
+
for (const event of Array.isArray(runtimeEvents) ? runtimeEvents : []) {
|
|
12076
|
+
const actionKey = prdWorkflowChecklistActionKey(event);
|
|
12077
|
+
const producer = String(event?.source || event?.producer || "").trim().toLowerCase();
|
|
12078
|
+
if (!actionKey || !producer || (!event?.checklist && !event?.actionModel?.checklist)) continue;
|
|
12079
|
+
const existing = actionSources.get(actionKey);
|
|
12080
|
+
actionSources.set(actionKey, existing && existing !== producer ? "" : producer);
|
|
12081
|
+
}
|
|
12082
|
+
const stateEntries = prdWorkflowChecklistStateEntries(runtimeEvents);
|
|
12083
|
+
const terminalStatuses = new Set(["passed", "skipped"]);
|
|
12084
|
+
const decorate = (action) => {
|
|
12085
|
+
if (!action || typeof action !== "object" || Array.isArray(action)) return action;
|
|
12086
|
+
const definition = action.checklist || action.actionModel?.checklist;
|
|
12087
|
+
if (!definition || typeof definition !== "object" || Array.isArray(definition)) return action;
|
|
12088
|
+
const actionKey = prdWorkflowChecklistActionKey(action);
|
|
12089
|
+
const producer = String(
|
|
12090
|
+
definition.source || action.source || action.producer || action.actionModel?.source || actionSources.get(actionKey) || "",
|
|
12091
|
+
).trim().toLowerCase();
|
|
12092
|
+
const items = (Array.isArray(definition.items) ? definition.items : []).map((item) => {
|
|
12093
|
+
const itemKey = String(item?.key || item?.id || "").trim();
|
|
12094
|
+
const resourceKey = producer && actionKey && itemKey
|
|
12095
|
+
? prdWorkflowChecklistResourceKey(producer, actionKey, itemKey)
|
|
12096
|
+
: "";
|
|
12097
|
+
const stored = resourceKey ? stateEntries.get(resourceKey) : null;
|
|
12098
|
+
const state = stored || {
|
|
12099
|
+
producer,
|
|
12100
|
+
actionKey,
|
|
12101
|
+
itemKey,
|
|
12102
|
+
status: "pending",
|
|
12103
|
+
note: "",
|
|
12104
|
+
evidence: [],
|
|
12105
|
+
resourceKey,
|
|
12106
|
+
version: "absent",
|
|
12107
|
+
};
|
|
12108
|
+
return { ...item, state };
|
|
12109
|
+
});
|
|
12110
|
+
const required = items.filter((item) => item.required !== false);
|
|
12111
|
+
const completed = items.filter((item) => terminalStatuses.has(String(item?.state?.status || "pending"))).length;
|
|
12112
|
+
const requiredCompleted = required.filter((item) => terminalStatuses.has(String(item?.state?.status || "pending"))).length;
|
|
12113
|
+
const completionPolicy = String(definition.completionPolicy || definition.completion_policy || "all_required").trim().toLowerCase();
|
|
12114
|
+
const completionCandidates = required.length ? required : items;
|
|
12115
|
+
const ready = completionPolicy === "manual"
|
|
12116
|
+
? false
|
|
12117
|
+
: completionPolicy === "any_required"
|
|
12118
|
+
? completionCandidates.some((item) => terminalStatuses.has(String(item?.state?.status || "pending")))
|
|
12119
|
+
: completionCandidates.length > 0 && completionCandidates.every((item) => terminalStatuses.has(String(item?.state?.status || "pending")));
|
|
12120
|
+
const checklist = {
|
|
12121
|
+
...definition,
|
|
12122
|
+
source: producer,
|
|
12123
|
+
items,
|
|
12124
|
+
progress: {
|
|
12125
|
+
total: items.length,
|
|
12126
|
+
completed,
|
|
12127
|
+
required: required.length,
|
|
12128
|
+
requiredCompleted,
|
|
12129
|
+
percent: items.length ? Math.round((completed / items.length) * 100) : 0,
|
|
12130
|
+
ready,
|
|
12131
|
+
},
|
|
12132
|
+
};
|
|
12133
|
+
return {
|
|
12134
|
+
...action,
|
|
12135
|
+
checklist,
|
|
12136
|
+
...(action.actionModel && typeof action.actionModel === "object" && !Array.isArray(action.actionModel)
|
|
12137
|
+
? { actionModel: { ...action.actionModel, checklist } }
|
|
12138
|
+
: {}),
|
|
12139
|
+
};
|
|
12140
|
+
};
|
|
12141
|
+
const out = { ...snapshot };
|
|
12142
|
+
for (const key of ["actions", "workflowActions", "workflow_actions", "timeline", "history", "events", "runtimeEvents", "runtime_events"]) {
|
|
12143
|
+
if (Array.isArray(snapshot?.[key])) out[key] = snapshot[key].map(decorate);
|
|
12144
|
+
}
|
|
12145
|
+
out.checklistStates = [...stateEntries.values()];
|
|
12146
|
+
return out;
|
|
12147
|
+
}
|
|
12148
|
+
|
|
12149
|
+
function prdWorkflowFindChecklistAction(snapshot = {}, producer = "", actionKey = "") {
|
|
12150
|
+
const wantedProducer = String(producer || "").trim().toLowerCase();
|
|
12151
|
+
const wantedActionKey = String(actionKey || "").trim();
|
|
12152
|
+
const matches = [];
|
|
12153
|
+
for (const key of ["actions", "workflowActions", "workflow_actions", "timeline", "history", "events", "runtimeEvents", "runtime_events"]) {
|
|
12154
|
+
for (const action of Array.isArray(snapshot?.[key]) ? snapshot[key] : []) {
|
|
12155
|
+
if (!action || typeof action !== "object" || Array.isArray(action)) continue;
|
|
12156
|
+
const checklist = action.checklist || action.actionModel?.checklist;
|
|
12157
|
+
if (!checklist || typeof checklist !== "object" || Array.isArray(checklist)) continue;
|
|
12158
|
+
const resolvedActionKey = prdWorkflowChecklistActionKey(action);
|
|
12159
|
+
const resolvedProducer = String(checklist.source || action.source || action.producer || "").trim().toLowerCase();
|
|
12160
|
+
if (resolvedActionKey !== wantedActionKey) continue;
|
|
12161
|
+
if (wantedProducer && resolvedProducer !== wantedProducer) continue;
|
|
12162
|
+
matches.push({ ...action, checklist, source: resolvedProducer || wantedProducer });
|
|
12163
|
+
}
|
|
12164
|
+
}
|
|
12165
|
+
return matches.at(-1) || null;
|
|
12166
|
+
}
|
|
12167
|
+
|
|
11841
12168
|
function prdWorkflowMergeRuntimeEvents(scopedRoot, tapdId, snapshot) {
|
|
11842
12169
|
const runtime = prdWorkflowReadRuntimeEvents(scopedRoot, tapdId);
|
|
11843
12170
|
const runtimeEvents = runtime.events;
|
|
@@ -11854,7 +12181,7 @@ function prdWorkflowMergeRuntimeEvents(scopedRoot, tapdId, snapshot) {
|
|
|
11854
12181
|
for (const key of ["issues", "issueGroups", "issue_groups", "epics", "epicGroups", "epic_groups", "aiDocs", "ai_docs"]) {
|
|
11855
12182
|
if (Object.prototype.hasOwnProperty.call(prdFlowExtension, key)) prdFlowExtensionView[key] = prdFlowExtension[key];
|
|
11856
12183
|
}
|
|
11857
|
-
|
|
12184
|
+
let materialized = {
|
|
11858
12185
|
...snapshot,
|
|
11859
12186
|
...prdFlowExtensionView,
|
|
11860
12187
|
workflow: globalState.workflow,
|
|
@@ -11871,6 +12198,7 @@ function prdWorkflowMergeRuntimeEvents(scopedRoot, tapdId, snapshot) {
|
|
|
11871
12198
|
runtimeEventsUpdatedAt: runtime.updatedAt || "",
|
|
11872
12199
|
},
|
|
11873
12200
|
};
|
|
12201
|
+
materialized = prdWorkflowMaterializeChecklists(materialized, runtimeEvents);
|
|
11874
12202
|
materialized.resourceVersions = workflowSnapshotResourceVersions(materialized);
|
|
11875
12203
|
return materialized;
|
|
11876
12204
|
}
|
|
@@ -13380,7 +13708,18 @@ export function startUiServer({
|
|
|
13380
13708
|
? getTeamById(requestedTeamId)
|
|
13381
13709
|
: getTeamForUser(userCtx.userId);
|
|
13382
13710
|
if (!team || team.status !== "active") {
|
|
13383
|
-
json(res, 200, {
|
|
13711
|
+
json(res, 200, {
|
|
13712
|
+
ok: true,
|
|
13713
|
+
view: "team",
|
|
13714
|
+
team: null,
|
|
13715
|
+
workflows: [],
|
|
13716
|
+
timeline: [],
|
|
13717
|
+
unassignedCount: 0,
|
|
13718
|
+
availableCount: 0,
|
|
13719
|
+
selectedTimelineKey: "all",
|
|
13720
|
+
defaultTimelineKey: "all",
|
|
13721
|
+
pagination: { page: 1, pageSize: 20, total: 0, totalPages: 1, hasPrevious: false, hasNext: false },
|
|
13722
|
+
});
|
|
13384
13723
|
return;
|
|
13385
13724
|
}
|
|
13386
13725
|
records = listPrdWorkflowCollaborationsForTeam(team.id);
|
|
@@ -13400,11 +13739,19 @@ export function startUiServer({
|
|
|
13400
13739
|
return prdWorkflowDashboardSummary(record, materialized, userCtx, projectBindings);
|
|
13401
13740
|
});
|
|
13402
13741
|
const dashboardTimeline = prdWorkflowDashboardTimeline(workflows);
|
|
13742
|
+
const dashboardPage = prdWorkflowDashboardPage(workflows, dashboardTimeline, {
|
|
13743
|
+
timelineKey: url.searchParams.has("timelineKey") ? url.searchParams.get("timelineKey") : "",
|
|
13744
|
+
query: url.searchParams.get("q"),
|
|
13745
|
+
scope: url.searchParams.get("scope"),
|
|
13746
|
+
state: url.searchParams.get("state"),
|
|
13747
|
+
page: url.searchParams.get("page"),
|
|
13748
|
+
pageSize: url.searchParams.get("pageSize"),
|
|
13749
|
+
});
|
|
13403
13750
|
json(res, 200, {
|
|
13404
13751
|
ok: true,
|
|
13405
13752
|
view: view === "team" ? "team" : "personal",
|
|
13406
13753
|
team: teamSummaryWithUsers(team),
|
|
13407
|
-
|
|
13754
|
+
...dashboardPage,
|
|
13408
13755
|
...dashboardTimeline,
|
|
13409
13756
|
});
|
|
13410
13757
|
} catch (error) {
|
|
@@ -14016,6 +14363,14 @@ export function startUiServer({
|
|
|
14016
14363
|
}
|
|
14017
14364
|
const flowId = String(url.searchParams.get("flowId") || "").trim();
|
|
14018
14365
|
const flowSource = String(url.searchParams.get("flowSource") || "user").trim() || "user";
|
|
14366
|
+
const adminVersionRepair = prdWorkflowAdminVersionRepairOperation(
|
|
14367
|
+
url.searchParams.get("adminOperation") || url.searchParams.get("admin_operation") || "",
|
|
14368
|
+
userCtx,
|
|
14369
|
+
);
|
|
14370
|
+
if (adminVersionRepair.error) {
|
|
14371
|
+
json(res, adminVersionRepair.status || 400, { error: adminVersionRepair.error });
|
|
14372
|
+
return;
|
|
14373
|
+
}
|
|
14019
14374
|
const workflowScope = resolvePrdWorkflowScope(root, {
|
|
14020
14375
|
tapdId: workflow.id,
|
|
14021
14376
|
flowId,
|
|
@@ -14023,14 +14378,15 @@ export function startUiServer({
|
|
|
14023
14378
|
archived: url.searchParams.get("archived") === "1",
|
|
14024
14379
|
workspaceId: url.searchParams.get("workspaceId") || "",
|
|
14025
14380
|
workflowShare: url.searchParams.get("workflowShare") || "",
|
|
14026
|
-
}, userCtx);
|
|
14381
|
+
}, userCtx, adminVersionRepair.requested ? "admin-version-repair" : "read");
|
|
14027
14382
|
if (workflowScope.error) {
|
|
14028
14383
|
json(res, workflowScope.status || 400, { error: workflowScope.error });
|
|
14029
14384
|
return;
|
|
14030
14385
|
}
|
|
14031
14386
|
const scopedRoot = workflowScope.stateRoot;
|
|
14032
14387
|
prdWorkflowMigrateLegacyState(workflowScope.executionRoot, scopedRoot, workflow.id);
|
|
14033
|
-
const runtimeOnly =
|
|
14388
|
+
const runtimeOnly = adminVersionRepair.requested ||
|
|
14389
|
+
url.searchParams.get("runtimeOnly") === "1" ||
|
|
14034
14390
|
url.searchParams.get("runtime_only") === "1" ||
|
|
14035
14391
|
url.searchParams.get("cached") === "1";
|
|
14036
14392
|
const baseSnapshot = runtimeOnly
|
|
@@ -14046,6 +14402,276 @@ export function startUiServer({
|
|
|
14046
14402
|
}
|
|
14047
14403
|
return;
|
|
14048
14404
|
}
|
|
14405
|
+
if (req.method === "GET" && url.pathname === "/api/workflows/checklist") {
|
|
14406
|
+
try {
|
|
14407
|
+
const workflow = normalizeWorkflowReference({ workflow: url.searchParams.get("workflow") || "" });
|
|
14408
|
+
if (workflow.error) {
|
|
14409
|
+
json(res, 400, { error: workflow.error });
|
|
14410
|
+
return;
|
|
14411
|
+
}
|
|
14412
|
+
if (workflow.namespace !== "tapd") {
|
|
14413
|
+
json(res, 400, { error: `Unsupported workflow namespace: ${workflow.namespace}` });
|
|
14414
|
+
return;
|
|
14415
|
+
}
|
|
14416
|
+
const source = String(url.searchParams.get("source") || "").trim().toLowerCase();
|
|
14417
|
+
const actionKey = String(url.searchParams.get("actionKey") || url.searchParams.get("action_key") || "").trim();
|
|
14418
|
+
if (!/^[a-z][a-z0-9._-]{0,119}$/.test(source)) {
|
|
14419
|
+
json(res, 400, { error: "Invalid checklist source" });
|
|
14420
|
+
return;
|
|
14421
|
+
}
|
|
14422
|
+
if (!actionKey || actionKey.length > 240 || /[\0\r\n]/.test(actionKey)) {
|
|
14423
|
+
json(res, 400, { error: "Invalid checklist actionKey" });
|
|
14424
|
+
return;
|
|
14425
|
+
}
|
|
14426
|
+
const flowId = String(url.searchParams.get("flowId") || "").trim();
|
|
14427
|
+
const flowSource = String(url.searchParams.get("flowSource") || "user").trim() || "user";
|
|
14428
|
+
const workflowScope = resolvePrdWorkflowScope(root, {
|
|
14429
|
+
tapdId: workflow.id,
|
|
14430
|
+
flowId,
|
|
14431
|
+
flowSource,
|
|
14432
|
+
archived: url.searchParams.get("archived") === "1",
|
|
14433
|
+
workspaceId: url.searchParams.get("workspaceId") || "",
|
|
14434
|
+
workflowShare: url.searchParams.get("workflowShare") || "",
|
|
14435
|
+
}, userCtx, "read");
|
|
14436
|
+
if (workflowScope.error) {
|
|
14437
|
+
json(res, workflowScope.status || 400, { error: workflowScope.error });
|
|
14438
|
+
return;
|
|
14439
|
+
}
|
|
14440
|
+
prdWorkflowMigrateLegacyState(workflowScope.executionRoot, workflowScope.stateRoot, workflow.id);
|
|
14441
|
+
const snapshot = prdWorkflowMaterializeSnapshot(
|
|
14442
|
+
workflowScope.executionRoot,
|
|
14443
|
+
workflowScope.stateRoot,
|
|
14444
|
+
workflow.id,
|
|
14445
|
+
userCtx,
|
|
14446
|
+
{ flowSource, flowId },
|
|
14447
|
+
);
|
|
14448
|
+
const action = prdWorkflowFindChecklistAction(snapshot, source, actionKey);
|
|
14449
|
+
if (!action) {
|
|
14450
|
+
json(res, 404, { error: "Workflow Action checklist not found" });
|
|
14451
|
+
return;
|
|
14452
|
+
}
|
|
14453
|
+
const access = workflowScope.collaborationAccess || {};
|
|
14454
|
+
const canWrite = Boolean(authUser?.userId) && !workflowScope.sharedByLink && !workflowScope.adminReadonly && (
|
|
14455
|
+
workflowScope.collaboration ? access.writable === true : true
|
|
14456
|
+
);
|
|
14457
|
+
json(res, 200, {
|
|
14458
|
+
ok: true,
|
|
14459
|
+
workflow,
|
|
14460
|
+
action: {
|
|
14461
|
+
key: actionKey,
|
|
14462
|
+
source,
|
|
14463
|
+
title: String(action.title || action.label || actionKey),
|
|
14464
|
+
status: String(action.status || "pending"),
|
|
14465
|
+
checklist: action.checklist,
|
|
14466
|
+
},
|
|
14467
|
+
canWrite,
|
|
14468
|
+
});
|
|
14469
|
+
} catch (error) {
|
|
14470
|
+
json(res, 500, { error: (error && error.message) || String(error) });
|
|
14471
|
+
}
|
|
14472
|
+
return;
|
|
14473
|
+
}
|
|
14474
|
+
if (req.method === "PATCH" && url.pathname === "/api/workflows/checklist") {
|
|
14475
|
+
if (!authUser?.userId) {
|
|
14476
|
+
json(res, 401, { error: "Authentication required" });
|
|
14477
|
+
return;
|
|
14478
|
+
}
|
|
14479
|
+
let payload;
|
|
14480
|
+
try {
|
|
14481
|
+
payload = JSON.parse(await readBody(req, 1024 * 1024));
|
|
14482
|
+
} catch (error) {
|
|
14483
|
+
json(res, error?.status === 413 ? 413 : 400, { error: error?.status === 413 ? error.message : "Invalid JSON body" });
|
|
14484
|
+
return;
|
|
14485
|
+
}
|
|
14486
|
+
let releaseWorkflowWriteLock = null;
|
|
14487
|
+
try {
|
|
14488
|
+
const workflow = normalizeWorkflowReference(payload);
|
|
14489
|
+
if (workflow.error) {
|
|
14490
|
+
json(res, 400, { error: workflow.error });
|
|
14491
|
+
return;
|
|
14492
|
+
}
|
|
14493
|
+
if (workflow.namespace !== "tapd") {
|
|
14494
|
+
json(res, 400, { error: `Unsupported workflow namespace: ${workflow.namespace}` });
|
|
14495
|
+
return;
|
|
14496
|
+
}
|
|
14497
|
+
const source = String(payload.source || "").trim().toLowerCase();
|
|
14498
|
+
const actionKey = String(payload.actionKey || payload.action_key || "").trim();
|
|
14499
|
+
const itemKey = String(payload.itemKey || payload.item_key || "").trim();
|
|
14500
|
+
if (!/^[a-z][a-z0-9._-]{0,119}$/.test(source)) {
|
|
14501
|
+
json(res, 400, { error: "Invalid checklist source" });
|
|
14502
|
+
return;
|
|
14503
|
+
}
|
|
14504
|
+
if (!actionKey || actionKey.length > 240 || /[\0\r\n]/.test(actionKey)) {
|
|
14505
|
+
json(res, 400, { error: "Invalid checklist actionKey" });
|
|
14506
|
+
return;
|
|
14507
|
+
}
|
|
14508
|
+
if (!itemKey || itemKey.length > 240 || /[\0\r\n]/.test(itemKey)) {
|
|
14509
|
+
json(res, 400, { error: "Invalid checklist itemKey" });
|
|
14510
|
+
return;
|
|
14511
|
+
}
|
|
14512
|
+
const rawStatus = String(payload.status || "pending").trim().toLowerCase();
|
|
14513
|
+
if (!["pending", "passed", "failed", "blocked", "skipped", "done", "complete", "completed", "success", "error", "cancelled", "canceled"].includes(rawStatus)) {
|
|
14514
|
+
json(res, 400, { error: `Invalid checklist item status: ${rawStatus}` });
|
|
14515
|
+
return;
|
|
14516
|
+
}
|
|
14517
|
+
const status = normalizeWorkflowChecklistItemStatus(rawStatus);
|
|
14518
|
+
const note = String(payload.note || "").trim();
|
|
14519
|
+
if (note.length > 4000) {
|
|
14520
|
+
json(res, 400, { error: "Checklist note exceeds 4000 characters" });
|
|
14521
|
+
return;
|
|
14522
|
+
}
|
|
14523
|
+
const rawEvidence = Array.isArray(payload.evidence) ? payload.evidence : [];
|
|
14524
|
+
if (rawEvidence.length > 20) {
|
|
14525
|
+
json(res, 400, { error: "Checklist evidence supports at most 20 entries" });
|
|
14526
|
+
return;
|
|
14527
|
+
}
|
|
14528
|
+
const evidence = [];
|
|
14529
|
+
for (let index = 0; index < rawEvidence.length; index += 1) {
|
|
14530
|
+
const item = rawEvidence[index];
|
|
14531
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
14532
|
+
json(res, 400, { error: `evidence[${index}] must be an object` });
|
|
14533
|
+
return;
|
|
14534
|
+
}
|
|
14535
|
+
const evidenceUrl = String(item.url || item.href || "").trim();
|
|
14536
|
+
if (!evidenceUrl || evidenceUrl.length > 4000 || !isSafeWorkflowUrl(evidenceUrl)) {
|
|
14537
|
+
json(res, 400, { error: `evidence[${index}].url must use http, https, or an absolute application path` });
|
|
14538
|
+
return;
|
|
14539
|
+
}
|
|
14540
|
+
evidence.push({
|
|
14541
|
+
title: String(item.title || item.label || `证据 ${index + 1}`).trim().slice(0, 500),
|
|
14542
|
+
url: evidenceUrl,
|
|
14543
|
+
});
|
|
14544
|
+
}
|
|
14545
|
+
const expectedVersion = String(payload.expectedVersion || payload.expected_version || "").trim();
|
|
14546
|
+
if (!expectedVersion) {
|
|
14547
|
+
json(res, 400, { error: "Checklist update requires expectedVersion" });
|
|
14548
|
+
return;
|
|
14549
|
+
}
|
|
14550
|
+
const flowId = String(payload.flowId || payload.flow_id || "").trim();
|
|
14551
|
+
const flowSource = String(payload.flowSource || payload.flow_source || "user").trim() || "user";
|
|
14552
|
+
const workflowScope = resolvePrdWorkflowScope(root, {
|
|
14553
|
+
...payload,
|
|
14554
|
+
tapdId: workflow.id,
|
|
14555
|
+
flowId,
|
|
14556
|
+
flowSource,
|
|
14557
|
+
}, userCtx, "write");
|
|
14558
|
+
if (workflowScope.error) {
|
|
14559
|
+
json(res, workflowScope.status || 400, { error: workflowScope.error });
|
|
14560
|
+
return;
|
|
14561
|
+
}
|
|
14562
|
+
if (!workflowScope.collaboration) {
|
|
14563
|
+
const ensured = ensurePrdWorkflowCollaboration({ tapdId: workflow.id, userId: userCtx.userId });
|
|
14564
|
+
if (ensured.error) {
|
|
14565
|
+
json(res, ensured.status || 400, { error: ensured.error });
|
|
14566
|
+
return;
|
|
14567
|
+
}
|
|
14568
|
+
}
|
|
14569
|
+
const scopedRoot = workflowScope.stateRoot;
|
|
14570
|
+
prdWorkflowMigrateLegacyState(workflowScope.executionRoot, scopedRoot, workflow.id);
|
|
14571
|
+
releaseWorkflowWriteLock = await prdWorkflowAcquireWriteLock(`${scopedRoot}\t${workflow.id}`);
|
|
14572
|
+
let snapshot = prdWorkflowMaterializeSnapshot(
|
|
14573
|
+
workflowScope.executionRoot,
|
|
14574
|
+
scopedRoot,
|
|
14575
|
+
workflow.id,
|
|
14576
|
+
userCtx,
|
|
14577
|
+
{ flowSource, flowId },
|
|
14578
|
+
);
|
|
14579
|
+
const idempotencyKey = String(payload.idempotencyKey || payload.idempotency_key || "").trim().slice(0, 500);
|
|
14580
|
+
if (idempotencyKey) {
|
|
14581
|
+
const existing = prdWorkflowFindIdempotencyEvent(scopedRoot, workflow.id, idempotencyKey, "agentflow-checklist", false, "checklist.update");
|
|
14582
|
+
if (existing) {
|
|
14583
|
+
json(res, 200, { ok: true, alreadyApplied: true, workflow, checklistState: existing.checklistState, snapshot });
|
|
14584
|
+
return;
|
|
14585
|
+
}
|
|
14586
|
+
}
|
|
14587
|
+
const action = prdWorkflowFindChecklistAction(snapshot, source, actionKey);
|
|
14588
|
+
const checklistItem = action?.checklist?.items?.find((item) => String(item?.key || "") === itemKey);
|
|
14589
|
+
if (!action || !checklistItem) {
|
|
14590
|
+
json(res, 404, { error: "Workflow Action checklist item not found" });
|
|
14591
|
+
return;
|
|
14592
|
+
}
|
|
14593
|
+
if (status === "passed" && checklistItem.evidenceRequired === true && evidence.length === 0) {
|
|
14594
|
+
json(res, 400, { error: "Checklist item requires evidence before it can pass" });
|
|
14595
|
+
return;
|
|
14596
|
+
}
|
|
14597
|
+
const resourceKey = prdWorkflowChecklistResourceKey(source, actionKey, itemKey);
|
|
14598
|
+
const currentVersion = String(snapshot.resourceVersions?.[resourceKey] || "absent");
|
|
14599
|
+
if (expectedVersion !== currentVersion) {
|
|
14600
|
+
json(res, 409, {
|
|
14601
|
+
error: "Checklist item changed; refresh it before saving",
|
|
14602
|
+
conflict: { type: "workflow-resource-conflict", conflicts: [{ resourceKey, expectedVersion, currentVersion }], workflow },
|
|
14603
|
+
snapshot,
|
|
14604
|
+
});
|
|
14605
|
+
return;
|
|
14606
|
+
}
|
|
14607
|
+
const now = new Date().toISOString();
|
|
14608
|
+
const checklistState = {
|
|
14609
|
+
producer: source,
|
|
14610
|
+
actionKey,
|
|
14611
|
+
itemKey,
|
|
14612
|
+
status,
|
|
14613
|
+
note,
|
|
14614
|
+
evidence,
|
|
14615
|
+
updatedAt: now,
|
|
14616
|
+
updatedBy: {
|
|
14617
|
+
userId: String(userCtx.userId || ""),
|
|
14618
|
+
username: String(authUser.username || userCtx.userId || ""),
|
|
14619
|
+
},
|
|
14620
|
+
};
|
|
14621
|
+
const event = prdWorkflowAppendRuntimeEvent(scopedRoot, workflow.id, {
|
|
14622
|
+
id: `checklist_state_${prdWorkflowSafeStateId([source, actionKey, itemKey].join(":"))}`,
|
|
14623
|
+
type: "workflow-checklist-update",
|
|
14624
|
+
operation: "checklist.update",
|
|
14625
|
+
source: "agentflow-checklist",
|
|
14626
|
+
auxiliary: true,
|
|
14627
|
+
aggregateByStage: false,
|
|
14628
|
+
status: "done",
|
|
14629
|
+
checklistState,
|
|
14630
|
+
...(idempotencyKey ? { idempotencyKey } : {}),
|
|
14631
|
+
});
|
|
14632
|
+
if (!event) throw new Error("Failed to store checklist state");
|
|
14633
|
+
snapshot = prdWorkflowMaterializeSnapshot(
|
|
14634
|
+
workflowScope.executionRoot,
|
|
14635
|
+
scopedRoot,
|
|
14636
|
+
workflow.id,
|
|
14637
|
+
userCtx,
|
|
14638
|
+
{ flowSource, flowId },
|
|
14639
|
+
);
|
|
14640
|
+
const updatedAction = prdWorkflowFindChecklistAction(snapshot, source, actionKey);
|
|
14641
|
+
const updatedItem = updatedAction?.checklist?.items?.find((item) => String(item?.key || "") === itemKey);
|
|
14642
|
+
prdWorkflowAppendAudit(scopedRoot, workflow.id, {
|
|
14643
|
+
type: "checklist-item-updated",
|
|
14644
|
+
source,
|
|
14645
|
+
actionKey,
|
|
14646
|
+
itemKey,
|
|
14647
|
+
status,
|
|
14648
|
+
resourceKey,
|
|
14649
|
+
actorUserId: String(userCtx.userId || ""),
|
|
14650
|
+
});
|
|
14651
|
+
prdWorkflowBroadcast(prdWorkflowKey(userCtx, flowSource, flowId, workflow.id), {
|
|
14652
|
+
type: "workflow-checklist-updated",
|
|
14653
|
+
tapdId: workflow.id,
|
|
14654
|
+
source,
|
|
14655
|
+
actionKey,
|
|
14656
|
+
itemKey,
|
|
14657
|
+
checklistState: updatedItem?.state || checklistState,
|
|
14658
|
+
snapshot,
|
|
14659
|
+
});
|
|
14660
|
+
json(res, 200, {
|
|
14661
|
+
ok: true,
|
|
14662
|
+
alreadyApplied: false,
|
|
14663
|
+
workflow,
|
|
14664
|
+
checklistState: updatedItem?.state || checklistState,
|
|
14665
|
+
checklist: updatedAction?.checklist || null,
|
|
14666
|
+
snapshot,
|
|
14667
|
+
});
|
|
14668
|
+
} catch (error) {
|
|
14669
|
+
json(res, 500, { error: (error && error.message) || String(error) });
|
|
14670
|
+
} finally {
|
|
14671
|
+
releaseWorkflowWriteLock?.();
|
|
14672
|
+
}
|
|
14673
|
+
return;
|
|
14674
|
+
}
|
|
14049
14675
|
if (req.method === "GET" && url.pathname === "/api/prd-workflow/snapshot") {
|
|
14050
14676
|
try {
|
|
14051
14677
|
const tapdId = String(url.searchParams.get("tapdId") || "").trim();
|
|
@@ -14867,6 +15493,11 @@ export function startUiServer({
|
|
|
14867
15493
|
json(res, 400, { error: `Unsupported workflow namespace: ${report.workflow.namespace}` });
|
|
14868
15494
|
return;
|
|
14869
15495
|
}
|
|
15496
|
+
const adminVersionRepair = prdWorkflowAdminVersionRepairIntent(payload, report, userCtx);
|
|
15497
|
+
if (adminVersionRepair.error) {
|
|
15498
|
+
json(res, adminVersionRepair.status || 400, { error: adminVersionRepair.error });
|
|
15499
|
+
return;
|
|
15500
|
+
}
|
|
14870
15501
|
const tapdId = report.workflow.id;
|
|
14871
15502
|
const flowId = report.flowId;
|
|
14872
15503
|
const flowSource = report.flowSource || "user";
|
|
@@ -14877,12 +15508,12 @@ export function startUiServer({
|
|
|
14877
15508
|
flowId,
|
|
14878
15509
|
flowSource,
|
|
14879
15510
|
archived,
|
|
14880
|
-
}, userCtx, "write");
|
|
15511
|
+
}, userCtx, adminVersionRepair.requested ? "admin-version-repair" : "write");
|
|
14881
15512
|
if (workflowScope.error) {
|
|
14882
15513
|
json(res, workflowScope.status || 400, { error: workflowScope.error });
|
|
14883
15514
|
return;
|
|
14884
15515
|
}
|
|
14885
|
-
if (!workflowScope.collaboration) {
|
|
15516
|
+
if (!workflowScope.collaboration && !adminVersionRepair.requested) {
|
|
14886
15517
|
const ensured = ensurePrdWorkflowCollaboration({ tapdId, userId: userCtx.userId });
|
|
14887
15518
|
if (ensured.error) {
|
|
14888
15519
|
json(res, ensured.status || 400, { error: ensured.error });
|
|
@@ -14990,7 +15621,9 @@ export function startUiServer({
|
|
|
14990
15621
|
});
|
|
14991
15622
|
return;
|
|
14992
15623
|
}
|
|
14993
|
-
report =
|
|
15624
|
+
report = adminVersionRepair.requested
|
|
15625
|
+
? prdWorkflowMergeAdminVersionTimeline(report, currentSnapshot)
|
|
15626
|
+
: prdWorkflowMergeProducerTimeline(report, currentSnapshot);
|
|
14994
15627
|
if (report.error) {
|
|
14995
15628
|
json(res, 400, { error: report.error });
|
|
14996
15629
|
return;
|
|
@@ -15037,11 +15670,17 @@ export function startUiServer({
|
|
|
15037
15670
|
getSessionTokenFromRequest(req) || "",
|
|
15038
15671
|
);
|
|
15039
15672
|
prdWorkflowBroadcast(
|
|
15040
|
-
prdWorkflowKey(
|
|
15673
|
+
prdWorkflowKey(
|
|
15674
|
+
adminVersionRepair.requested ? { userId: workflowScope.stateOwnerId } : userCtx,
|
|
15675
|
+
flowSource,
|
|
15676
|
+
flowId,
|
|
15677
|
+
tapdId,
|
|
15678
|
+
),
|
|
15041
15679
|
{ type: "workflow-report", tapdId, workflow: report.workflow, event, observation: Boolean(observation), snapshot },
|
|
15042
15680
|
);
|
|
15043
15681
|
json(res, 200, {
|
|
15044
15682
|
ok: true,
|
|
15683
|
+
...(adminVersionRepair.requested ? { administrativeRepair: report.event.administrativeRepair } : {}),
|
|
15045
15684
|
report,
|
|
15046
15685
|
resourceKeys,
|
|
15047
15686
|
event,
|