taskchef 5.7.2 → 5.9.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 -2
- package/BACKLOG.md +4 -4
- package/README.md +125 -45
- package/SPEC.md +106 -89
- package/docs/delegation-design.md +158 -254
- package/hooks/hooks.json +18 -0
- package/hooks/taskchef-initial-prompt.js +17 -0
- package/index.js +13 -1
- package/package.json +3 -2
- package/skills/taskchef-bootstrap/SKILL.md +6 -3
- package/skills/taskchef-delegate/SKILL.md +49 -91
- package/skills/taskchef-report/SKILL.md +60 -27
- package/src/cli.js +52 -2
- package/src/dashboard/app.js +255 -0
- package/src/dashboard/index.html +86 -0
- package/src/dashboard/state.js +40 -0
- package/src/dashboard/styles.css +147 -0
- package/src/dashboard.js +597 -0
- package/src/delegation.js +109 -359
- package/src/hook.js +60 -0
- package/src/mcp.js +35 -2
- package/src/workspace-path.js +5 -1
- package/src/workspace.js +239 -23
package/src/workspace-path.js
CHANGED
|
@@ -37,8 +37,12 @@ export function resolveWorkspacePath({
|
|
|
37
37
|
source = "explicit";
|
|
38
38
|
value = explicit;
|
|
39
39
|
}
|
|
40
|
+
const expanded = expandHome(value, homedir);
|
|
41
|
+
if (source === "environment" && !path.isAbsolute(expanded)) {
|
|
42
|
+
throw new Error(`${TASKCHEF_WORKSPACE_ENV} must be an absolute path or start with ~/`);
|
|
43
|
+
}
|
|
40
44
|
return {
|
|
41
|
-
workspace: path.resolve(cwd,
|
|
45
|
+
workspace: path.resolve(cwd, expanded),
|
|
42
46
|
source,
|
|
43
47
|
};
|
|
44
48
|
}
|
package/src/workspace.js
CHANGED
|
@@ -46,8 +46,10 @@ const SKILLS_SOURCE_ROOT = fileURLToPath(new URL("../skills/", import.meta.url))
|
|
|
46
46
|
const DISPATCH_FILE_NAME = "tasks.jsonl";
|
|
47
47
|
const WORKSPACE_LOCK_NAME = ".taskchef-workspace.lock";
|
|
48
48
|
const SAFE_ID = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
49
|
-
const
|
|
49
|
+
const CURRENT_CONFIG_SCHEMA_VERSION = 2;
|
|
50
|
+
const CURRENT_TASK_SCHEMA_VERSION = 3;
|
|
50
51
|
const LEGACY_SCHEMA_VERSION = 1;
|
|
52
|
+
const PREVIOUS_SCHEMA_VERSION = 2;
|
|
51
53
|
const CONFIG_FIELDS = new Set(["schemaVersion", "projects"]);
|
|
52
54
|
const PROJECT_FIELDS = new Set([
|
|
53
55
|
"name",
|
|
@@ -66,6 +68,20 @@ const DISPATCH_FIELDS = new Set([
|
|
|
66
68
|
"instruction",
|
|
67
69
|
"threadId",
|
|
68
70
|
"createdAt",
|
|
71
|
+
"status",
|
|
72
|
+
"summary",
|
|
73
|
+
"turnId",
|
|
74
|
+
"updatedAt",
|
|
75
|
+
"updatedBy",
|
|
76
|
+
]);
|
|
77
|
+
const LEGACY_DISPATCH_FIELDS = new Set([
|
|
78
|
+
"schemaVersion",
|
|
79
|
+
"id",
|
|
80
|
+
"project",
|
|
81
|
+
"title",
|
|
82
|
+
"instruction",
|
|
83
|
+
"threadId",
|
|
84
|
+
"createdAt",
|
|
69
85
|
]);
|
|
70
86
|
const RECORD_DISPATCH_FIELDS = new Set([
|
|
71
87
|
"id",
|
|
@@ -74,6 +90,10 @@ const RECORD_DISPATCH_FIELDS = new Set([
|
|
|
74
90
|
"instruction",
|
|
75
91
|
"threadId",
|
|
76
92
|
]);
|
|
93
|
+
const RESULT_STATUSES = new Set(["needs_input", "completed", "failed"]);
|
|
94
|
+
const TASK_STATUSES = new Set(["working", ...RESULT_STATUSES]);
|
|
95
|
+
const TASK_UPDATE_SOURCES = new Set(["dispatcher", "hook", "mcp"]);
|
|
96
|
+
const MAX_RESULT_SUMMARY_LENGTH = 2_000;
|
|
77
97
|
|
|
78
98
|
function requireExactFields(value, fields, name) {
|
|
79
99
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -101,6 +121,22 @@ function requireTimestamp(value, name) {
|
|
|
101
121
|
return value;
|
|
102
122
|
}
|
|
103
123
|
|
|
124
|
+
function optionalString(value, name, { maxLength = null } = {}) {
|
|
125
|
+
if (value === null) return null;
|
|
126
|
+
const normalized = requireString(value, name).trim();
|
|
127
|
+
if (maxLength !== null && normalized.length > maxLength) {
|
|
128
|
+
throw new Error(`${name} must be at most ${maxLength} characters`);
|
|
129
|
+
}
|
|
130
|
+
return normalized;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function requireEnum(value, allowed, name) {
|
|
134
|
+
if (!allowed.has(value)) {
|
|
135
|
+
throw new Error(`${name} must be one of: ${[...allowed].join(", ")}`);
|
|
136
|
+
}
|
|
137
|
+
return value;
|
|
138
|
+
}
|
|
139
|
+
|
|
104
140
|
export function requireSafeId(value, name = "id") {
|
|
105
141
|
requireString(value, name);
|
|
106
142
|
if (!SAFE_ID.test(value)) {
|
|
@@ -556,11 +592,11 @@ async function inspectProject(input, index = 0) {
|
|
|
556
592
|
|
|
557
593
|
export async function validateConfig(config, { checkPaths = true } = {}) {
|
|
558
594
|
requireExactFields(config, CONFIG_FIELDS, "taskchef.json");
|
|
559
|
-
if (![LEGACY_SCHEMA_VERSION,
|
|
595
|
+
if (![LEGACY_SCHEMA_VERSION, CURRENT_CONFIG_SCHEMA_VERSION].includes(config.schemaVersion)) {
|
|
560
596
|
throw new Error("unsupported configuration schemaVersion");
|
|
561
597
|
}
|
|
562
598
|
return {
|
|
563
|
-
schemaVersion:
|
|
599
|
+
schemaVersion: CURRENT_CONFIG_SCHEMA_VERSION,
|
|
564
600
|
projects: await normalizeProjects(config.projects, {
|
|
565
601
|
checkPaths,
|
|
566
602
|
allowLegacyGithubRepo: config.schemaVersion === LEGACY_SCHEMA_VERSION,
|
|
@@ -597,14 +633,14 @@ export async function initializeWorkspace(workspaceRoot) {
|
|
|
597
633
|
: null;
|
|
598
634
|
const config = configExists
|
|
599
635
|
? await readConfig(root, { checkPaths: false })
|
|
600
|
-
: { schemaVersion:
|
|
636
|
+
: { schemaVersion: CURRENT_CONFIG_SCHEMA_VERSION, projects: [] };
|
|
601
637
|
const dispatchPath = path.join(root, DISPATCH_FILE_NAME);
|
|
602
638
|
if (configExists && (await managedRegularFileExists(dispatchPath))) {
|
|
603
639
|
await readDispatchesUnlocked(root);
|
|
604
640
|
}
|
|
605
641
|
const { legacySkills } = await ensureWorkspaceSkills(root);
|
|
606
642
|
if (!configExists) await writeJsonAtomic(configPath, config, { exclusive: true });
|
|
607
|
-
else if (storedConfigVersion !==
|
|
643
|
+
else if (storedConfigVersion !== CURRENT_CONFIG_SCHEMA_VERSION) {
|
|
608
644
|
await writeJsonAtomic(configPath, config);
|
|
609
645
|
}
|
|
610
646
|
const tasks = await ensureDispatchFile(root);
|
|
@@ -620,7 +656,7 @@ export async function initializeWorkspace(workspaceRoot) {
|
|
|
620
656
|
path: configPath,
|
|
621
657
|
action: !configExists
|
|
622
658
|
? "created"
|
|
623
|
-
: storedConfigVersion ===
|
|
659
|
+
: storedConfigVersion === CURRENT_CONFIG_SCHEMA_VERSION ? "unchanged" : "migrated",
|
|
624
660
|
value: config,
|
|
625
661
|
},
|
|
626
662
|
tasks,
|
|
@@ -673,7 +709,7 @@ export async function addProject(workspaceRoot, input) {
|
|
|
673
709
|
const project = await inspectProject(input);
|
|
674
710
|
assertWorkspaceOutsideProject(root, project.path);
|
|
675
711
|
const updated = await validateConfig({
|
|
676
|
-
schemaVersion:
|
|
712
|
+
schemaVersion: CURRENT_CONFIG_SCHEMA_VERSION,
|
|
677
713
|
projects: [...config.projects, project],
|
|
678
714
|
});
|
|
679
715
|
await writeJsonAtomic(path.join(root, "taskchef.json"), updated);
|
|
@@ -710,7 +746,10 @@ export async function importProjects(workspaceRoot, inputs, { replace = false }
|
|
|
710
746
|
if (index === -1) projects.push(project);
|
|
711
747
|
else projects[index] = project;
|
|
712
748
|
}
|
|
713
|
-
const config = await validateConfig({
|
|
749
|
+
const config = await validateConfig({
|
|
750
|
+
schemaVersion: CURRENT_CONFIG_SCHEMA_VERSION,
|
|
751
|
+
projects,
|
|
752
|
+
});
|
|
714
753
|
await writeJsonAtomic(path.join(root, "taskchef.json"), config);
|
|
715
754
|
return {
|
|
716
755
|
mode: replace ? "replace" : "merge",
|
|
@@ -732,7 +771,7 @@ export async function removeProject(workspaceRoot, name) {
|
|
|
732
771
|
const [project] = config.projects.slice(index, index + 1);
|
|
733
772
|
const projects = config.projects.filter((_, projectIndex) => projectIndex !== index);
|
|
734
773
|
await writeJsonAtomic(path.join(root, "taskchef.json"), {
|
|
735
|
-
schemaVersion:
|
|
774
|
+
schemaVersion: CURRENT_CONFIG_SCHEMA_VERSION,
|
|
736
775
|
projects,
|
|
737
776
|
});
|
|
738
777
|
return { project };
|
|
@@ -744,17 +783,28 @@ async function validateDispatchShape(
|
|
|
744
783
|
name = "task",
|
|
745
784
|
{ allowLegacyHeading = false } = {},
|
|
746
785
|
) {
|
|
747
|
-
|
|
748
|
-
|
|
786
|
+
const supportedVersions = [
|
|
787
|
+
LEGACY_SCHEMA_VERSION,
|
|
788
|
+
PREVIOUS_SCHEMA_VERSION,
|
|
789
|
+
CURRENT_TASK_SCHEMA_VERSION,
|
|
790
|
+
];
|
|
791
|
+
if (!supportedVersions.includes(dispatch?.schemaVersion)) {
|
|
749
792
|
throw new Error(`unsupported ${name} schemaVersion`);
|
|
750
793
|
}
|
|
794
|
+
requireExactFields(
|
|
795
|
+
dispatch,
|
|
796
|
+
dispatch.schemaVersion === CURRENT_TASK_SCHEMA_VERSION
|
|
797
|
+
? DISPATCH_FIELDS
|
|
798
|
+
: LEGACY_DISPATCH_FIELDS,
|
|
799
|
+
name,
|
|
800
|
+
);
|
|
751
801
|
const id = requireSafeId(dispatch.id, `${name}.id`);
|
|
752
802
|
const project = await normalizeProject(dispatch.project, 0, {
|
|
753
803
|
checkPath: false,
|
|
754
804
|
allowLegacyGithubRepo: dispatch.schemaVersion === LEGACY_SCHEMA_VERSION,
|
|
755
805
|
});
|
|
756
806
|
const normalized = {
|
|
757
|
-
schemaVersion:
|
|
807
|
+
schemaVersion: CURRENT_TASK_SCHEMA_VERSION,
|
|
758
808
|
id,
|
|
759
809
|
project,
|
|
760
810
|
title: requireString(dispatch.title, `${name}.title`).trim(),
|
|
@@ -763,7 +813,36 @@ async function validateDispatchShape(
|
|
|
763
813
|
? null
|
|
764
814
|
: normalizeDurableThreadId(dispatch.threadId, `${name}.threadId`),
|
|
765
815
|
createdAt: requireTimestamp(dispatch.createdAt, `${name}.createdAt`),
|
|
816
|
+
status: dispatch.schemaVersion === CURRENT_TASK_SCHEMA_VERSION
|
|
817
|
+
? requireEnum(dispatch.status, TASK_STATUSES, `${name}.status`)
|
|
818
|
+
: null,
|
|
819
|
+
summary: dispatch.schemaVersion === CURRENT_TASK_SCHEMA_VERSION
|
|
820
|
+
? optionalString(dispatch.summary, `${name}.summary`, {
|
|
821
|
+
maxLength: MAX_RESULT_SUMMARY_LENGTH,
|
|
822
|
+
})
|
|
823
|
+
: null,
|
|
824
|
+
turnId: dispatch.schemaVersion === CURRENT_TASK_SCHEMA_VERSION
|
|
825
|
+
? optionalString(dispatch.turnId, `${name}.turnId`, { maxLength: 256 })
|
|
826
|
+
: null,
|
|
827
|
+
updatedAt: dispatch.schemaVersion === CURRENT_TASK_SCHEMA_VERSION
|
|
828
|
+
? requireTimestamp(dispatch.updatedAt, `${name}.updatedAt`)
|
|
829
|
+
: null,
|
|
830
|
+
updatedBy: dispatch.schemaVersion === CURRENT_TASK_SCHEMA_VERSION
|
|
831
|
+
? requireEnum(dispatch.updatedBy, TASK_UPDATE_SOURCES, `${name}.updatedBy`)
|
|
832
|
+
: null,
|
|
766
833
|
};
|
|
834
|
+
if (normalized.status === "working" && normalized.summary !== null) {
|
|
835
|
+
throw new Error(`${name}.summary must be null while status is working`);
|
|
836
|
+
}
|
|
837
|
+
if (RESULT_STATUSES.has(normalized.status) && normalized.summary === null) {
|
|
838
|
+
throw new Error(`${name}.summary is required for status ${normalized.status}`);
|
|
839
|
+
}
|
|
840
|
+
if (
|
|
841
|
+
normalized.updatedAt !== null
|
|
842
|
+
&& Date.parse(normalized.updatedAt) < Date.parse(normalized.createdAt)
|
|
843
|
+
) {
|
|
844
|
+
throw new Error(`${name}.updatedAt must not be earlier than createdAt`);
|
|
845
|
+
}
|
|
767
846
|
if (
|
|
768
847
|
normalized.threadId === null &&
|
|
769
848
|
parseTaskChefMarker(normalized.instruction, { allowLegacyHeading }) !== normalized.id
|
|
@@ -773,13 +852,8 @@ async function validateDispatchShape(
|
|
|
773
852
|
return normalized;
|
|
774
853
|
}
|
|
775
854
|
|
|
776
|
-
async function
|
|
855
|
+
async function parseDispatchRecordsUnlocked(root, content) {
|
|
777
856
|
await readConfig(root, { checkPaths: false });
|
|
778
|
-
const filePath = path.join(root, DISPATCH_FILE_NAME);
|
|
779
|
-
if (!(await managedRegularFileExists(filePath))) {
|
|
780
|
-
throw new Error(`task log does not exist: ${filePath}`);
|
|
781
|
-
}
|
|
782
|
-
const content = await readFile(filePath, "utf8");
|
|
783
857
|
if (content.length > 0 && !content.endsWith("\n")) {
|
|
784
858
|
throw new Error(`${DISPATCH_FILE_NAME} must end with a newline`);
|
|
785
859
|
}
|
|
@@ -816,6 +890,14 @@ async function readDispatchRecordsUnlocked(root) {
|
|
|
816
890
|
return records;
|
|
817
891
|
}
|
|
818
892
|
|
|
893
|
+
async function readDispatchRecordsUnlocked(root) {
|
|
894
|
+
const filePath = path.join(root, DISPATCH_FILE_NAME);
|
|
895
|
+
if (!(await managedRegularFileExists(filePath))) {
|
|
896
|
+
throw new Error(`task log does not exist: ${filePath}`);
|
|
897
|
+
}
|
|
898
|
+
return parseDispatchRecordsUnlocked(root, await readFile(filePath, "utf8"));
|
|
899
|
+
}
|
|
900
|
+
|
|
819
901
|
async function readDispatchesUnlocked(root) {
|
|
820
902
|
return (await readDispatchRecordsUnlocked(root)).map((record) => record.normalized);
|
|
821
903
|
}
|
|
@@ -825,6 +907,12 @@ export async function listTasks(workspaceRoot) {
|
|
|
825
907
|
return readDispatchesUnlocked(root);
|
|
826
908
|
}
|
|
827
909
|
|
|
910
|
+
export async function parseTaskLogContent(workspaceRoot, content) {
|
|
911
|
+
const root = await realpath(path.resolve(workspaceRoot));
|
|
912
|
+
if (typeof content !== "string") throw new Error("task log content must be a string");
|
|
913
|
+
return (await parseDispatchRecordsUnlocked(root, content)).map((record) => record.normalized);
|
|
914
|
+
}
|
|
915
|
+
|
|
828
916
|
export async function recordTask(workspaceRoot, input, { now } = {}) {
|
|
829
917
|
requireExactFields(input, RECORD_DISPATCH_FIELDS, "task input");
|
|
830
918
|
const root = await realpath(path.resolve(workspaceRoot));
|
|
@@ -833,14 +921,20 @@ export async function recordTask(workspaceRoot, input, { now } = {}) {
|
|
|
833
921
|
const projectPath = await canonicalDirectory(input.project);
|
|
834
922
|
const project = config.projects.find((candidate) => candidate.path === projectPath);
|
|
835
923
|
if (!project) throw new Error(`project is not configured in taskchef.json: ${projectPath}`);
|
|
924
|
+
const createdAt = now ?? new Date().toISOString();
|
|
836
925
|
const dispatch = await validateDispatchShape({
|
|
837
|
-
schemaVersion:
|
|
926
|
+
schemaVersion: CURRENT_TASK_SCHEMA_VERSION,
|
|
838
927
|
id: input.id,
|
|
839
928
|
project,
|
|
840
929
|
title: input.title,
|
|
841
930
|
instruction: input.instruction,
|
|
842
931
|
threadId: input.threadId,
|
|
843
|
-
createdAt
|
|
932
|
+
createdAt,
|
|
933
|
+
status: "working",
|
|
934
|
+
summary: null,
|
|
935
|
+
turnId: null,
|
|
936
|
+
updatedAt: createdAt,
|
|
937
|
+
updatedBy: "dispatcher",
|
|
844
938
|
});
|
|
845
939
|
const existing = await readDispatchesUnlocked(root);
|
|
846
940
|
if (existing.some((item) => item.id === dispatch.id)) {
|
|
@@ -857,7 +951,7 @@ export async function recordTask(workspaceRoot, input, { now } = {}) {
|
|
|
857
951
|
});
|
|
858
952
|
}
|
|
859
953
|
|
|
860
|
-
export async function resolveTask(workspaceRoot, taskId, threadId) {
|
|
954
|
+
export async function resolveTask(workspaceRoot, taskId, threadId, { now } = {}) {
|
|
861
955
|
const id = requireSafeId(taskId, "taskId");
|
|
862
956
|
const durableThreadId = normalizeDurableThreadId(threadId);
|
|
863
957
|
const root = await realpath(path.resolve(workspaceRoot));
|
|
@@ -877,15 +971,137 @@ export async function resolveTask(workspaceRoot, taskId, threadId) {
|
|
|
877
971
|
if (dispatches.some((item) => item.threadId === durableThreadId)) {
|
|
878
972
|
throw new Error(`threadId is already recorded: ${durableThreadId}`);
|
|
879
973
|
}
|
|
880
|
-
const
|
|
974
|
+
const currentRecord = records[index];
|
|
975
|
+
const resolved = currentRecord.raw.schemaVersion === CURRENT_TASK_SCHEMA_VERSION
|
|
976
|
+
? await validateDispatchShape({
|
|
977
|
+
...dispatch,
|
|
978
|
+
threadId: durableThreadId,
|
|
979
|
+
updatedAt: now ?? new Date().toISOString(),
|
|
980
|
+
updatedBy: "dispatcher",
|
|
981
|
+
})
|
|
982
|
+
: { ...dispatch, threadId: durableThreadId };
|
|
881
983
|
const lines = records.map((record, recordIndex) => recordIndex === index
|
|
882
|
-
?
|
|
984
|
+
? currentRecord.raw.schemaVersion === CURRENT_TASK_SCHEMA_VERSION
|
|
985
|
+
? dispatchLineWithState(resolved, {})
|
|
986
|
+
: JSON.stringify({ ...record.raw, threadId: durableThreadId })
|
|
883
987
|
: record.line);
|
|
884
988
|
await writeDispatchLinesAtomic(root, lines);
|
|
885
989
|
return resolved;
|
|
886
990
|
});
|
|
887
991
|
}
|
|
888
992
|
|
|
993
|
+
function dispatchLineWithState(dispatch, patch) {
|
|
994
|
+
return JSON.stringify({
|
|
995
|
+
...dispatch,
|
|
996
|
+
schemaVersion: CURRENT_TASK_SCHEMA_VERSION,
|
|
997
|
+
...patch,
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
export async function startTaskFromHook(
|
|
1002
|
+
workspaceRoot,
|
|
1003
|
+
taskId,
|
|
1004
|
+
threadId,
|
|
1005
|
+
turnId,
|
|
1006
|
+
{ now } = {},
|
|
1007
|
+
) {
|
|
1008
|
+
const id = requireSafeId(taskId, "taskId");
|
|
1009
|
+
const durableThreadId = normalizeDurableThreadId(threadId);
|
|
1010
|
+
const currentTurnId = optionalString(turnId, "turnId", { maxLength: 256 });
|
|
1011
|
+
const root = await realpath(path.resolve(workspaceRoot));
|
|
1012
|
+
return withWorkspaceLock(root, async () => {
|
|
1013
|
+
const records = await readDispatchRecordsUnlocked(root);
|
|
1014
|
+
const dispatches = records.map((record) => record.normalized);
|
|
1015
|
+
const index = dispatches.findIndex((dispatch) => dispatch.id === id);
|
|
1016
|
+
if (index === -1) throw new Error(`task not found: ${id}`);
|
|
1017
|
+
const dispatch = dispatches[index];
|
|
1018
|
+
if (parseTaskChefMarker(dispatch.instruction) !== dispatch.id) {
|
|
1019
|
+
throw new Error(`task instruction does not contain its exact TaskChef marker: ${id}`);
|
|
1020
|
+
}
|
|
1021
|
+
if (dispatch.threadId !== null && dispatch.threadId !== durableThreadId) {
|
|
1022
|
+
throw new Error(`task already has a different threadId: ${id}`);
|
|
1023
|
+
}
|
|
1024
|
+
if (
|
|
1025
|
+
dispatch.threadId === null
|
|
1026
|
+
&& dispatches.some((item) => item.id !== id && item.threadId === durableThreadId)
|
|
1027
|
+
) {
|
|
1028
|
+
throw new Error(`threadId is already recorded: ${durableThreadId}`);
|
|
1029
|
+
}
|
|
1030
|
+
if (dispatch.threadId === durableThreadId && dispatch.updatedBy === "mcp") {
|
|
1031
|
+
return dispatch;
|
|
1032
|
+
}
|
|
1033
|
+
if (dispatch.threadId === durableThreadId && dispatch.updatedBy === "hook") {
|
|
1034
|
+
return dispatch;
|
|
1035
|
+
}
|
|
1036
|
+
const updatedAt = now ?? new Date().toISOString();
|
|
1037
|
+
const started = await validateDispatchShape({
|
|
1038
|
+
...dispatch,
|
|
1039
|
+
threadId: durableThreadId,
|
|
1040
|
+
status: "working",
|
|
1041
|
+
summary: null,
|
|
1042
|
+
turnId: currentTurnId,
|
|
1043
|
+
updatedAt,
|
|
1044
|
+
updatedBy: "hook",
|
|
1045
|
+
});
|
|
1046
|
+
const lines = records.map((record, recordIndex) => recordIndex === index
|
|
1047
|
+
? dispatchLineWithState(started, {})
|
|
1048
|
+
: record.line);
|
|
1049
|
+
await writeDispatchLinesAtomic(root, lines);
|
|
1050
|
+
return started;
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
export async function reportTaskResult(workspaceRoot, input, { now } = {}) {
|
|
1055
|
+
requireExactFields(
|
|
1056
|
+
input,
|
|
1057
|
+
new Set(["taskId", "threadId", "turnId", "status", "summary"]),
|
|
1058
|
+
"task result",
|
|
1059
|
+
);
|
|
1060
|
+
const id = requireSafeId(input.taskId, "taskId");
|
|
1061
|
+
const threadId = input.threadId === null
|
|
1062
|
+
? null
|
|
1063
|
+
: normalizeDurableThreadId(input.threadId, "threadId");
|
|
1064
|
+
const turnId = optionalString(input.turnId, "turnId", { maxLength: 256 });
|
|
1065
|
+
const status = requireEnum(input.status, RESULT_STATUSES, "status");
|
|
1066
|
+
const summary = optionalString(input.summary, "summary", {
|
|
1067
|
+
maxLength: MAX_RESULT_SUMMARY_LENGTH,
|
|
1068
|
+
});
|
|
1069
|
+
if (summary === null) throw new Error("summary must be a non-empty string");
|
|
1070
|
+
const root = await realpath(path.resolve(workspaceRoot));
|
|
1071
|
+
return withWorkspaceLock(root, async () => {
|
|
1072
|
+
const records = await readDispatchRecordsUnlocked(root);
|
|
1073
|
+
const dispatches = records.map((record) => record.normalized);
|
|
1074
|
+
const index = dispatches.findIndex((dispatch) => dispatch.id === id);
|
|
1075
|
+
if (index === -1) throw new Error(`task not found: ${id}`);
|
|
1076
|
+
const dispatch = dispatches[index];
|
|
1077
|
+
if (dispatch.threadId === null) {
|
|
1078
|
+
if (threadId !== null || turnId !== null || status !== "failed") {
|
|
1079
|
+
throw new Error(`task without a durable threadId accepts only failed with null thread/turn IDs: ${id}`);
|
|
1080
|
+
}
|
|
1081
|
+
} else {
|
|
1082
|
+
if (threadId !== dispatch.threadId) {
|
|
1083
|
+
throw new Error(`task result threadId does not match recorded threadId: ${id}`);
|
|
1084
|
+
}
|
|
1085
|
+
if (turnId === null) {
|
|
1086
|
+
throw new Error(`task result turnId is required for a linked task: ${id}`);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
const updated = await validateDispatchShape({
|
|
1090
|
+
...dispatch,
|
|
1091
|
+
status,
|
|
1092
|
+
summary,
|
|
1093
|
+
turnId,
|
|
1094
|
+
updatedAt: now ?? new Date().toISOString(),
|
|
1095
|
+
updatedBy: "mcp",
|
|
1096
|
+
});
|
|
1097
|
+
const lines = records.map((record, recordIndex) => recordIndex === index
|
|
1098
|
+
? dispatchLineWithState(updated, {})
|
|
1099
|
+
: record.line);
|
|
1100
|
+
await writeDispatchLinesAtomic(root, lines);
|
|
1101
|
+
return updated;
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
|
|
889
1105
|
export async function readTask(workspaceRoot, taskId) {
|
|
890
1106
|
const id = requireSafeId(taskId, "taskId");
|
|
891
1107
|
const dispatch = (await listTasks(workspaceRoot)).find((item) => item.id === id);
|