taskchef 7.0.0 → 7.2.0
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/.codex-plugin/plugin.json +2 -1
- package/README.md +35 -14
- package/docs/firstmate-taskchef-comparison.md +9 -7
- package/docs/spec.md +57 -28
- package/docs/workflows.md +42 -23
- package/index.js +3 -0
- package/package.json +2 -1
- package/skills/taskchef-bootstrap/SKILL.md +1 -1
- package/skills/taskchef-delegate/SKILL.md +12 -30
- package/skills/taskchef-executor/SKILL.md +69 -0
- package/skills/taskchef-executor/agents/openai.yaml +4 -0
- package/skills/taskchef-report/SKILL.md +14 -14
- package/src/cli.js +7 -3
- package/src/dashboard/app.js +8 -4
- package/src/dashboard/state.js +1 -0
- package/src/dashboard.js +2 -0
- package/src/delegation.js +18 -6
- package/src/mcp.js +38 -4
- package/src/workspace.js +289 -38
package/src/workspace.js
CHANGED
|
@@ -38,7 +38,8 @@ const DISPATCH_FILE_NAME = "tasks.jsonl";
|
|
|
38
38
|
const WORKSPACE_LOCK_NAME = ".taskchef-workspace.lock";
|
|
39
39
|
const SAFE_ID = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
40
40
|
const CURRENT_CONFIG_SCHEMA_VERSION = 2;
|
|
41
|
-
const CURRENT_TASK_SCHEMA_VERSION =
|
|
41
|
+
const CURRENT_TASK_SCHEMA_VERSION = 5;
|
|
42
|
+
const PREVIOUS_SELF_LINKING_TASK_SCHEMA_VERSION = 4;
|
|
42
43
|
const CONFIG_FIELDS = new Set(["schemaVersion", "projects"]);
|
|
43
44
|
const PROJECT_FIELDS = new Set([
|
|
44
45
|
"name",
|
|
@@ -48,7 +49,7 @@ const PROJECT_FIELDS = new Set([
|
|
|
48
49
|
"description",
|
|
49
50
|
]);
|
|
50
51
|
const PROJECT_INPUT_FIELDS = new Set(["name", "path", "githubRepos", "description"]);
|
|
51
|
-
const
|
|
52
|
+
const STATEFUL_DISPATCH_FIELDS = new Set([
|
|
52
53
|
"schemaVersion",
|
|
53
54
|
"id",
|
|
54
55
|
"project",
|
|
@@ -62,6 +63,7 @@ const DISPATCH_FIELDS = new Set([
|
|
|
62
63
|
"updatedAt",
|
|
63
64
|
"updatedBy",
|
|
64
65
|
]);
|
|
66
|
+
const DISPATCH_FIELDS = new Set([...STATEFUL_DISPATCH_FIELDS, "lastResult"]);
|
|
65
67
|
const RECORD_DISPATCH_FIELDS = new Set([
|
|
66
68
|
"id",
|
|
67
69
|
"project",
|
|
@@ -73,6 +75,7 @@ const RESULT_STATUSES = new Set(["needs_input", "completed", "failed"]);
|
|
|
73
75
|
const TASK_STATUSES = new Set(["working", ...RESULT_STATUSES]);
|
|
74
76
|
const TASK_UPDATE_SOURCES = new Set(["dispatcher", "mcp"]);
|
|
75
77
|
const MAX_RESULT_SUMMARY_LENGTH = 2_000;
|
|
78
|
+
const LAST_RESULT_FIELDS = new Set(["status", "summary", "turnId", "updatedAt"]);
|
|
76
79
|
|
|
77
80
|
function requireExactFields(value, fields, name) {
|
|
78
81
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -100,6 +103,13 @@ function requireTimestamp(value, name) {
|
|
|
100
103
|
return value;
|
|
101
104
|
}
|
|
102
105
|
|
|
106
|
+
function transitionTimestamp(now, currentUpdatedAt) {
|
|
107
|
+
const candidate = requireTimestamp(now ?? new Date().toISOString(), "transition timestamp");
|
|
108
|
+
return Date.parse(candidate) < Date.parse(currentUpdatedAt)
|
|
109
|
+
? currentUpdatedAt
|
|
110
|
+
: candidate;
|
|
111
|
+
}
|
|
112
|
+
|
|
103
113
|
function optionalString(value, name, { maxLength = null } = {}) {
|
|
104
114
|
if (value === null) return null;
|
|
105
115
|
const normalized = requireString(value, name).trim();
|
|
@@ -668,14 +678,62 @@ export async function removeProject(workspaceRoot, name) {
|
|
|
668
678
|
}
|
|
669
679
|
|
|
670
680
|
async function validateDispatchShape(dispatch, name = "task") {
|
|
671
|
-
|
|
681
|
+
const supportedVersions = [
|
|
682
|
+
PREVIOUS_SELF_LINKING_TASK_SCHEMA_VERSION,
|
|
683
|
+
CURRENT_TASK_SCHEMA_VERSION,
|
|
684
|
+
];
|
|
685
|
+
if (!supportedVersions.includes(dispatch?.schemaVersion)) {
|
|
672
686
|
throw new Error(`unsupported ${name} schemaVersion`);
|
|
673
687
|
}
|
|
674
|
-
requireExactFields(
|
|
688
|
+
requireExactFields(
|
|
689
|
+
dispatch,
|
|
690
|
+
dispatch.schemaVersion >= CURRENT_TASK_SCHEMA_VERSION
|
|
691
|
+
? DISPATCH_FIELDS
|
|
692
|
+
: STATEFUL_DISPATCH_FIELDS,
|
|
693
|
+
name,
|
|
694
|
+
);
|
|
675
695
|
const id = requireSafeId(dispatch.id, `${name}.id`);
|
|
676
696
|
const project = await normalizeProject(dispatch.project, 0, { checkPath: false });
|
|
697
|
+
const status = requireEnum(dispatch.status, TASK_STATUSES, `${name}.status`);
|
|
698
|
+
const summary = optionalString(dispatch.summary, `${name}.summary`, {
|
|
699
|
+
maxLength: MAX_RESULT_SUMMARY_LENGTH,
|
|
700
|
+
});
|
|
701
|
+
const turnId = optionalString(dispatch.turnId, `${name}.turnId`, { maxLength: 256 });
|
|
702
|
+
const updatedAt = requireTimestamp(dispatch.updatedAt, `${name}.updatedAt`);
|
|
703
|
+
let lastResult = null;
|
|
704
|
+
if (dispatch.schemaVersion >= CURRENT_TASK_SCHEMA_VERSION) {
|
|
705
|
+
if (dispatch.lastResult !== null) {
|
|
706
|
+
requireExactFields(dispatch.lastResult, LAST_RESULT_FIELDS, `${name}.lastResult`);
|
|
707
|
+
lastResult = {
|
|
708
|
+
status: requireEnum(
|
|
709
|
+
dispatch.lastResult.status,
|
|
710
|
+
RESULT_STATUSES,
|
|
711
|
+
`${name}.lastResult.status`,
|
|
712
|
+
),
|
|
713
|
+
summary: optionalString(
|
|
714
|
+
dispatch.lastResult.summary,
|
|
715
|
+
`${name}.lastResult.summary`,
|
|
716
|
+
{ maxLength: MAX_RESULT_SUMMARY_LENGTH },
|
|
717
|
+
),
|
|
718
|
+
turnId: optionalString(
|
|
719
|
+
dispatch.lastResult.turnId,
|
|
720
|
+
`${name}.lastResult.turnId`,
|
|
721
|
+
{ maxLength: 256 },
|
|
722
|
+
),
|
|
723
|
+
updatedAt: requireTimestamp(
|
|
724
|
+
dispatch.lastResult.updatedAt,
|
|
725
|
+
`${name}.lastResult.updatedAt`,
|
|
726
|
+
),
|
|
727
|
+
};
|
|
728
|
+
if (lastResult.summary === null) {
|
|
729
|
+
throw new Error(`${name}.lastResult.summary must be a non-empty string`);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
} else if (RESULT_STATUSES.has(status)) {
|
|
733
|
+
lastResult = { status, summary, turnId, updatedAt };
|
|
734
|
+
}
|
|
677
735
|
const normalized = {
|
|
678
|
-
schemaVersion:
|
|
736
|
+
schemaVersion: dispatch.schemaVersion,
|
|
679
737
|
id,
|
|
680
738
|
project,
|
|
681
739
|
title: requireString(dispatch.title, `${name}.title`).trim(),
|
|
@@ -684,20 +742,93 @@ async function validateDispatchShape(dispatch, name = "task") {
|
|
|
684
742
|
? null
|
|
685
743
|
: normalizeDurableThreadId(dispatch.threadId, `${name}.threadId`),
|
|
686
744
|
createdAt: requireTimestamp(dispatch.createdAt, `${name}.createdAt`),
|
|
687
|
-
status
|
|
688
|
-
summary
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
turnId: optionalString(dispatch.turnId, `${name}.turnId`, { maxLength: 256 }),
|
|
692
|
-
updatedAt: requireTimestamp(dispatch.updatedAt, `${name}.updatedAt`),
|
|
745
|
+
status,
|
|
746
|
+
summary,
|
|
747
|
+
turnId,
|
|
748
|
+
updatedAt,
|
|
693
749
|
updatedBy: requireEnum(dispatch.updatedBy, TASK_UPDATE_SOURCES, `${name}.updatedBy`),
|
|
750
|
+
lastResult,
|
|
694
751
|
};
|
|
752
|
+
const isSelfLinkingRecord = normalized.threadId !== null
|
|
753
|
+
&& parseTaskChefMarker(normalized.instruction) === normalized.id;
|
|
754
|
+
if (isSelfLinkingRecord) {
|
|
755
|
+
if (normalized.turnId !== null) {
|
|
756
|
+
normalizeCodexThreadId(normalized.turnId, `${name}.turnId`);
|
|
757
|
+
}
|
|
758
|
+
if (normalized.lastResult?.turnId != null) {
|
|
759
|
+
normalizeCodexThreadId(
|
|
760
|
+
normalized.lastResult.turnId,
|
|
761
|
+
`${name}.lastResult.turnId`,
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
if (normalized.schemaVersion >= PREVIOUS_SELF_LINKING_TASK_SCHEMA_VERSION) {
|
|
766
|
+
if (normalized.threadId === null) {
|
|
767
|
+
const isLinkPending = normalized.status === "working"
|
|
768
|
+
&& normalized.summary === null
|
|
769
|
+
&& normalized.turnId === null
|
|
770
|
+
&& normalized.lastResult === null
|
|
771
|
+
&& normalized.updatedBy === "dispatcher";
|
|
772
|
+
const isCreationFailure = normalized.status === "failed"
|
|
773
|
+
&& normalized.summary !== null
|
|
774
|
+
&& normalized.turnId === null
|
|
775
|
+
&& normalized.lastResult?.status === "failed"
|
|
776
|
+
&& normalized.lastResult.turnId === null
|
|
777
|
+
&& normalized.updatedBy === "mcp";
|
|
778
|
+
if (!isLinkPending && !isCreationFailure) {
|
|
779
|
+
throw new Error(`${name} has an invalid unlinked lifecycle state`);
|
|
780
|
+
}
|
|
781
|
+
} else {
|
|
782
|
+
if (RESULT_STATUSES.has(normalized.status) && normalized.turnId === null) {
|
|
783
|
+
throw new Error(`${name}.turnId is required for a linked semantic state`);
|
|
784
|
+
}
|
|
785
|
+
if (normalized.lastResult !== null && normalized.lastResult.turnId === null) {
|
|
786
|
+
throw new Error(`${name}.lastResult.turnId is required for a linked result`);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
695
790
|
if (normalized.status === "working" && normalized.summary !== null) {
|
|
696
791
|
throw new Error(`${name}.summary must be null while status is working`);
|
|
697
792
|
}
|
|
698
793
|
if (RESULT_STATUSES.has(normalized.status) && normalized.summary === null) {
|
|
699
794
|
throw new Error(`${name}.summary is required for status ${normalized.status}`);
|
|
700
795
|
}
|
|
796
|
+
if (normalized.schemaVersion >= CURRENT_TASK_SCHEMA_VERSION) {
|
|
797
|
+
if (RESULT_STATUSES.has(normalized.status)) {
|
|
798
|
+
if (
|
|
799
|
+
normalized.lastResult === null
|
|
800
|
+
|| normalized.lastResult.status !== normalized.status
|
|
801
|
+
|| normalized.lastResult.summary !== normalized.summary
|
|
802
|
+
|| normalized.lastResult.turnId !== normalized.turnId
|
|
803
|
+
|| normalized.lastResult.updatedAt !== normalized.updatedAt
|
|
804
|
+
) {
|
|
805
|
+
throw new Error(`${name}.lastResult must match the current semantic state`);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
if (
|
|
809
|
+
normalized.lastResult !== null
|
|
810
|
+
&& Date.parse(normalized.lastResult.updatedAt) < Date.parse(normalized.createdAt)
|
|
811
|
+
) {
|
|
812
|
+
throw new Error(`${name}.lastResult.updatedAt must not be earlier than createdAt`);
|
|
813
|
+
}
|
|
814
|
+
if (
|
|
815
|
+
normalized.lastResult !== null
|
|
816
|
+
&& Date.parse(normalized.lastResult.updatedAt) > Date.parse(normalized.updatedAt)
|
|
817
|
+
) {
|
|
818
|
+
throw new Error(`${name}.lastResult.updatedAt must not be later than updatedAt`);
|
|
819
|
+
}
|
|
820
|
+
if (
|
|
821
|
+
normalized.status === "working"
|
|
822
|
+
&& isSelfLinkingRecord
|
|
823
|
+
&& normalized.lastResult?.turnId != null
|
|
824
|
+
&& (
|
|
825
|
+
normalized.turnId === null
|
|
826
|
+
|| normalized.turnId <= normalized.lastResult.turnId
|
|
827
|
+
)
|
|
828
|
+
) {
|
|
829
|
+
throw new Error(`${name}.turnId must be newer than lastResult.turnId while working`);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
701
832
|
if (
|
|
702
833
|
Date.parse(normalized.updatedAt) < Date.parse(normalized.createdAt)
|
|
703
834
|
) {
|
|
@@ -802,6 +933,7 @@ export async function recordTask(workspaceRoot, input, { now } = {}) {
|
|
|
802
933
|
turnId: null,
|
|
803
934
|
updatedAt: createdAt,
|
|
804
935
|
updatedBy: "dispatcher",
|
|
936
|
+
lastResult: null,
|
|
805
937
|
});
|
|
806
938
|
const existing = await readDispatchesUnlocked(root);
|
|
807
939
|
if (existing.some((item) => item.id === dispatch.id)) {
|
|
@@ -845,8 +977,9 @@ export async function linkTask(workspaceRoot, taskId, threadId, { now } = {}) {
|
|
|
845
977
|
if (dispatch.threadId === durableThreadId) return dispatch;
|
|
846
978
|
const canonical = await validateDispatchShape({
|
|
847
979
|
...dispatch,
|
|
980
|
+
schemaVersion: CURRENT_TASK_SCHEMA_VERSION,
|
|
848
981
|
threadId: durableThreadId,
|
|
849
|
-
updatedAt: now
|
|
982
|
+
updatedAt: transitionTimestamp(now, dispatch.updatedAt),
|
|
850
983
|
updatedBy: "mcp",
|
|
851
984
|
});
|
|
852
985
|
const lines = records.map((record, recordIndex) => recordIndex === index
|
|
@@ -875,8 +1008,9 @@ export async function linkTask(workspaceRoot, taskId, threadId, { now } = {}) {
|
|
|
875
1008
|
}
|
|
876
1009
|
const linked = await validateDispatchShape({
|
|
877
1010
|
...dispatch,
|
|
1011
|
+
schemaVersion: CURRENT_TASK_SCHEMA_VERSION,
|
|
878
1012
|
threadId: durableThreadId,
|
|
879
|
-
updatedAt: now
|
|
1013
|
+
updatedAt: transitionTimestamp(now, dispatch.updatedAt),
|
|
880
1014
|
updatedBy: "mcp",
|
|
881
1015
|
});
|
|
882
1016
|
const lines = records.map((record, recordIndex) => recordIndex === index
|
|
@@ -895,22 +1029,53 @@ function dispatchLineWithState(dispatch, patch) {
|
|
|
895
1029
|
});
|
|
896
1030
|
}
|
|
897
1031
|
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
new
|
|
902
|
-
|
|
903
|
-
);
|
|
1032
|
+
function normalizeTaskStateInput(input, { allowWorking }) {
|
|
1033
|
+
const fields = new Set(["taskId", "threadId", "turnId", "status", "summary"]);
|
|
1034
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
1035
|
+
throw new Error("task state must be an object");
|
|
1036
|
+
}
|
|
1037
|
+
const unexpected = Object.keys(input).find((key) => !fields.has(key));
|
|
1038
|
+
if (unexpected) throw new Error(`task state has unsupported field: ${unexpected}`);
|
|
1039
|
+
for (const field of ["taskId", "threadId", "turnId", "status"]) {
|
|
1040
|
+
if (!(field in input)) throw new Error(`task state is missing field: ${field}`);
|
|
1041
|
+
}
|
|
904
1042
|
const id = requireSafeId(input.taskId, "taskId");
|
|
905
1043
|
const threadId = input.threadId === null
|
|
906
1044
|
? null
|
|
907
1045
|
: normalizeDurableThreadId(input.threadId, "threadId");
|
|
908
1046
|
const turnId = optionalString(input.turnId, "turnId", { maxLength: 256 });
|
|
909
|
-
const status = requireEnum(
|
|
910
|
-
|
|
1047
|
+
const status = requireEnum(
|
|
1048
|
+
input.status,
|
|
1049
|
+
allowWorking ? TASK_STATUSES : RESULT_STATUSES,
|
|
1050
|
+
"status",
|
|
1051
|
+
);
|
|
1052
|
+
const summary = optionalString("summary" in input ? input.summary : null, "summary", {
|
|
911
1053
|
maxLength: MAX_RESULT_SUMMARY_LENGTH,
|
|
912
1054
|
});
|
|
913
|
-
if (
|
|
1055
|
+
if (status === "working" && summary !== null) {
|
|
1056
|
+
throw new Error("summary must be null while status is working");
|
|
1057
|
+
}
|
|
1058
|
+
if (status !== "working" && summary === null) {
|
|
1059
|
+
throw new Error(`summary is required for status ${status}`);
|
|
1060
|
+
}
|
|
1061
|
+
return { id, threadId, turnId, status, summary };
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function sameLastResult(lastResult, { status, summary, turnId }) {
|
|
1065
|
+
return lastResult !== null
|
|
1066
|
+
&& lastResult.status === status
|
|
1067
|
+
&& lastResult.summary === summary
|
|
1068
|
+
&& lastResult.turnId === turnId;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
async function reportTaskStateInternal(
|
|
1072
|
+
workspaceRoot,
|
|
1073
|
+
input,
|
|
1074
|
+
{ now, compatibilityAlias = false } = {},
|
|
1075
|
+
) {
|
|
1076
|
+
const { id, threadId, turnId, status, summary } = normalizeTaskStateInput(input, {
|
|
1077
|
+
allowWorking: !compatibilityAlias,
|
|
1078
|
+
});
|
|
914
1079
|
const root = await realpath(path.resolve(workspaceRoot));
|
|
915
1080
|
return withWorkspaceLock(root, async () => {
|
|
916
1081
|
const records = await readDispatchRecordsUnlocked(root);
|
|
@@ -922,45 +1087,120 @@ export async function reportTaskResult(workspaceRoot, input, { now } = {}) {
|
|
|
922
1087
|
dispatch.threadId !== null
|
|
923
1088
|
&& parseTaskChefMarker(dispatch.instruction) === dispatch.id
|
|
924
1089
|
);
|
|
925
|
-
|
|
1090
|
+
if (
|
|
1091
|
+
!compatibilityAlias
|
|
1092
|
+
&& dispatch.threadId !== null
|
|
1093
|
+
&& !isSelfLinkingJourney
|
|
1094
|
+
) {
|
|
1095
|
+
throw new Error(`report_state accepts only self-linked task records: ${id}`);
|
|
1096
|
+
}
|
|
1097
|
+
let stateTurnId = turnId;
|
|
926
1098
|
if (isSelfLinkingJourney) {
|
|
927
1099
|
if (dispatch.updatedBy === "dispatcher") {
|
|
928
1100
|
throw new Error(`self-linking task must link before reporting a result: ${id}`);
|
|
929
1101
|
}
|
|
930
|
-
|
|
1102
|
+
stateTurnId = normalizeCodexThreadId(turnId, "turnId");
|
|
931
1103
|
}
|
|
932
1104
|
if (dispatch.threadId === null) {
|
|
933
|
-
if (threadId !== null ||
|
|
1105
|
+
if (threadId !== null || stateTurnId !== null || status !== "failed") {
|
|
934
1106
|
throw new Error(`task without a durable threadId accepts only failed with null thread/turn IDs: ${id}`);
|
|
935
1107
|
}
|
|
1108
|
+
if (!compatibilityAlias) {
|
|
1109
|
+
const rawSchemaVersion = records[index].raw.schemaVersion;
|
|
1110
|
+
const hasCurrentMarker = rawSchemaVersion
|
|
1111
|
+
>= PREVIOUS_SELF_LINKING_TASK_SCHEMA_VERSION
|
|
1112
|
+
&& parseTaskChefMarker(dispatch.instruction) === dispatch.id;
|
|
1113
|
+
const isFreshCreationFailure = hasCurrentMarker
|
|
1114
|
+
&& dispatch.status === "working"
|
|
1115
|
+
&& dispatch.turnId === null
|
|
1116
|
+
&& dispatch.lastResult === null
|
|
1117
|
+
&& dispatch.updatedBy === "dispatcher";
|
|
1118
|
+
const isIdenticalCreationFailureRetry = hasCurrentMarker
|
|
1119
|
+
&& dispatch.status === "failed"
|
|
1120
|
+
&& dispatch.turnId === null
|
|
1121
|
+
&& dispatch.updatedBy === "mcp"
|
|
1122
|
+
&& sameLastResult(dispatch.lastResult, {
|
|
1123
|
+
status,
|
|
1124
|
+
summary,
|
|
1125
|
+
turnId: stateTurnId,
|
|
1126
|
+
});
|
|
1127
|
+
if (!isFreshCreationFailure && !isIdenticalCreationFailureRetry) {
|
|
1128
|
+
throw new Error(`report_state unlinked failure requires a fresh link-pending task: ${id}`);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
936
1131
|
} else {
|
|
937
1132
|
if (threadIdentityKey(threadId) !== threadIdentityKey(dispatch.threadId)) {
|
|
938
1133
|
throw new Error(`task result threadId does not match recorded threadId: ${id}`);
|
|
939
1134
|
}
|
|
940
|
-
if (
|
|
941
|
-
throw new Error(`task
|
|
1135
|
+
if (stateTurnId === null) {
|
|
1136
|
+
throw new Error(`task state turnId is required for a linked task: ${id}`);
|
|
942
1137
|
}
|
|
943
1138
|
}
|
|
944
|
-
if (
|
|
945
|
-
if (status ===
|
|
946
|
-
|
|
1139
|
+
if (status === "working") {
|
|
1140
|
+
if (dispatch.status === "working" && stateTurnId === dispatch.turnId) return dispatch;
|
|
1141
|
+
if (isSelfLinkingJourney) {
|
|
1142
|
+
if (dispatch.turnId !== null && stateTurnId <= dispatch.turnId) {
|
|
1143
|
+
throw new Error(`working turnId must be newer than the current task turnId: ${id}`);
|
|
1144
|
+
}
|
|
1145
|
+
if (dispatch.lastResult?.turnId != null && stateTurnId <= dispatch.lastResult.turnId) {
|
|
1146
|
+
throw new Error(`working turnId must be newer than the last result turnId: ${id}`);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
const updatedAt = transitionTimestamp(now, dispatch.updatedAt);
|
|
1150
|
+
const updated = await validateDispatchShape({
|
|
1151
|
+
...dispatch,
|
|
1152
|
+
schemaVersion: CURRENT_TASK_SCHEMA_VERSION,
|
|
1153
|
+
status,
|
|
1154
|
+
summary: null,
|
|
1155
|
+
turnId: stateTurnId,
|
|
1156
|
+
updatedAt,
|
|
1157
|
+
updatedBy: "mcp",
|
|
1158
|
+
lastResult: dispatch.lastResult,
|
|
1159
|
+
});
|
|
1160
|
+
const lines = records.map((record, recordIndex) => recordIndex === index
|
|
1161
|
+
? dispatchLineWithState(updated, {})
|
|
1162
|
+
: record.line);
|
|
1163
|
+
await writeDispatchLinesAtomic(root, lines);
|
|
1164
|
+
return updated;
|
|
947
1165
|
}
|
|
948
1166
|
if (
|
|
949
|
-
|
|
950
|
-
&& dispatch.turnId
|
|
951
|
-
&&
|
|
1167
|
+
dispatch.status === status
|
|
1168
|
+
&& dispatch.turnId === stateTurnId
|
|
1169
|
+
&& dispatch.summary === summary
|
|
1170
|
+
&& sameLastResult(dispatch.lastResult, { status, summary, turnId: stateTurnId })
|
|
952
1171
|
) {
|
|
953
|
-
|
|
1172
|
+
return dispatch;
|
|
1173
|
+
}
|
|
1174
|
+
if (dispatch.turnId === stateTurnId && dispatch.status !== "working") {
|
|
1175
|
+
throw new Error(`task turn already has a different semantic result: ${id}`);
|
|
954
1176
|
}
|
|
955
|
-
|
|
1177
|
+
if (compatibilityAlias) {
|
|
1178
|
+
const matchesWorkingTurn = dispatch.status === "working"
|
|
1179
|
+
&& dispatch.turnId === stateTurnId;
|
|
1180
|
+
if (!matchesWorkingTurn && isSelfLinkingJourney) {
|
|
1181
|
+
if (dispatch.turnId !== null && stateTurnId <= dispatch.turnId) {
|
|
1182
|
+
throw new Error(`task result turnId must be newer than the stored turnId: ${id}`);
|
|
1183
|
+
}
|
|
1184
|
+
if (dispatch.lastResult?.turnId != null && stateTurnId <= dispatch.lastResult.turnId) {
|
|
1185
|
+
throw new Error(`task result turnId must be newer than the last result turnId: ${id}`);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
} else if (dispatch.status !== "working" || dispatch.turnId !== stateTurnId) {
|
|
1189
|
+
throw new Error(`task result must match the current working turnId: ${id}`);
|
|
1190
|
+
}
|
|
1191
|
+
const updatedAt = transitionTimestamp(now, dispatch.updatedAt);
|
|
1192
|
+
const lastResult = { status, summary, turnId: stateTurnId, updatedAt };
|
|
1193
|
+
const candidate = {
|
|
956
1194
|
...dispatch,
|
|
957
1195
|
schemaVersion: CURRENT_TASK_SCHEMA_VERSION,
|
|
958
1196
|
status,
|
|
959
1197
|
summary,
|
|
960
|
-
turnId:
|
|
961
|
-
updatedAt
|
|
1198
|
+
turnId: stateTurnId,
|
|
1199
|
+
updatedAt,
|
|
962
1200
|
updatedBy: "mcp",
|
|
963
|
-
|
|
1201
|
+
lastResult,
|
|
1202
|
+
};
|
|
1203
|
+
const updated = await validateDispatchShape(candidate);
|
|
964
1204
|
const lines = records.map((record, recordIndex) => recordIndex === index
|
|
965
1205
|
? dispatchLineWithState(updated, {})
|
|
966
1206
|
: record.line);
|
|
@@ -969,6 +1209,17 @@ export async function reportTaskResult(workspaceRoot, input, { now } = {}) {
|
|
|
969
1209
|
});
|
|
970
1210
|
}
|
|
971
1211
|
|
|
1212
|
+
export async function reportTaskState(workspaceRoot, input, { now } = {}) {
|
|
1213
|
+
return reportTaskStateInternal(workspaceRoot, input, { now });
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
export async function reportTaskResult(workspaceRoot, input, options = {}) {
|
|
1217
|
+
return reportTaskStateInternal(workspaceRoot, input, {
|
|
1218
|
+
...options,
|
|
1219
|
+
compatibilityAlias: true,
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
|
|
972
1223
|
export async function readTask(workspaceRoot, taskId) {
|
|
973
1224
|
const id = requireSafeId(taskId, "taskId");
|
|
974
1225
|
const dispatch = (await listTasks(workspaceRoot)).find((item) => item.id === id);
|