taskchef 6.1.3 → 7.1.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 +1 -1
- package/BACKLOG.md +1 -1
- package/README.md +100 -453
- package/docs/firstmate-taskchef-comparison.md +279 -0
- package/docs/spec.md +265 -0
- package/docs/workflows.md +249 -0
- package/index.js +2 -2
- package/package.json +4 -3
- package/scripts/benchmark-dispatch-prepare.js +7 -7
- package/skills/taskchef-bootstrap/SKILL.md +7 -8
- package/skills/taskchef-delegate/SKILL.md +15 -17
- package/skills/taskchef-report/SKILL.md +18 -22
- package/src/cli.js +7 -24
- package/src/dashboard/app.js +8 -4
- package/src/dashboard/state.js +1 -0
- package/src/dashboard.js +3 -1
- package/src/delegation.js +7 -10
- package/src/github.js +0 -4
- package/src/mcp.js +42 -8
- package/src/workspace.js +295 -251
- package/SPEC.md +0 -107
- package/docs/delegation-design.md +0 -227
package/src/workspace.js
CHANGED
|
@@ -5,17 +5,14 @@ import {
|
|
|
5
5
|
lstat,
|
|
6
6
|
mkdir,
|
|
7
7
|
readFile,
|
|
8
|
-
readdir,
|
|
9
8
|
realpath,
|
|
10
9
|
link,
|
|
11
10
|
rename,
|
|
12
|
-
rmdir,
|
|
13
11
|
stat,
|
|
14
12
|
unlink,
|
|
15
13
|
writeFile,
|
|
16
14
|
} from "node:fs/promises";
|
|
17
15
|
import { randomUUID } from "node:crypto";
|
|
18
|
-
import { fileURLToPath } from "node:url";
|
|
19
16
|
import path from "node:path";
|
|
20
17
|
import { promisify } from "node:util";
|
|
21
18
|
import lockfile from "proper-lockfile";
|
|
@@ -37,32 +34,22 @@ const DISPATCHER_INSTRUCTIONS_URL = new URL(
|
|
|
37
34
|
);
|
|
38
35
|
const DISPATCHER_INSTRUCTIONS_START = "<!-- taskchef:dispatcher-instructions:start -->";
|
|
39
36
|
const DISPATCHER_INSTRUCTIONS_END = "<!-- taskchef:dispatcher-instructions:end -->";
|
|
40
|
-
const TASKCHEF_SKILL_NAMES = [
|
|
41
|
-
"taskchef-bootstrap",
|
|
42
|
-
"taskchef-delegate",
|
|
43
|
-
"taskchef-report",
|
|
44
|
-
];
|
|
45
|
-
const LEGACY_TASKCHEF_SKILL_NAMES = [...TASKCHEF_SKILL_NAMES, "taskchef-reconcile"];
|
|
46
|
-
const SKILLS_SOURCE_ROOT = fileURLToPath(new URL("../skills/", import.meta.url));
|
|
47
37
|
const DISPATCH_FILE_NAME = "tasks.jsonl";
|
|
48
38
|
const WORKSPACE_LOCK_NAME = ".taskchef-workspace.lock";
|
|
49
39
|
const SAFE_ID = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
50
40
|
const CURRENT_CONFIG_SCHEMA_VERSION = 2;
|
|
51
|
-
const CURRENT_TASK_SCHEMA_VERSION =
|
|
52
|
-
const
|
|
53
|
-
const LEGACY_SCHEMA_VERSION = 1;
|
|
54
|
-
const PREVIOUS_SCHEMA_VERSION = 2;
|
|
41
|
+
const CURRENT_TASK_SCHEMA_VERSION = 5;
|
|
42
|
+
const PREVIOUS_SELF_LINKING_TASK_SCHEMA_VERSION = 4;
|
|
55
43
|
const CONFIG_FIELDS = new Set(["schemaVersion", "projects"]);
|
|
56
44
|
const PROJECT_FIELDS = new Set([
|
|
57
45
|
"name",
|
|
58
46
|
"path",
|
|
59
47
|
"isGitRepository",
|
|
60
|
-
"githubRepo",
|
|
61
48
|
"githubRepos",
|
|
62
49
|
"description",
|
|
63
50
|
]);
|
|
64
51
|
const PROJECT_INPUT_FIELDS = new Set(["name", "path", "githubRepos", "description"]);
|
|
65
|
-
const
|
|
52
|
+
const STATEFUL_DISPATCH_FIELDS = new Set([
|
|
66
53
|
"schemaVersion",
|
|
67
54
|
"id",
|
|
68
55
|
"project",
|
|
@@ -76,15 +63,7 @@ const DISPATCH_FIELDS = new Set([
|
|
|
76
63
|
"updatedAt",
|
|
77
64
|
"updatedBy",
|
|
78
65
|
]);
|
|
79
|
-
const
|
|
80
|
-
"schemaVersion",
|
|
81
|
-
"id",
|
|
82
|
-
"project",
|
|
83
|
-
"title",
|
|
84
|
-
"instruction",
|
|
85
|
-
"threadId",
|
|
86
|
-
"createdAt",
|
|
87
|
-
]);
|
|
66
|
+
const DISPATCH_FIELDS = new Set([...STATEFUL_DISPATCH_FIELDS, "lastResult"]);
|
|
88
67
|
const RECORD_DISPATCH_FIELDS = new Set([
|
|
89
68
|
"id",
|
|
90
69
|
"project",
|
|
@@ -94,8 +73,9 @@ const RECORD_DISPATCH_FIELDS = new Set([
|
|
|
94
73
|
]);
|
|
95
74
|
const RESULT_STATUSES = new Set(["needs_input", "completed", "failed"]);
|
|
96
75
|
const TASK_STATUSES = new Set(["working", ...RESULT_STATUSES]);
|
|
97
|
-
const TASK_UPDATE_SOURCES = new Set(["dispatcher", "
|
|
76
|
+
const TASK_UPDATE_SOURCES = new Set(["dispatcher", "mcp"]);
|
|
98
77
|
const MAX_RESULT_SUMMARY_LENGTH = 2_000;
|
|
78
|
+
const LAST_RESULT_FIELDS = new Set(["status", "summary", "turnId", "updatedAt"]);
|
|
99
79
|
|
|
100
80
|
function requireExactFields(value, fields, name) {
|
|
101
81
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -123,6 +103,13 @@ function requireTimestamp(value, name) {
|
|
|
123
103
|
return value;
|
|
124
104
|
}
|
|
125
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
|
+
|
|
126
113
|
function optionalString(value, name, { maxLength = null } = {}) {
|
|
127
114
|
if (value === null) return null;
|
|
128
115
|
const normalized = requireString(value, name).trim();
|
|
@@ -347,75 +334,6 @@ export async function ensureWorkspaceInstructions(workspaceRoot) {
|
|
|
347
334
|
return { path: filePath, action };
|
|
348
335
|
}
|
|
349
336
|
|
|
350
|
-
async function findLegacySkillLinks(workspaceRoot) {
|
|
351
|
-
const agentsDirectory = path.join(workspaceRoot, ".agents");
|
|
352
|
-
const agentsDetails = await lstat(agentsDirectory).catch((error) => {
|
|
353
|
-
if (error.code === "ENOENT") return null;
|
|
354
|
-
throw error;
|
|
355
|
-
});
|
|
356
|
-
if (agentsDetails === null) return { agentsDirectory: null, skillsDirectory: null, links: [] };
|
|
357
|
-
if (agentsDetails.isSymbolicLink() || !agentsDetails.isDirectory()) {
|
|
358
|
-
return { agentsDirectory: null, skillsDirectory: null, links: [] };
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
const skillsDirectory = path.join(agentsDirectory, "skills");
|
|
362
|
-
const skillsDetails = await lstat(skillsDirectory).catch((error) => {
|
|
363
|
-
if (error.code === "ENOENT") return null;
|
|
364
|
-
throw error;
|
|
365
|
-
});
|
|
366
|
-
if (skillsDetails === null) return { agentsDirectory, skillsDirectory, links: [] };
|
|
367
|
-
if (skillsDetails.isSymbolicLink() || !skillsDetails.isDirectory()) {
|
|
368
|
-
return { agentsDirectory, skillsDirectory: null, links: [] };
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
const links = [];
|
|
372
|
-
for (const skillName of LEGACY_TASKCHEF_SKILL_NAMES) {
|
|
373
|
-
const skillPath = path.join(skillsDirectory, skillName);
|
|
374
|
-
const details = await lstat(skillPath).catch((error) => {
|
|
375
|
-
if (error.code === "ENOENT") return null;
|
|
376
|
-
throw error;
|
|
377
|
-
});
|
|
378
|
-
if (details === null) continue;
|
|
379
|
-
if (!details.isSymbolicLink()) {
|
|
380
|
-
throw new Error(`legacy TaskChef skill path is not a symlink: ${skillPath}`);
|
|
381
|
-
}
|
|
382
|
-
links.push({ name: skillName, path: skillPath });
|
|
383
|
-
}
|
|
384
|
-
return { agentsDirectory, skillsDirectory, links };
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
async function removeLegacySkillLinks(workspaceRoot) {
|
|
388
|
-
const legacy = await findLegacySkillLinks(workspaceRoot);
|
|
389
|
-
for (const link of legacy.links) await unlink(link.path);
|
|
390
|
-
const removedDirectories = [];
|
|
391
|
-
if (legacy.skillsDirectory && (await readdir(legacy.skillsDirectory)).length === 0) {
|
|
392
|
-
await rmdir(legacy.skillsDirectory);
|
|
393
|
-
removedDirectories.push(legacy.skillsDirectory);
|
|
394
|
-
}
|
|
395
|
-
if (legacy.agentsDirectory && (await readdir(legacy.agentsDirectory).catch((error) => {
|
|
396
|
-
if (error.code === "ENOENT") return null;
|
|
397
|
-
throw error;
|
|
398
|
-
}))?.length === 0) {
|
|
399
|
-
await rmdir(legacy.agentsDirectory);
|
|
400
|
-
removedDirectories.push(legacy.agentsDirectory);
|
|
401
|
-
}
|
|
402
|
-
return { removed: legacy.links, removedDirectories };
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
export async function ensureWorkspaceSkills(workspaceRoot) {
|
|
406
|
-
const root = await realpath(path.resolve(workspaceRoot));
|
|
407
|
-
const legacySkills = await removeLegacySkillLinks(root);
|
|
408
|
-
return {
|
|
409
|
-
directory: null,
|
|
410
|
-
skills: TASKCHEF_SKILL_NAMES.map((name) => ({
|
|
411
|
-
name,
|
|
412
|
-
path: path.join(SKILLS_SOURCE_ROOT, name),
|
|
413
|
-
action: "provided-by-plugin",
|
|
414
|
-
})),
|
|
415
|
-
legacySkills,
|
|
416
|
-
};
|
|
417
|
-
}
|
|
418
|
-
|
|
419
337
|
export async function canonicalGitRoot(projectPath) {
|
|
420
338
|
const requested = await canonicalDirectory(projectPath);
|
|
421
339
|
const { stdout } = await execFile("git", ["rev-parse", "--show-toplevel"], {
|
|
@@ -445,7 +363,7 @@ export async function canonicalDirectory(projectPath) {
|
|
|
445
363
|
async function normalizeProject(
|
|
446
364
|
project,
|
|
447
365
|
index,
|
|
448
|
-
{ checkPath = true
|
|
366
|
+
{ checkPath = true } = {},
|
|
449
367
|
) {
|
|
450
368
|
const field = `projects[${index}]`;
|
|
451
369
|
if (!project || typeof project !== "object" || Array.isArray(project)) {
|
|
@@ -453,12 +371,7 @@ async function normalizeProject(
|
|
|
453
371
|
}
|
|
454
372
|
const unexpected = Object.keys(project).find((key) => !PROJECT_FIELDS.has(key));
|
|
455
373
|
if (unexpected) throw new Error(`${field} has unsupported field: ${unexpected}`);
|
|
456
|
-
const
|
|
457
|
-
const unsupportedRepositoryField = allowLegacyGithubRepo ? "githubRepos" : "githubRepo";
|
|
458
|
-
if (unsupportedRepositoryField in project) {
|
|
459
|
-
throw new Error(`${field} has unsupported field: ${unsupportedRepositoryField}`);
|
|
460
|
-
}
|
|
461
|
-
for (const required of ["name", "path", "isGitRepository", repositoryField]) {
|
|
374
|
+
for (const required of ["name", "path", "isGitRepository", "githubRepos"]) {
|
|
462
375
|
if (!(required in project)) throw new Error(`${field} is missing field: ${required}`);
|
|
463
376
|
}
|
|
464
377
|
const name = requireString(project.name, `${field}.name`).trim();
|
|
@@ -476,9 +389,7 @@ async function normalizeProject(
|
|
|
476
389
|
throw new Error(`${field}.path must be a normalized absolute path`);
|
|
477
390
|
}
|
|
478
391
|
}
|
|
479
|
-
const githubRepos = normalizeGithubRepositories(project
|
|
480
|
-
allowLegacyScalar: allowLegacyGithubRepo,
|
|
481
|
-
});
|
|
392
|
+
const githubRepos = normalizeGithubRepositories(project.githubRepos, `${field}.githubRepos`);
|
|
482
393
|
const normalized = {
|
|
483
394
|
name,
|
|
484
395
|
path: projectPath,
|
|
@@ -496,14 +407,13 @@ async function normalizeProject(
|
|
|
496
407
|
|
|
497
408
|
async function normalizeProjects(
|
|
498
409
|
projects,
|
|
499
|
-
{ checkPaths = true
|
|
410
|
+
{ checkPaths = true } = {},
|
|
500
411
|
) {
|
|
501
412
|
if (!Array.isArray(projects)) throw new Error("projects must be an array");
|
|
502
413
|
const normalized = [];
|
|
503
414
|
for (const [index, project] of projects.entries()) {
|
|
504
415
|
normalized.push(await normalizeProject(project, index, {
|
|
505
416
|
checkPath: checkPaths,
|
|
506
|
-
allowLegacyGithubRepo,
|
|
507
417
|
}));
|
|
508
418
|
}
|
|
509
419
|
if (new Set(normalized.map((project) => project.path)).size !== normalized.length) {
|
|
@@ -594,15 +504,12 @@ async function inspectProject(input, index = 0) {
|
|
|
594
504
|
|
|
595
505
|
export async function validateConfig(config, { checkPaths = true } = {}) {
|
|
596
506
|
requireExactFields(config, CONFIG_FIELDS, "taskchef.json");
|
|
597
|
-
if (
|
|
507
|
+
if (config.schemaVersion !== CURRENT_CONFIG_SCHEMA_VERSION) {
|
|
598
508
|
throw new Error("unsupported configuration schemaVersion");
|
|
599
509
|
}
|
|
600
510
|
return {
|
|
601
511
|
schemaVersion: CURRENT_CONFIG_SCHEMA_VERSION,
|
|
602
|
-
projects: await normalizeProjects(config.projects, {
|
|
603
|
-
checkPaths,
|
|
604
|
-
allowLegacyGithubRepo: config.schemaVersion === LEGACY_SCHEMA_VERSION,
|
|
605
|
-
}),
|
|
512
|
+
projects: await normalizeProjects(config.projects, { checkPaths }),
|
|
606
513
|
};
|
|
607
514
|
}
|
|
608
515
|
|
|
@@ -630,9 +537,6 @@ export async function initializeWorkspace(workspaceRoot) {
|
|
|
630
537
|
return withWorkspaceLock(root, async () => {
|
|
631
538
|
const configPath = path.join(root, "taskchef.json");
|
|
632
539
|
const configExists = await managedRegularFileExists(configPath);
|
|
633
|
-
const storedConfigVersion = configExists
|
|
634
|
-
? JSON.parse(await readFile(configPath, "utf8")).schemaVersion
|
|
635
|
-
: null;
|
|
636
540
|
const config = configExists
|
|
637
541
|
? await readConfig(root, { checkPaths: false })
|
|
638
542
|
: { schemaVersion: CURRENT_CONFIG_SCHEMA_VERSION, projects: [] };
|
|
@@ -640,11 +544,7 @@ export async function initializeWorkspace(workspaceRoot) {
|
|
|
640
544
|
if (configExists && (await managedRegularFileExists(dispatchPath))) {
|
|
641
545
|
await readDispatchesUnlocked(root);
|
|
642
546
|
}
|
|
643
|
-
const { legacySkills } = await ensureWorkspaceSkills(root);
|
|
644
547
|
if (!configExists) await writeJsonAtomic(configPath, config, { exclusive: true });
|
|
645
|
-
else if (storedConfigVersion !== CURRENT_CONFIG_SCHEMA_VERSION) {
|
|
646
|
-
await writeJsonAtomic(configPath, config);
|
|
647
|
-
}
|
|
648
548
|
const tasks = await ensureDispatchFile(root);
|
|
649
549
|
const instructions = await ensureWorkspaceInstructions(root).catch(async (error) => {
|
|
650
550
|
if (!configExists) await unlink(configPath).catch(() => {});
|
|
@@ -656,14 +556,11 @@ export async function initializeWorkspace(workspaceRoot) {
|
|
|
656
556
|
workspace: root,
|
|
657
557
|
config: {
|
|
658
558
|
path: configPath,
|
|
659
|
-
action:
|
|
660
|
-
? "created"
|
|
661
|
-
: storedConfigVersion === CURRENT_CONFIG_SCHEMA_VERSION ? "unchanged" : "migrated",
|
|
559
|
+
action: configExists ? "unchanged" : "created",
|
|
662
560
|
value: config,
|
|
663
561
|
},
|
|
664
562
|
tasks,
|
|
665
563
|
instructions,
|
|
666
|
-
legacySkills,
|
|
667
564
|
};
|
|
668
565
|
});
|
|
669
566
|
}
|
|
@@ -780,15 +677,9 @@ export async function removeProject(workspaceRoot, name) {
|
|
|
780
677
|
});
|
|
781
678
|
}
|
|
782
679
|
|
|
783
|
-
async function validateDispatchShape(
|
|
784
|
-
dispatch,
|
|
785
|
-
name = "task",
|
|
786
|
-
{ allowLegacyHeading = false } = {},
|
|
787
|
-
) {
|
|
680
|
+
async function validateDispatchShape(dispatch, name = "task") {
|
|
788
681
|
const supportedVersions = [
|
|
789
|
-
|
|
790
|
-
PREVIOUS_SCHEMA_VERSION,
|
|
791
|
-
PREVIOUS_STATEFUL_TASK_SCHEMA_VERSION,
|
|
682
|
+
PREVIOUS_SELF_LINKING_TASK_SCHEMA_VERSION,
|
|
792
683
|
CURRENT_TASK_SCHEMA_VERSION,
|
|
793
684
|
];
|
|
794
685
|
if (!supportedVersions.includes(dispatch?.schemaVersion)) {
|
|
@@ -796,16 +687,51 @@ async function validateDispatchShape(
|
|
|
796
687
|
}
|
|
797
688
|
requireExactFields(
|
|
798
689
|
dispatch,
|
|
799
|
-
dispatch.schemaVersion >=
|
|
690
|
+
dispatch.schemaVersion >= CURRENT_TASK_SCHEMA_VERSION
|
|
800
691
|
? DISPATCH_FIELDS
|
|
801
|
-
:
|
|
692
|
+
: STATEFUL_DISPATCH_FIELDS,
|
|
802
693
|
name,
|
|
803
694
|
);
|
|
804
695
|
const id = requireSafeId(dispatch.id, `${name}.id`);
|
|
805
|
-
const project = await normalizeProject(dispatch.project, 0, {
|
|
806
|
-
|
|
807
|
-
|
|
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,
|
|
808
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
|
+
}
|
|
809
735
|
const normalized = {
|
|
810
736
|
schemaVersion: dispatch.schemaVersion,
|
|
811
737
|
id,
|
|
@@ -816,39 +742,101 @@ async function validateDispatchShape(
|
|
|
816
742
|
? null
|
|
817
743
|
: normalizeDurableThreadId(dispatch.threadId, `${name}.threadId`),
|
|
818
744
|
createdAt: requireTimestamp(dispatch.createdAt, `${name}.createdAt`),
|
|
819
|
-
status
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
})
|
|
826
|
-
: null,
|
|
827
|
-
turnId: dispatch.schemaVersion >= PREVIOUS_STATEFUL_TASK_SCHEMA_VERSION
|
|
828
|
-
? optionalString(dispatch.turnId, `${name}.turnId`, { maxLength: 256 })
|
|
829
|
-
: null,
|
|
830
|
-
updatedAt: dispatch.schemaVersion >= PREVIOUS_STATEFUL_TASK_SCHEMA_VERSION
|
|
831
|
-
? requireTimestamp(dispatch.updatedAt, `${name}.updatedAt`)
|
|
832
|
-
: null,
|
|
833
|
-
updatedBy: dispatch.schemaVersion >= PREVIOUS_STATEFUL_TASK_SCHEMA_VERSION
|
|
834
|
-
? requireEnum(dispatch.updatedBy, TASK_UPDATE_SOURCES, `${name}.updatedBy`)
|
|
835
|
-
: null,
|
|
745
|
+
status,
|
|
746
|
+
summary,
|
|
747
|
+
turnId,
|
|
748
|
+
updatedAt,
|
|
749
|
+
updatedBy: requireEnum(dispatch.updatedBy, TASK_UPDATE_SOURCES, `${name}.updatedBy`),
|
|
750
|
+
lastResult,
|
|
836
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
|
+
}
|
|
837
790
|
if (normalized.status === "working" && normalized.summary !== null) {
|
|
838
791
|
throw new Error(`${name}.summary must be null while status is working`);
|
|
839
792
|
}
|
|
840
793
|
if (RESULT_STATUSES.has(normalized.status) && normalized.summary === null) {
|
|
841
794
|
throw new Error(`${name}.summary is required for status ${normalized.status}`);
|
|
842
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
|
+
}
|
|
843
832
|
if (
|
|
844
|
-
normalized.updatedAt
|
|
845
|
-
&& Date.parse(normalized.updatedAt) < Date.parse(normalized.createdAt)
|
|
833
|
+
Date.parse(normalized.updatedAt) < Date.parse(normalized.createdAt)
|
|
846
834
|
) {
|
|
847
835
|
throw new Error(`${name}.updatedAt must not be earlier than createdAt`);
|
|
848
836
|
}
|
|
849
837
|
if (
|
|
850
838
|
normalized.threadId === null &&
|
|
851
|
-
parseTaskChefMarker(normalized.instruction
|
|
839
|
+
parseTaskChefMarker(normalized.instruction) !== normalized.id
|
|
852
840
|
) {
|
|
853
841
|
throw new Error(`${name} with a null threadId must contain its exact TaskChef marker`);
|
|
854
842
|
}
|
|
@@ -875,9 +863,7 @@ async function parseDispatchRecordsUnlocked(root, content) {
|
|
|
875
863
|
records.push({
|
|
876
864
|
line,
|
|
877
865
|
raw: value,
|
|
878
|
-
normalized: await validateDispatchShape(value, `task line ${index + 1}
|
|
879
|
-
allowLegacyHeading: true,
|
|
880
|
-
}),
|
|
866
|
+
normalized: await validateDispatchShape(value, `task line ${index + 1}`),
|
|
881
867
|
});
|
|
882
868
|
}
|
|
883
869
|
const ids = new Set();
|
|
@@ -947,6 +933,7 @@ export async function recordTask(workspaceRoot, input, { now } = {}) {
|
|
|
947
933
|
turnId: null,
|
|
948
934
|
updatedAt: createdAt,
|
|
949
935
|
updatedBy: "dispatcher",
|
|
936
|
+
lastResult: null,
|
|
950
937
|
});
|
|
951
938
|
const existing = await readDispatchesUnlocked(root);
|
|
952
939
|
if (existing.some((item) => item.id === dispatch.id)) {
|
|
@@ -972,53 +959,6 @@ export async function recordTask(workspaceRoot, input, { now } = {}) {
|
|
|
972
959
|
});
|
|
973
960
|
}
|
|
974
961
|
|
|
975
|
-
export async function resolveTask(workspaceRoot, taskId, threadId, { now } = {}) {
|
|
976
|
-
const id = requireSafeId(taskId, "taskId");
|
|
977
|
-
const durableThreadId = normalizeDurableThreadId(threadId);
|
|
978
|
-
const root = await realpath(path.resolve(workspaceRoot));
|
|
979
|
-
return withWorkspaceLock(root, async () => {
|
|
980
|
-
const records = await readDispatchRecordsUnlocked(root);
|
|
981
|
-
const dispatches = records.map((record) => record.normalized);
|
|
982
|
-
const index = dispatches.findIndex((dispatch) => dispatch.id === id);
|
|
983
|
-
if (index === -1) throw new Error(`task not found: ${id}`);
|
|
984
|
-
const currentRecord = records[index];
|
|
985
|
-
if (currentRecord.raw.schemaVersion >= CURRENT_TASK_SCHEMA_VERSION) {
|
|
986
|
-
throw new Error(`task resolve is only available for legacy pre-self-linking records: ${id}`);
|
|
987
|
-
}
|
|
988
|
-
const dispatch = dispatches[index];
|
|
989
|
-
if (dispatch.threadId === durableThreadId) return dispatch;
|
|
990
|
-
if (dispatch.threadId !== null) {
|
|
991
|
-
throw new Error(`task already has a different threadId: ${id}`);
|
|
992
|
-
}
|
|
993
|
-
if (parseTaskChefMarker(dispatch.instruction) !== dispatch.id) {
|
|
994
|
-
throw new Error(`task instruction does not contain its exact TaskChef marker: ${id}`);
|
|
995
|
-
}
|
|
996
|
-
if (dispatches.some((item) => (
|
|
997
|
-
item.threadId !== null
|
|
998
|
-
&& threadIdentityKey(item.threadId) === threadIdentityKey(durableThreadId)
|
|
999
|
-
))) {
|
|
1000
|
-
throw new Error(`threadId is already recorded: ${durableThreadId}`);
|
|
1001
|
-
}
|
|
1002
|
-
const statefulLegacy = currentRecord.raw.schemaVersion === PREVIOUS_STATEFUL_TASK_SCHEMA_VERSION;
|
|
1003
|
-
const rawResolved = statefulLegacy
|
|
1004
|
-
? {
|
|
1005
|
-
...currentRecord.raw,
|
|
1006
|
-
threadId: durableThreadId,
|
|
1007
|
-
updatedAt: now ?? new Date().toISOString(),
|
|
1008
|
-
updatedBy: "dispatcher",
|
|
1009
|
-
}
|
|
1010
|
-
: { ...currentRecord.raw, threadId: durableThreadId };
|
|
1011
|
-
const resolved = statefulLegacy
|
|
1012
|
-
? await validateDispatchShape(rawResolved)
|
|
1013
|
-
: { ...dispatch, threadId: durableThreadId };
|
|
1014
|
-
const lines = records.map((record, recordIndex) => recordIndex === index
|
|
1015
|
-
? JSON.stringify(rawResolved)
|
|
1016
|
-
: record.line);
|
|
1017
|
-
await writeDispatchLinesAtomic(root, lines);
|
|
1018
|
-
return resolved;
|
|
1019
|
-
});
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
962
|
export async function linkTask(workspaceRoot, taskId, threadId, { now } = {}) {
|
|
1023
963
|
const id = requireSafeId(taskId, "taskId");
|
|
1024
964
|
const durableThreadId = normalizeCodexThreadId(threadId);
|
|
@@ -1028,10 +968,6 @@ export async function linkTask(workspaceRoot, taskId, threadId, { now } = {}) {
|
|
|
1028
968
|
const dispatches = records.map((record) => record.normalized);
|
|
1029
969
|
const index = dispatches.findIndex((dispatch) => dispatch.id === id);
|
|
1030
970
|
if (index === -1) throw new Error(`task not found: ${id}`);
|
|
1031
|
-
const currentRecord = records[index];
|
|
1032
|
-
if (currentRecord.raw.schemaVersion < CURRENT_TASK_SCHEMA_VERSION) {
|
|
1033
|
-
throw new Error(`link_task accepts only self-linking task records: ${id}`);
|
|
1034
|
-
}
|
|
1035
971
|
const dispatch = dispatches[index];
|
|
1036
972
|
const sameIdentity = dispatch.threadId?.toLowerCase() === durableThreadId;
|
|
1037
973
|
if (sameIdentity && dispatch.updatedBy === "mcp") {
|
|
@@ -1041,8 +977,9 @@ export async function linkTask(workspaceRoot, taskId, threadId, { now } = {}) {
|
|
|
1041
977
|
if (dispatch.threadId === durableThreadId) return dispatch;
|
|
1042
978
|
const canonical = await validateDispatchShape({
|
|
1043
979
|
...dispatch,
|
|
980
|
+
schemaVersion: CURRENT_TASK_SCHEMA_VERSION,
|
|
1044
981
|
threadId: durableThreadId,
|
|
1045
|
-
updatedAt: now
|
|
982
|
+
updatedAt: transitionTimestamp(now, dispatch.updatedAt),
|
|
1046
983
|
updatedBy: "mcp",
|
|
1047
984
|
});
|
|
1048
985
|
const lines = records.map((record, recordIndex) => recordIndex === index
|
|
@@ -1071,8 +1008,9 @@ export async function linkTask(workspaceRoot, taskId, threadId, { now } = {}) {
|
|
|
1071
1008
|
}
|
|
1072
1009
|
const linked = await validateDispatchShape({
|
|
1073
1010
|
...dispatch,
|
|
1011
|
+
schemaVersion: CURRENT_TASK_SCHEMA_VERSION,
|
|
1074
1012
|
threadId: durableThreadId,
|
|
1075
|
-
updatedAt: now
|
|
1013
|
+
updatedAt: transitionTimestamp(now, dispatch.updatedAt),
|
|
1076
1014
|
updatedBy: "mcp",
|
|
1077
1015
|
});
|
|
1078
1016
|
const lines = records.map((record, recordIndex) => recordIndex === index
|
|
@@ -1091,22 +1029,53 @@ function dispatchLineWithState(dispatch, patch) {
|
|
|
1091
1029
|
});
|
|
1092
1030
|
}
|
|
1093
1031
|
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
new
|
|
1098
|
-
|
|
1099
|
-
);
|
|
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
|
+
}
|
|
1100
1042
|
const id = requireSafeId(input.taskId, "taskId");
|
|
1101
1043
|
const threadId = input.threadId === null
|
|
1102
1044
|
? null
|
|
1103
1045
|
: normalizeDurableThreadId(input.threadId, "threadId");
|
|
1104
1046
|
const turnId = optionalString(input.turnId, "turnId", { maxLength: 256 });
|
|
1105
|
-
const status = requireEnum(
|
|
1106
|
-
|
|
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", {
|
|
1107
1053
|
maxLength: MAX_RESULT_SUMMARY_LENGTH,
|
|
1108
1054
|
});
|
|
1109
|
-
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
|
+
});
|
|
1110
1079
|
const root = await realpath(path.resolve(workspaceRoot));
|
|
1111
1080
|
return withWorkspaceLock(root, async () => {
|
|
1112
1081
|
const records = await readDispatchRecordsUnlocked(root);
|
|
@@ -1115,60 +1084,142 @@ export async function reportTaskResult(workspaceRoot, input, { now } = {}) {
|
|
|
1115
1084
|
if (index === -1) throw new Error(`task not found: ${id}`);
|
|
1116
1085
|
const dispatch = dispatches[index];
|
|
1117
1086
|
const isSelfLinkingJourney = (
|
|
1118
|
-
|
|
1119
|
-
&& dispatch.threadId !== null
|
|
1087
|
+
dispatch.threadId !== null
|
|
1120
1088
|
&& parseTaskChefMarker(dispatch.instruction) === dispatch.id
|
|
1121
1089
|
);
|
|
1122
|
-
|
|
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;
|
|
1123
1098
|
if (isSelfLinkingJourney) {
|
|
1124
1099
|
if (dispatch.updatedBy === "dispatcher") {
|
|
1125
1100
|
throw new Error(`self-linking task must link before reporting a result: ${id}`);
|
|
1126
1101
|
}
|
|
1127
|
-
|
|
1102
|
+
stateTurnId = normalizeCodexThreadId(turnId, "turnId");
|
|
1128
1103
|
}
|
|
1129
1104
|
if (dispatch.threadId === null) {
|
|
1130
|
-
if (threadId !== null ||
|
|
1105
|
+
if (threadId !== null || stateTurnId !== null || status !== "failed") {
|
|
1131
1106
|
throw new Error(`task without a durable threadId accepts only failed with null thread/turn IDs: ${id}`);
|
|
1132
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
|
+
}
|
|
1133
1131
|
} else {
|
|
1134
1132
|
if (threadIdentityKey(threadId) !== threadIdentityKey(dispatch.threadId)) {
|
|
1135
1133
|
throw new Error(`task result threadId does not match recorded threadId: ${id}`);
|
|
1136
1134
|
}
|
|
1137
|
-
if (
|
|
1138
|
-
throw new Error(`task
|
|
1135
|
+
if (stateTurnId === null) {
|
|
1136
|
+
throw new Error(`task state turnId is required for a linked task: ${id}`);
|
|
1139
1137
|
}
|
|
1140
1138
|
}
|
|
1141
|
-
if (
|
|
1142
|
-
if (status ===
|
|
1143
|
-
|
|
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;
|
|
1144
1165
|
}
|
|
1145
1166
|
if (
|
|
1146
|
-
|
|
1147
|
-
&& dispatch.turnId
|
|
1148
|
-
&&
|
|
1167
|
+
dispatch.status === status
|
|
1168
|
+
&& dispatch.turnId === stateTurnId
|
|
1169
|
+
&& dispatch.summary === summary
|
|
1170
|
+
&& sameLastResult(dispatch.lastResult, { status, summary, turnId: stateTurnId })
|
|
1149
1171
|
) {
|
|
1150
|
-
|
|
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}`);
|
|
1151
1176
|
}
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
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 = {
|
|
1156
1194
|
...dispatch,
|
|
1157
|
-
schemaVersion:
|
|
1195
|
+
schemaVersion: CURRENT_TASK_SCHEMA_VERSION,
|
|
1158
1196
|
status,
|
|
1159
1197
|
summary,
|
|
1160
|
-
turnId:
|
|
1161
|
-
updatedAt
|
|
1198
|
+
turnId: stateTurnId,
|
|
1199
|
+
updatedAt,
|
|
1162
1200
|
updatedBy: "mcp",
|
|
1163
|
-
|
|
1201
|
+
lastResult,
|
|
1202
|
+
};
|
|
1203
|
+
const updated = await validateDispatchShape(candidate);
|
|
1164
1204
|
const lines = records.map((record, recordIndex) => recordIndex === index
|
|
1165
|
-
? dispatchLineWithState(updated, {
|
|
1205
|
+
? dispatchLineWithState(updated, {})
|
|
1166
1206
|
: record.line);
|
|
1167
1207
|
await writeDispatchLinesAtomic(root, lines);
|
|
1168
1208
|
return updated;
|
|
1169
1209
|
});
|
|
1170
1210
|
}
|
|
1171
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
|
+
|
|
1172
1223
|
export async function readTask(workspaceRoot, taskId) {
|
|
1173
1224
|
const id = requireSafeId(taskId, "taskId");
|
|
1174
1225
|
const dispatch = (await listTasks(workspaceRoot)).find((item) => item.id === id);
|
|
@@ -1234,12 +1285,5 @@ export async function doctorWorkspace(workspaceRoot) {
|
|
|
1234
1285
|
}
|
|
1235
1286
|
return "managed AGENTS.md instructions current";
|
|
1236
1287
|
});
|
|
1237
|
-
await check("legacy-skill-links", async () => {
|
|
1238
|
-
const legacy = await findLegacySkillLinks(root);
|
|
1239
|
-
if (legacy.links.length > 0) {
|
|
1240
|
-
throw new Error("legacy TaskChef skill links remain; run workspace init to remove them");
|
|
1241
|
-
}
|
|
1242
|
-
return "no legacy TaskChef skill links";
|
|
1243
|
-
});
|
|
1244
1288
|
return { workspace: root, ok: checks.every((item) => item.status === "pass"), checks };
|
|
1245
1289
|
}
|