taskchef 6.1.2 → 7.0.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 +96 -454
- package/docs/firstmate-taskchef-comparison.md +277 -0
- package/docs/spec.md +246 -0
- package/docs/workflows.md +236 -0
- package/index.js +0 -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 +2 -9
- package/skills/taskchef-report/SKILL.md +4 -8
- package/src/cli.js +0 -21
- package/src/dashboard/app.js +6 -1
- package/src/dashboard/index.html +4 -1
- package/src/dashboard/styles.css +4 -1
- package/src/dashboard.js +1 -1
- package/src/delegation.js +4 -8
- package/src/github.js +0 -4
- package/src/mcp.js +5 -5
- package/src/workspace.js +26 -233
- 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,27 +34,16 @@ 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
41
|
const CURRENT_TASK_SCHEMA_VERSION = 4;
|
|
52
|
-
const PREVIOUS_STATEFUL_TASK_SCHEMA_VERSION = 3;
|
|
53
|
-
const LEGACY_SCHEMA_VERSION = 1;
|
|
54
|
-
const PREVIOUS_SCHEMA_VERSION = 2;
|
|
55
42
|
const CONFIG_FIELDS = new Set(["schemaVersion", "projects"]);
|
|
56
43
|
const PROJECT_FIELDS = new Set([
|
|
57
44
|
"name",
|
|
58
45
|
"path",
|
|
59
46
|
"isGitRepository",
|
|
60
|
-
"githubRepo",
|
|
61
47
|
"githubRepos",
|
|
62
48
|
"description",
|
|
63
49
|
]);
|
|
@@ -76,15 +62,6 @@ const DISPATCH_FIELDS = new Set([
|
|
|
76
62
|
"updatedAt",
|
|
77
63
|
"updatedBy",
|
|
78
64
|
]);
|
|
79
|
-
const LEGACY_DISPATCH_FIELDS = new Set([
|
|
80
|
-
"schemaVersion",
|
|
81
|
-
"id",
|
|
82
|
-
"project",
|
|
83
|
-
"title",
|
|
84
|
-
"instruction",
|
|
85
|
-
"threadId",
|
|
86
|
-
"createdAt",
|
|
87
|
-
]);
|
|
88
65
|
const RECORD_DISPATCH_FIELDS = new Set([
|
|
89
66
|
"id",
|
|
90
67
|
"project",
|
|
@@ -94,7 +71,7 @@ const RECORD_DISPATCH_FIELDS = new Set([
|
|
|
94
71
|
]);
|
|
95
72
|
const RESULT_STATUSES = new Set(["needs_input", "completed", "failed"]);
|
|
96
73
|
const TASK_STATUSES = new Set(["working", ...RESULT_STATUSES]);
|
|
97
|
-
const TASK_UPDATE_SOURCES = new Set(["dispatcher", "
|
|
74
|
+
const TASK_UPDATE_SOURCES = new Set(["dispatcher", "mcp"]);
|
|
98
75
|
const MAX_RESULT_SUMMARY_LENGTH = 2_000;
|
|
99
76
|
|
|
100
77
|
function requireExactFields(value, fields, name) {
|
|
@@ -347,75 +324,6 @@ export async function ensureWorkspaceInstructions(workspaceRoot) {
|
|
|
347
324
|
return { path: filePath, action };
|
|
348
325
|
}
|
|
349
326
|
|
|
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
327
|
export async function canonicalGitRoot(projectPath) {
|
|
420
328
|
const requested = await canonicalDirectory(projectPath);
|
|
421
329
|
const { stdout } = await execFile("git", ["rev-parse", "--show-toplevel"], {
|
|
@@ -445,7 +353,7 @@ export async function canonicalDirectory(projectPath) {
|
|
|
445
353
|
async function normalizeProject(
|
|
446
354
|
project,
|
|
447
355
|
index,
|
|
448
|
-
{ checkPath = true
|
|
356
|
+
{ checkPath = true } = {},
|
|
449
357
|
) {
|
|
450
358
|
const field = `projects[${index}]`;
|
|
451
359
|
if (!project || typeof project !== "object" || Array.isArray(project)) {
|
|
@@ -453,12 +361,7 @@ async function normalizeProject(
|
|
|
453
361
|
}
|
|
454
362
|
const unexpected = Object.keys(project).find((key) => !PROJECT_FIELDS.has(key));
|
|
455
363
|
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]) {
|
|
364
|
+
for (const required of ["name", "path", "isGitRepository", "githubRepos"]) {
|
|
462
365
|
if (!(required in project)) throw new Error(`${field} is missing field: ${required}`);
|
|
463
366
|
}
|
|
464
367
|
const name = requireString(project.name, `${field}.name`).trim();
|
|
@@ -476,9 +379,7 @@ async function normalizeProject(
|
|
|
476
379
|
throw new Error(`${field}.path must be a normalized absolute path`);
|
|
477
380
|
}
|
|
478
381
|
}
|
|
479
|
-
const githubRepos = normalizeGithubRepositories(project
|
|
480
|
-
allowLegacyScalar: allowLegacyGithubRepo,
|
|
481
|
-
});
|
|
382
|
+
const githubRepos = normalizeGithubRepositories(project.githubRepos, `${field}.githubRepos`);
|
|
482
383
|
const normalized = {
|
|
483
384
|
name,
|
|
484
385
|
path: projectPath,
|
|
@@ -496,14 +397,13 @@ async function normalizeProject(
|
|
|
496
397
|
|
|
497
398
|
async function normalizeProjects(
|
|
498
399
|
projects,
|
|
499
|
-
{ checkPaths = true
|
|
400
|
+
{ checkPaths = true } = {},
|
|
500
401
|
) {
|
|
501
402
|
if (!Array.isArray(projects)) throw new Error("projects must be an array");
|
|
502
403
|
const normalized = [];
|
|
503
404
|
for (const [index, project] of projects.entries()) {
|
|
504
405
|
normalized.push(await normalizeProject(project, index, {
|
|
505
406
|
checkPath: checkPaths,
|
|
506
|
-
allowLegacyGithubRepo,
|
|
507
407
|
}));
|
|
508
408
|
}
|
|
509
409
|
if (new Set(normalized.map((project) => project.path)).size !== normalized.length) {
|
|
@@ -594,15 +494,12 @@ async function inspectProject(input, index = 0) {
|
|
|
594
494
|
|
|
595
495
|
export async function validateConfig(config, { checkPaths = true } = {}) {
|
|
596
496
|
requireExactFields(config, CONFIG_FIELDS, "taskchef.json");
|
|
597
|
-
if (
|
|
497
|
+
if (config.schemaVersion !== CURRENT_CONFIG_SCHEMA_VERSION) {
|
|
598
498
|
throw new Error("unsupported configuration schemaVersion");
|
|
599
499
|
}
|
|
600
500
|
return {
|
|
601
501
|
schemaVersion: CURRENT_CONFIG_SCHEMA_VERSION,
|
|
602
|
-
projects: await normalizeProjects(config.projects, {
|
|
603
|
-
checkPaths,
|
|
604
|
-
allowLegacyGithubRepo: config.schemaVersion === LEGACY_SCHEMA_VERSION,
|
|
605
|
-
}),
|
|
502
|
+
projects: await normalizeProjects(config.projects, { checkPaths }),
|
|
606
503
|
};
|
|
607
504
|
}
|
|
608
505
|
|
|
@@ -630,9 +527,6 @@ export async function initializeWorkspace(workspaceRoot) {
|
|
|
630
527
|
return withWorkspaceLock(root, async () => {
|
|
631
528
|
const configPath = path.join(root, "taskchef.json");
|
|
632
529
|
const configExists = await managedRegularFileExists(configPath);
|
|
633
|
-
const storedConfigVersion = configExists
|
|
634
|
-
? JSON.parse(await readFile(configPath, "utf8")).schemaVersion
|
|
635
|
-
: null;
|
|
636
530
|
const config = configExists
|
|
637
531
|
? await readConfig(root, { checkPaths: false })
|
|
638
532
|
: { schemaVersion: CURRENT_CONFIG_SCHEMA_VERSION, projects: [] };
|
|
@@ -640,11 +534,7 @@ export async function initializeWorkspace(workspaceRoot) {
|
|
|
640
534
|
if (configExists && (await managedRegularFileExists(dispatchPath))) {
|
|
641
535
|
await readDispatchesUnlocked(root);
|
|
642
536
|
}
|
|
643
|
-
const { legacySkills } = await ensureWorkspaceSkills(root);
|
|
644
537
|
if (!configExists) await writeJsonAtomic(configPath, config, { exclusive: true });
|
|
645
|
-
else if (storedConfigVersion !== CURRENT_CONFIG_SCHEMA_VERSION) {
|
|
646
|
-
await writeJsonAtomic(configPath, config);
|
|
647
|
-
}
|
|
648
538
|
const tasks = await ensureDispatchFile(root);
|
|
649
539
|
const instructions = await ensureWorkspaceInstructions(root).catch(async (error) => {
|
|
650
540
|
if (!configExists) await unlink(configPath).catch(() => {});
|
|
@@ -656,14 +546,11 @@ export async function initializeWorkspace(workspaceRoot) {
|
|
|
656
546
|
workspace: root,
|
|
657
547
|
config: {
|
|
658
548
|
path: configPath,
|
|
659
|
-
action:
|
|
660
|
-
? "created"
|
|
661
|
-
: storedConfigVersion === CURRENT_CONFIG_SCHEMA_VERSION ? "unchanged" : "migrated",
|
|
549
|
+
action: configExists ? "unchanged" : "created",
|
|
662
550
|
value: config,
|
|
663
551
|
},
|
|
664
552
|
tasks,
|
|
665
553
|
instructions,
|
|
666
|
-
legacySkills,
|
|
667
554
|
};
|
|
668
555
|
});
|
|
669
556
|
}
|
|
@@ -780,34 +667,15 @@ export async function removeProject(workspaceRoot, name) {
|
|
|
780
667
|
});
|
|
781
668
|
}
|
|
782
669
|
|
|
783
|
-
async function validateDispatchShape(
|
|
784
|
-
dispatch
|
|
785
|
-
name = "task",
|
|
786
|
-
{ allowLegacyHeading = false } = {},
|
|
787
|
-
) {
|
|
788
|
-
const supportedVersions = [
|
|
789
|
-
LEGACY_SCHEMA_VERSION,
|
|
790
|
-
PREVIOUS_SCHEMA_VERSION,
|
|
791
|
-
PREVIOUS_STATEFUL_TASK_SCHEMA_VERSION,
|
|
792
|
-
CURRENT_TASK_SCHEMA_VERSION,
|
|
793
|
-
];
|
|
794
|
-
if (!supportedVersions.includes(dispatch?.schemaVersion)) {
|
|
670
|
+
async function validateDispatchShape(dispatch, name = "task") {
|
|
671
|
+
if (dispatch?.schemaVersion !== CURRENT_TASK_SCHEMA_VERSION) {
|
|
795
672
|
throw new Error(`unsupported ${name} schemaVersion`);
|
|
796
673
|
}
|
|
797
|
-
requireExactFields(
|
|
798
|
-
dispatch,
|
|
799
|
-
dispatch.schemaVersion >= PREVIOUS_STATEFUL_TASK_SCHEMA_VERSION
|
|
800
|
-
? DISPATCH_FIELDS
|
|
801
|
-
: LEGACY_DISPATCH_FIELDS,
|
|
802
|
-
name,
|
|
803
|
-
);
|
|
674
|
+
requireExactFields(dispatch, DISPATCH_FIELDS, name);
|
|
804
675
|
const id = requireSafeId(dispatch.id, `${name}.id`);
|
|
805
|
-
const project = await normalizeProject(dispatch.project, 0, {
|
|
806
|
-
checkPath: false,
|
|
807
|
-
allowLegacyGithubRepo: dispatch.schemaVersion === LEGACY_SCHEMA_VERSION,
|
|
808
|
-
});
|
|
676
|
+
const project = await normalizeProject(dispatch.project, 0, { checkPath: false });
|
|
809
677
|
const normalized = {
|
|
810
|
-
schemaVersion:
|
|
678
|
+
schemaVersion: CURRENT_TASK_SCHEMA_VERSION,
|
|
811
679
|
id,
|
|
812
680
|
project,
|
|
813
681
|
title: requireString(dispatch.title, `${name}.title`).trim(),
|
|
@@ -816,23 +684,13 @@ async function validateDispatchShape(
|
|
|
816
684
|
? null
|
|
817
685
|
: normalizeDurableThreadId(dispatch.threadId, `${name}.threadId`),
|
|
818
686
|
createdAt: requireTimestamp(dispatch.createdAt, `${name}.createdAt`),
|
|
819
|
-
status: dispatch.
|
|
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,
|
|
687
|
+
status: requireEnum(dispatch.status, TASK_STATUSES, `${name}.status`),
|
|
688
|
+
summary: optionalString(dispatch.summary, `${name}.summary`, {
|
|
689
|
+
maxLength: MAX_RESULT_SUMMARY_LENGTH,
|
|
690
|
+
}),
|
|
691
|
+
turnId: optionalString(dispatch.turnId, `${name}.turnId`, { maxLength: 256 }),
|
|
692
|
+
updatedAt: requireTimestamp(dispatch.updatedAt, `${name}.updatedAt`),
|
|
693
|
+
updatedBy: requireEnum(dispatch.updatedBy, TASK_UPDATE_SOURCES, `${name}.updatedBy`),
|
|
836
694
|
};
|
|
837
695
|
if (normalized.status === "working" && normalized.summary !== null) {
|
|
838
696
|
throw new Error(`${name}.summary must be null while status is working`);
|
|
@@ -841,14 +699,13 @@ async function validateDispatchShape(
|
|
|
841
699
|
throw new Error(`${name}.summary is required for status ${normalized.status}`);
|
|
842
700
|
}
|
|
843
701
|
if (
|
|
844
|
-
normalized.updatedAt
|
|
845
|
-
&& Date.parse(normalized.updatedAt) < Date.parse(normalized.createdAt)
|
|
702
|
+
Date.parse(normalized.updatedAt) < Date.parse(normalized.createdAt)
|
|
846
703
|
) {
|
|
847
704
|
throw new Error(`${name}.updatedAt must not be earlier than createdAt`);
|
|
848
705
|
}
|
|
849
706
|
if (
|
|
850
707
|
normalized.threadId === null &&
|
|
851
|
-
parseTaskChefMarker(normalized.instruction
|
|
708
|
+
parseTaskChefMarker(normalized.instruction) !== normalized.id
|
|
852
709
|
) {
|
|
853
710
|
throw new Error(`${name} with a null threadId must contain its exact TaskChef marker`);
|
|
854
711
|
}
|
|
@@ -875,9 +732,7 @@ async function parseDispatchRecordsUnlocked(root, content) {
|
|
|
875
732
|
records.push({
|
|
876
733
|
line,
|
|
877
734
|
raw: value,
|
|
878
|
-
normalized: await validateDispatchShape(value, `task line ${index + 1}
|
|
879
|
-
allowLegacyHeading: true,
|
|
880
|
-
}),
|
|
735
|
+
normalized: await validateDispatchShape(value, `task line ${index + 1}`),
|
|
881
736
|
});
|
|
882
737
|
}
|
|
883
738
|
const ids = new Set();
|
|
@@ -972,53 +827,6 @@ export async function recordTask(workspaceRoot, input, { now } = {}) {
|
|
|
972
827
|
});
|
|
973
828
|
}
|
|
974
829
|
|
|
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
830
|
export async function linkTask(workspaceRoot, taskId, threadId, { now } = {}) {
|
|
1023
831
|
const id = requireSafeId(taskId, "taskId");
|
|
1024
832
|
const durableThreadId = normalizeCodexThreadId(threadId);
|
|
@@ -1028,10 +836,6 @@ export async function linkTask(workspaceRoot, taskId, threadId, { now } = {}) {
|
|
|
1028
836
|
const dispatches = records.map((record) => record.normalized);
|
|
1029
837
|
const index = dispatches.findIndex((dispatch) => dispatch.id === id);
|
|
1030
838
|
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
839
|
const dispatch = dispatches[index];
|
|
1036
840
|
const sameIdentity = dispatch.threadId?.toLowerCase() === durableThreadId;
|
|
1037
841
|
if (sameIdentity && dispatch.updatedBy === "mcp") {
|
|
@@ -1115,8 +919,7 @@ export async function reportTaskResult(workspaceRoot, input, { now } = {}) {
|
|
|
1115
919
|
if (index === -1) throw new Error(`task not found: ${id}`);
|
|
1116
920
|
const dispatch = dispatches[index];
|
|
1117
921
|
const isSelfLinkingJourney = (
|
|
1118
|
-
|
|
1119
|
-
&& dispatch.threadId !== null
|
|
922
|
+
dispatch.threadId !== null
|
|
1120
923
|
&& parseTaskChefMarker(dispatch.instruction) === dispatch.id
|
|
1121
924
|
);
|
|
1122
925
|
let resultTurnId = turnId;
|
|
@@ -1149,12 +952,9 @@ export async function reportTaskResult(workspaceRoot, input, { now } = {}) {
|
|
|
1149
952
|
) {
|
|
1150
953
|
throw new Error(`task result turnId must be newer than the stored turnId: ${id}`);
|
|
1151
954
|
}
|
|
1152
|
-
const persistedSchemaVersion = records[index].raw.schemaVersion >= CURRENT_TASK_SCHEMA_VERSION
|
|
1153
|
-
? CURRENT_TASK_SCHEMA_VERSION
|
|
1154
|
-
: PREVIOUS_STATEFUL_TASK_SCHEMA_VERSION;
|
|
1155
955
|
const updated = await validateDispatchShape({
|
|
1156
956
|
...dispatch,
|
|
1157
|
-
schemaVersion:
|
|
957
|
+
schemaVersion: CURRENT_TASK_SCHEMA_VERSION,
|
|
1158
958
|
status,
|
|
1159
959
|
summary,
|
|
1160
960
|
turnId: resultTurnId,
|
|
@@ -1162,7 +962,7 @@ export async function reportTaskResult(workspaceRoot, input, { now } = {}) {
|
|
|
1162
962
|
updatedBy: "mcp",
|
|
1163
963
|
});
|
|
1164
964
|
const lines = records.map((record, recordIndex) => recordIndex === index
|
|
1165
|
-
? dispatchLineWithState(updated, {
|
|
965
|
+
? dispatchLineWithState(updated, {})
|
|
1166
966
|
: record.line);
|
|
1167
967
|
await writeDispatchLinesAtomic(root, lines);
|
|
1168
968
|
return updated;
|
|
@@ -1234,12 +1034,5 @@ export async function doctorWorkspace(workspaceRoot) {
|
|
|
1234
1034
|
}
|
|
1235
1035
|
return "managed AGENTS.md instructions current";
|
|
1236
1036
|
});
|
|
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
1037
|
return { workspace: root, ok: checks.every((item) => item.status === "pass"), checks };
|
|
1245
1038
|
}
|
package/SPEC.md
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
# TaskChef specification
|
|
2
|
-
|
|
3
|
-
## Purpose
|
|
4
|
-
|
|
5
|
-
TaskChef routes work from a data-only dispatcher workspace to visible Codex
|
|
6
|
-
project tasks, stores exact child links, and caches explicit semantic results.
|
|
7
|
-
|
|
8
|
-
## Workspace
|
|
9
|
-
|
|
10
|
-
The canonical workspace contains only:
|
|
11
|
-
|
|
12
|
-
- `AGENTS.md`: managed dispatcher instructions plus user instructions.
|
|
13
|
-
- `taskchef.json`: schema-versioned configured projects.
|
|
14
|
-
- `tasks.jsonl`: append-on-create, atomic-rewrite-on-update task snapshots.
|
|
15
|
-
|
|
16
|
-
All workspace writes use one lock and atomic replacement. Task IDs and non-null
|
|
17
|
-
thread IDs are globally unique.
|
|
18
|
-
|
|
19
|
-
## Task schema
|
|
20
|
-
|
|
21
|
-
New records use schema version 4 and contain:
|
|
22
|
-
|
|
23
|
-
- immutable `id`, project snapshot, title, instruction, and `createdAt`;
|
|
24
|
-
- nullable `threadId` until executor self-linking succeeds;
|
|
25
|
-
- `status`, nullable bounded `summary`, nullable `turnId`, `updatedAt`, and
|
|
26
|
-
`updatedBy`.
|
|
27
|
-
|
|
28
|
-
New unresolved records require the exact first-line HTML marker followed by a
|
|
29
|
-
blank line. Schema 1-3 records and historical `updatedBy: hook` values remain
|
|
30
|
-
readable without eager migration, and task read/list APIs expose their persisted
|
|
31
|
-
schema version so reporting can distinguish legacy recovery from schema 4
|
|
32
|
-
executor link-pending state.
|
|
33
|
-
|
|
34
|
-
## Dispatch workflow
|
|
35
|
-
|
|
36
|
-
1. `prepare_dispatch` validates the workspace and returns one fresh lowercase
|
|
37
|
-
UUID, exact marker, timestamp, and routing targets.
|
|
38
|
-
2. The marked instruction contains the executor ownership paragraph, mandatory
|
|
39
|
-
first-action `link_task` paragraph, result callback paragraph, and task body.
|
|
40
|
-
3. `record_task` writes schema 4 with `threadId: null` before creation.
|
|
41
|
-
4. Native task creation runs once.
|
|
42
|
-
5. The dispatcher returns immediately, including for a provisional worktree
|
|
43
|
-
client ID. It does not list or read tasks, search markers, wait, poll, or
|
|
44
|
-
link the record.
|
|
45
|
-
6. Creation failure writes a terminal `failed` result with null identities and
|
|
46
|
-
a concise bounded summary.
|
|
47
|
-
|
|
48
|
-
## Executor self-linking
|
|
49
|
-
|
|
50
|
-
The executor obtains its own durable native thread ID and calls
|
|
51
|
-
`link_task(taskId, threadId)` before substantive work. The operation:
|
|
52
|
-
|
|
53
|
-
- accepts only schema 4 records;
|
|
54
|
-
- rejects unknown task IDs, malformed/provisional IDs, exact-marker mismatch,
|
|
55
|
-
thread reuse, and conflicting retries;
|
|
56
|
-
- atomically changes only `null` to one durable ID under the workspace lock;
|
|
57
|
-
- is idempotent for the same task and ID;
|
|
58
|
-
- leaves rejected, unavailable, interrupted, or cancelled links visibly
|
|
59
|
-
pending and retryable.
|
|
60
|
-
|
|
61
|
-
The executor must never use a parent/delegator ID, inherited session identity,
|
|
62
|
-
title match, or provisional client ID. This is a cooperative local assertion,
|
|
63
|
-
not transport-authenticated caller identity.
|
|
64
|
-
|
|
65
|
-
## Semantic results
|
|
66
|
-
|
|
67
|
-
`report_result` accepts `completed`, `needs_input`, or `failed` with a concise
|
|
68
|
-
summary. Linked tasks require an exact stored thread-ID match and a non-null
|
|
69
|
-
current turn ID. Self-linked schema 4 journeys require canonical, time-ordered
|
|
70
|
-
Codex UUID turn IDs; each changed semantic result must use an ID newer than the
|
|
71
|
-
stored turn, while an identical same-turn retry is idempotent. Creation failure
|
|
72
|
-
is the sole null-identity result. Follow-up turns must self-read the exact
|
|
73
|
-
thread and submit the new turn ID. Atomic writes preserve one JSONL line per
|
|
74
|
-
task.
|
|
75
|
-
|
|
76
|
-
## Legacy resolver
|
|
77
|
-
|
|
78
|
-
The CLI-only `taskchef task resolve` command accepts unresolved schema 1-3
|
|
79
|
-
records after an operator establishes one exact marker match. It rejects schema
|
|
80
|
-
4 records. There is no public `resolve_task` MCP tool.
|
|
81
|
-
|
|
82
|
-
## Dashboard and reports
|
|
83
|
-
|
|
84
|
-
The existing file watcher observes `link_task` and result rewrites in real
|
|
85
|
-
time. "Open task in Codex" uses only the self-linked child ID. Reports treat
|
|
86
|
-
`updatedBy: mcp` terminal or needs-input snapshots as semantic cache and may
|
|
87
|
-
compare them with one native metadata snapshot. Reporting never mutates task
|
|
88
|
-
history or infers completion from hooks.
|
|
89
|
-
|
|
90
|
-
## Packaging
|
|
91
|
-
|
|
92
|
-
The plugin ships its skills, MCP server, CLI, dashboard, and source modules. It
|
|
93
|
-
contains no hooks configuration, hook executable, hook runtime, or hook trust
|
|
94
|
-
requirement.
|
|
95
|
-
|
|
96
|
-
## Acceptance
|
|
97
|
-
|
|
98
|
-
- Record-before-create and exact-marker guarantees remain mandatory.
|
|
99
|
-
- Creation returns without post-create waiting, polling, task search, or reads.
|
|
100
|
-
- A provisional-path executor self-links its exact durable child ID before
|
|
101
|
-
substantive work.
|
|
102
|
-
- Parent/delegator and provisional IDs cannot be accidentally substituted.
|
|
103
|
-
- Link and result retries are idempotent; conflicts cannot corrupt JSONL.
|
|
104
|
-
- Link failures remain visible and retryable; creation failures are terminal.
|
|
105
|
-
- Current-turn freshness is retained across needs-input, follow-up, and final
|
|
106
|
-
completion.
|
|
107
|
-
- The plugin installs and runs without hook configuration or approval.
|