@agentplaneorg/core 0.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/LICENSE +22 -0
- package/README.md +5 -0
- package/dist/base-branch.d.ts +14 -0
- package/dist/base-branch.d.ts.map +1 -0
- package/dist/base-branch.js +38 -0
- package/dist/commit-policy.d.ts +12 -0
- package/dist/commit-policy.d.ts.map +1 -0
- package/dist/commit-policy.js +31 -0
- package/dist/config.d.ts +74 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +237 -0
- package/dist/git-utils.d.ts +9 -0
- package/dist/git-utils.d.ts.map +1 -0
- package/dist/git-utils.js +31 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/project-root.d.ts +11 -0
- package/dist/project-root.d.ts.map +1 -0
- package/dist/project-root.js +38 -0
- package/dist/task-readme.d.ts +8 -0
- package/dist/task-readme.d.ts.map +1 -0
- package/dist/task-readme.js +118 -0
- package/dist/task-store.d.ts +74 -0
- package/dist/task-store.d.ts.map +1 -0
- package/dist/task-store.js +211 -0
- package/dist/tasks-export.d.ts +49 -0
- package/dist/tasks-export.d.ts.map +1 -0
- package/dist/tasks-export.js +96 -0
- package/dist/tasks-lint.d.ts +19 -0
- package/dist/tasks-lint.d.ts.map +1 -0
- package/dist/tasks-lint.js +167 -0
- package/package.json +48 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { parse as parseYaml } from "yaml";
|
|
2
|
+
function isRecord(value) {
|
|
3
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
function splitFrontmatter(markdown) {
|
|
6
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(markdown);
|
|
7
|
+
if (!match)
|
|
8
|
+
return { frontmatterText: null, body: markdown };
|
|
9
|
+
return { frontmatterText: match[1] ?? null, body: markdown.slice(match[0].length) };
|
|
10
|
+
}
|
|
11
|
+
export function parseTaskReadme(markdown) {
|
|
12
|
+
const { frontmatterText, body } = splitFrontmatter(markdown);
|
|
13
|
+
if (frontmatterText == null) {
|
|
14
|
+
throw new Error("Task README is missing YAML frontmatter");
|
|
15
|
+
}
|
|
16
|
+
const parsed = parseYaml(frontmatterText);
|
|
17
|
+
if (!isRecord(parsed)) {
|
|
18
|
+
throw new TypeError("Task README frontmatter must be a YAML mapping");
|
|
19
|
+
}
|
|
20
|
+
return { frontmatter: parsed, body };
|
|
21
|
+
}
|
|
22
|
+
function renderScalar(value) {
|
|
23
|
+
if (value === null)
|
|
24
|
+
return "null";
|
|
25
|
+
if (typeof value === "string")
|
|
26
|
+
return JSON.stringify(value);
|
|
27
|
+
if (typeof value === "number")
|
|
28
|
+
return String(value);
|
|
29
|
+
if (typeof value === "boolean")
|
|
30
|
+
return value ? "true" : "false";
|
|
31
|
+
throw new TypeError(`Unsupported scalar type: ${typeof value}`);
|
|
32
|
+
}
|
|
33
|
+
function renderInlineMap(value, preferredKeyOrder) {
|
|
34
|
+
const keys = Object.keys(value);
|
|
35
|
+
const ordered = [];
|
|
36
|
+
if (preferredKeyOrder) {
|
|
37
|
+
for (const k of preferredKeyOrder)
|
|
38
|
+
if (k in value)
|
|
39
|
+
ordered.push(k);
|
|
40
|
+
}
|
|
41
|
+
const remaining = keys.filter((k) => !ordered.includes(k)).toSorted((a, b) => a.localeCompare(b));
|
|
42
|
+
ordered.push(...remaining);
|
|
43
|
+
const parts = ordered.map((k) => {
|
|
44
|
+
const v = value[k];
|
|
45
|
+
if (Array.isArray(v))
|
|
46
|
+
return `${k}: ${renderFlowSeq(v)}`;
|
|
47
|
+
if (isRecord(v))
|
|
48
|
+
return `${k}: ${renderInlineMap(v, null)}`;
|
|
49
|
+
return `${k}: ${renderScalar(v)}`;
|
|
50
|
+
});
|
|
51
|
+
return `{ ${parts.join(", ")} }`;
|
|
52
|
+
}
|
|
53
|
+
function renderFlowSeq(value) {
|
|
54
|
+
const parts = value.map((v) => {
|
|
55
|
+
if (Array.isArray(v))
|
|
56
|
+
return renderFlowSeq(v);
|
|
57
|
+
if (isRecord(v))
|
|
58
|
+
return renderInlineMap(v, null);
|
|
59
|
+
return renderScalar(v);
|
|
60
|
+
});
|
|
61
|
+
return `[${parts.join(", ")}]`;
|
|
62
|
+
}
|
|
63
|
+
function renderValue(key, value) {
|
|
64
|
+
if (Array.isArray(value)) {
|
|
65
|
+
if (value.length === 0)
|
|
66
|
+
return [`${key}: []`];
|
|
67
|
+
const allObjects = value.every((v) => isRecord(v));
|
|
68
|
+
if (!allObjects)
|
|
69
|
+
return [`${key}: ${renderFlowSeq(value)}`];
|
|
70
|
+
return [
|
|
71
|
+
`${key}:`,
|
|
72
|
+
...value.map((item) => {
|
|
73
|
+
if (!isRecord(item))
|
|
74
|
+
throw new TypeError("Expected an object item in YAML sequence");
|
|
75
|
+
const preferred = key === "comments" ? ["author", "body"] : null;
|
|
76
|
+
return ` - ${renderInlineMap(item, preferred)}`;
|
|
77
|
+
}),
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
if (isRecord(value)) {
|
|
81
|
+
const preferred = key === "commit" ? ["hash", "message"] : null;
|
|
82
|
+
return [`${key}: ${renderInlineMap(value, preferred)}`];
|
|
83
|
+
}
|
|
84
|
+
return [`${key}: ${renderScalar(value)}`];
|
|
85
|
+
}
|
|
86
|
+
export function renderTaskFrontmatter(frontmatter) {
|
|
87
|
+
const preferredKeyOrder = [
|
|
88
|
+
"id",
|
|
89
|
+
"title",
|
|
90
|
+
"status",
|
|
91
|
+
"priority",
|
|
92
|
+
"owner",
|
|
93
|
+
"depends_on",
|
|
94
|
+
"tags",
|
|
95
|
+
"verify",
|
|
96
|
+
"commit",
|
|
97
|
+
"comments",
|
|
98
|
+
"doc_version",
|
|
99
|
+
"doc_updated_at",
|
|
100
|
+
"doc_updated_by",
|
|
101
|
+
"description",
|
|
102
|
+
];
|
|
103
|
+
const keys = Object.keys(frontmatter);
|
|
104
|
+
const ordered = [];
|
|
105
|
+
for (const k of preferredKeyOrder)
|
|
106
|
+
if (k in frontmatter)
|
|
107
|
+
ordered.push(k);
|
|
108
|
+
const remaining = keys.filter((k) => !ordered.includes(k)).toSorted((a, b) => a.localeCompare(b));
|
|
109
|
+
ordered.push(...remaining);
|
|
110
|
+
const lines = [];
|
|
111
|
+
for (const k of ordered) {
|
|
112
|
+
lines.push(...renderValue(k, frontmatter[k]));
|
|
113
|
+
}
|
|
114
|
+
return `---\n${lines.join("\n")}\n---\n`;
|
|
115
|
+
}
|
|
116
|
+
export function renderTaskReadme(frontmatter, body) {
|
|
117
|
+
return `${renderTaskFrontmatter(frontmatter)}${body}`;
|
|
118
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export type TaskStatus = "TODO" | "DOING" | "DONE" | "BLOCKED";
|
|
2
|
+
export type TaskPriority = "low" | "normal" | "med" | "high";
|
|
3
|
+
export type TaskFrontmatter = {
|
|
4
|
+
id: string;
|
|
5
|
+
title: string;
|
|
6
|
+
status: TaskStatus;
|
|
7
|
+
priority: TaskPriority;
|
|
8
|
+
owner: string;
|
|
9
|
+
depends_on: string[];
|
|
10
|
+
tags: string[];
|
|
11
|
+
verify: string[];
|
|
12
|
+
comments: {
|
|
13
|
+
author: string;
|
|
14
|
+
body: string;
|
|
15
|
+
}[];
|
|
16
|
+
doc_version: 2;
|
|
17
|
+
doc_updated_at: string;
|
|
18
|
+
doc_updated_by: string;
|
|
19
|
+
description: string;
|
|
20
|
+
commit?: {
|
|
21
|
+
hash: string;
|
|
22
|
+
message: string;
|
|
23
|
+
} | null;
|
|
24
|
+
};
|
|
25
|
+
export type TaskRecord = {
|
|
26
|
+
id: string;
|
|
27
|
+
frontmatter: TaskFrontmatter;
|
|
28
|
+
body: string;
|
|
29
|
+
readmePath: string;
|
|
30
|
+
};
|
|
31
|
+
export declare function validateTaskDocMetadata(frontmatter: Record<string, unknown>): string[];
|
|
32
|
+
export declare function getTasksDir(opts: {
|
|
33
|
+
cwd: string;
|
|
34
|
+
rootOverride?: string | null;
|
|
35
|
+
}): Promise<{
|
|
36
|
+
gitRoot: string;
|
|
37
|
+
tasksDir: string;
|
|
38
|
+
idSuffixLengthDefault: number;
|
|
39
|
+
}>;
|
|
40
|
+
export declare function taskReadmePath(tasksDir: string, taskId: string): string;
|
|
41
|
+
export declare function createTask(opts: {
|
|
42
|
+
cwd: string;
|
|
43
|
+
rootOverride?: string | null;
|
|
44
|
+
title: string;
|
|
45
|
+
description: string;
|
|
46
|
+
owner: string;
|
|
47
|
+
priority: TaskPriority;
|
|
48
|
+
tags: string[];
|
|
49
|
+
dependsOn: string[];
|
|
50
|
+
verify: string[];
|
|
51
|
+
}): Promise<{
|
|
52
|
+
id: string;
|
|
53
|
+
readmePath: string;
|
|
54
|
+
}>;
|
|
55
|
+
export declare function setTaskDocSection(opts: {
|
|
56
|
+
cwd: string;
|
|
57
|
+
rootOverride?: string | null;
|
|
58
|
+
taskId: string;
|
|
59
|
+
section: string;
|
|
60
|
+
text: string;
|
|
61
|
+
updatedBy?: string | null;
|
|
62
|
+
}): Promise<{
|
|
63
|
+
readmePath: string;
|
|
64
|
+
}>;
|
|
65
|
+
export declare function readTask(opts: {
|
|
66
|
+
cwd: string;
|
|
67
|
+
rootOverride?: string | null;
|
|
68
|
+
taskId: string;
|
|
69
|
+
}): Promise<TaskRecord>;
|
|
70
|
+
export declare function listTasks(opts: {
|
|
71
|
+
cwd: string;
|
|
72
|
+
rootOverride?: string | null;
|
|
73
|
+
}): Promise<TaskRecord[]>;
|
|
74
|
+
//# sourceMappingURL=task-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"task-store.d.ts","sourceRoot":"","sources":["../src/task-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { loadConfig } from "./config.js";
|
|
4
|
+
import { resolveProject } from "./project-root.js";
|
|
5
|
+
import { parseTaskReadme, renderTaskReadme } from "./task-readme.js";
|
|
6
|
+
function nowIso() {
|
|
7
|
+
return new Date().toISOString();
|
|
8
|
+
}
|
|
9
|
+
export function validateTaskDocMetadata(frontmatter) {
|
|
10
|
+
const errors = [];
|
|
11
|
+
if (frontmatter.doc_version !== 2)
|
|
12
|
+
errors.push("doc_version must be 2");
|
|
13
|
+
const updatedAt = frontmatter.doc_updated_at;
|
|
14
|
+
if (typeof updatedAt !== "string" || Number.isNaN(Date.parse(updatedAt))) {
|
|
15
|
+
errors.push("doc_updated_at must be an ISO timestamp");
|
|
16
|
+
}
|
|
17
|
+
const updatedBy = frontmatter.doc_updated_by;
|
|
18
|
+
if (typeof updatedBy !== "string" || updatedBy.trim().length === 0) {
|
|
19
|
+
errors.push("doc_updated_by must be a non-empty string");
|
|
20
|
+
}
|
|
21
|
+
return errors;
|
|
22
|
+
}
|
|
23
|
+
const ID_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
24
|
+
function randomSuffix(length) {
|
|
25
|
+
let out = "";
|
|
26
|
+
for (let i = 0; i < length; i++) {
|
|
27
|
+
out += ID_ALPHABET[Math.floor(Math.random() * ID_ALPHABET.length)];
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
function timestampIdPrefix(date) {
|
|
32
|
+
const yyyy = String(date.getUTCFullYear()).padStart(4, "0");
|
|
33
|
+
const mm = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
34
|
+
const dd = String(date.getUTCDate()).padStart(2, "0");
|
|
35
|
+
const hh = String(date.getUTCHours()).padStart(2, "0");
|
|
36
|
+
const min = String(date.getUTCMinutes()).padStart(2, "0");
|
|
37
|
+
return `${yyyy}${mm}${dd}${hh}${min}`;
|
|
38
|
+
}
|
|
39
|
+
async function fileExists(filePath) {
|
|
40
|
+
try {
|
|
41
|
+
await readFile(filePath, "utf8");
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export async function getTasksDir(opts) {
|
|
49
|
+
const resolved = await resolveProject({ cwd: opts.cwd, rootOverride: opts.rootOverride ?? null });
|
|
50
|
+
const loaded = await loadConfig(resolved.agentplaneDir);
|
|
51
|
+
const tasksDir = path.join(resolved.gitRoot, loaded.config.paths.workflow_dir);
|
|
52
|
+
return {
|
|
53
|
+
gitRoot: resolved.gitRoot,
|
|
54
|
+
tasksDir,
|
|
55
|
+
idSuffixLengthDefault: loaded.config.tasks.id_suffix_length_default,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export function taskReadmePath(tasksDir, taskId) {
|
|
59
|
+
return path.join(tasksDir, taskId, "README.md");
|
|
60
|
+
}
|
|
61
|
+
function defaultTaskBody() {
|
|
62
|
+
return [
|
|
63
|
+
"## Summary",
|
|
64
|
+
"",
|
|
65
|
+
"",
|
|
66
|
+
"## Scope",
|
|
67
|
+
"",
|
|
68
|
+
"",
|
|
69
|
+
"## Risks",
|
|
70
|
+
"",
|
|
71
|
+
"",
|
|
72
|
+
"## Verify Steps",
|
|
73
|
+
"",
|
|
74
|
+
"",
|
|
75
|
+
"## Rollback Plan",
|
|
76
|
+
"",
|
|
77
|
+
].join("\n");
|
|
78
|
+
}
|
|
79
|
+
function escapeRegExp(text) {
|
|
80
|
+
return text.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw `\\$&`);
|
|
81
|
+
}
|
|
82
|
+
function setMarkdownSection(body, section, text) {
|
|
83
|
+
const lines = body.replaceAll("\r\n", "\n").split("\n");
|
|
84
|
+
const headingRe = new RegExp(String.raw `^##\s+${escapeRegExp(section)}\s*$`);
|
|
85
|
+
let start = -1;
|
|
86
|
+
let nextHeading = lines.length;
|
|
87
|
+
for (const [i, line] of lines.entries()) {
|
|
88
|
+
if (!line.startsWith("## "))
|
|
89
|
+
continue;
|
|
90
|
+
if (start === -1) {
|
|
91
|
+
if (headingRe.test(line))
|
|
92
|
+
start = i;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
nextHeading = i;
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
const newTextLines = text.replaceAll("\r\n", "\n").split("\n");
|
|
99
|
+
const replacement = ["", ...newTextLines, ""];
|
|
100
|
+
if (start === -1) {
|
|
101
|
+
const out = [...lines];
|
|
102
|
+
if (out.length > 0 && out.at(-1)?.trim() !== "")
|
|
103
|
+
out.push("");
|
|
104
|
+
out.push(`## ${section}`, ...replacement);
|
|
105
|
+
return `${out.join("\n")}\n`;
|
|
106
|
+
}
|
|
107
|
+
const out = [...lines.slice(0, start + 1), ...replacement, ...lines.slice(nextHeading)];
|
|
108
|
+
return `${out.join("\n")}\n`;
|
|
109
|
+
}
|
|
110
|
+
export async function createTask(opts) {
|
|
111
|
+
const { tasksDir, idSuffixLengthDefault } = await getTasksDir({
|
|
112
|
+
cwd: opts.cwd,
|
|
113
|
+
rootOverride: opts.rootOverride ?? null,
|
|
114
|
+
});
|
|
115
|
+
await mkdir(tasksDir, { recursive: true });
|
|
116
|
+
const suffixLength = idSuffixLengthDefault;
|
|
117
|
+
const base = timestampIdPrefix(new Date());
|
|
118
|
+
for (let attempt = 0; attempt < 20; attempt++) {
|
|
119
|
+
const id = `${base}-${randomSuffix(suffixLength)}`;
|
|
120
|
+
const readmePath = taskReadmePath(tasksDir, id);
|
|
121
|
+
if (await fileExists(readmePath))
|
|
122
|
+
continue;
|
|
123
|
+
const frontmatter = {
|
|
124
|
+
id,
|
|
125
|
+
title: opts.title,
|
|
126
|
+
status: "TODO",
|
|
127
|
+
priority: opts.priority,
|
|
128
|
+
owner: opts.owner,
|
|
129
|
+
depends_on: opts.dependsOn,
|
|
130
|
+
tags: opts.tags,
|
|
131
|
+
verify: opts.verify,
|
|
132
|
+
comments: [],
|
|
133
|
+
doc_version: 2,
|
|
134
|
+
doc_updated_at: nowIso(),
|
|
135
|
+
doc_updated_by: "agentplane",
|
|
136
|
+
description: opts.description,
|
|
137
|
+
};
|
|
138
|
+
const body = defaultTaskBody();
|
|
139
|
+
const text = renderTaskReadme(frontmatter, body);
|
|
140
|
+
await mkdir(path.dirname(readmePath), { recursive: true });
|
|
141
|
+
await writeFile(readmePath, text, "utf8");
|
|
142
|
+
return { id, readmePath };
|
|
143
|
+
}
|
|
144
|
+
throw new Error("Failed to generate a unique task id");
|
|
145
|
+
}
|
|
146
|
+
export async function setTaskDocSection(opts) {
|
|
147
|
+
const resolved = await resolveProject({ cwd: opts.cwd, rootOverride: opts.rootOverride ?? null });
|
|
148
|
+
const loaded = await loadConfig(resolved.agentplaneDir);
|
|
149
|
+
const allowed = loaded.config.tasks.doc.sections;
|
|
150
|
+
if (!allowed.includes(opts.section)) {
|
|
151
|
+
throw new Error(`Unknown doc section: ${opts.section}`);
|
|
152
|
+
}
|
|
153
|
+
const tasksDir = path.join(resolved.gitRoot, loaded.config.paths.workflow_dir);
|
|
154
|
+
const readmePath = taskReadmePath(tasksDir, opts.taskId);
|
|
155
|
+
const original = await readFile(readmePath, "utf8");
|
|
156
|
+
const parsed = parseTaskReadme(original);
|
|
157
|
+
const updatedBy = (opts.updatedBy ?? "agentplane").trim();
|
|
158
|
+
if (updatedBy.length === 0)
|
|
159
|
+
throw new Error("doc_updated_by must be a non-empty string");
|
|
160
|
+
const nextFrontmatter = {
|
|
161
|
+
...parsed.frontmatter,
|
|
162
|
+
doc_version: 2,
|
|
163
|
+
doc_updated_at: nowIso(),
|
|
164
|
+
doc_updated_by: updatedBy,
|
|
165
|
+
};
|
|
166
|
+
const nextBody = setMarkdownSection(parsed.body, opts.section, opts.text);
|
|
167
|
+
const nextText = renderTaskReadme(nextFrontmatter, nextBody);
|
|
168
|
+
await writeFile(readmePath, nextText, "utf8");
|
|
169
|
+
return { readmePath };
|
|
170
|
+
}
|
|
171
|
+
export async function readTask(opts) {
|
|
172
|
+
const { tasksDir } = await getTasksDir({
|
|
173
|
+
cwd: opts.cwd,
|
|
174
|
+
rootOverride: opts.rootOverride ?? null,
|
|
175
|
+
});
|
|
176
|
+
const readmePath = taskReadmePath(tasksDir, opts.taskId);
|
|
177
|
+
const text = await readFile(readmePath, "utf8");
|
|
178
|
+
const parsed = parseTaskReadme(text);
|
|
179
|
+
return {
|
|
180
|
+
id: opts.taskId,
|
|
181
|
+
frontmatter: parsed.frontmatter,
|
|
182
|
+
body: parsed.body,
|
|
183
|
+
readmePath,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
export async function listTasks(opts) {
|
|
187
|
+
const { tasksDir } = await getTasksDir({
|
|
188
|
+
cwd: opts.cwd,
|
|
189
|
+
rootOverride: opts.rootOverride ?? null,
|
|
190
|
+
});
|
|
191
|
+
const entries = await readdir(tasksDir, { withFileTypes: true }).catch(() => []);
|
|
192
|
+
const ids = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
193
|
+
const tasks = [];
|
|
194
|
+
for (const id of ids) {
|
|
195
|
+
const readmePath = taskReadmePath(tasksDir, id);
|
|
196
|
+
try {
|
|
197
|
+
const text = await readFile(readmePath, "utf8");
|
|
198
|
+
const parsed = parseTaskReadme(text);
|
|
199
|
+
tasks.push({
|
|
200
|
+
id,
|
|
201
|
+
frontmatter: parsed.frontmatter,
|
|
202
|
+
body: parsed.body,
|
|
203
|
+
readmePath,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
// Skip unreadable/broken tasks for now; lint will handle this later.
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return tasks.toSorted((a, b) => a.id.localeCompare(b.id));
|
|
211
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export declare function canonicalizeJson(value: unknown): unknown;
|
|
2
|
+
export type TasksExportMeta = {
|
|
3
|
+
schema_version: 1;
|
|
4
|
+
managed_by: string;
|
|
5
|
+
checksum_algo: "sha256";
|
|
6
|
+
checksum: string;
|
|
7
|
+
};
|
|
8
|
+
export type TasksExportTask = {
|
|
9
|
+
id: string;
|
|
10
|
+
title: string;
|
|
11
|
+
status: string;
|
|
12
|
+
priority: string;
|
|
13
|
+
owner: string;
|
|
14
|
+
depends_on: string[];
|
|
15
|
+
tags: string[];
|
|
16
|
+
verify: string[];
|
|
17
|
+
commit: {
|
|
18
|
+
hash: string;
|
|
19
|
+
message: string;
|
|
20
|
+
} | null;
|
|
21
|
+
comments: {
|
|
22
|
+
author: string;
|
|
23
|
+
body: string;
|
|
24
|
+
}[];
|
|
25
|
+
doc_version: 2;
|
|
26
|
+
doc_updated_at: string;
|
|
27
|
+
doc_updated_by: string;
|
|
28
|
+
description: string;
|
|
29
|
+
dirty: boolean;
|
|
30
|
+
id_source: string;
|
|
31
|
+
};
|
|
32
|
+
export type TasksExportSnapshot = {
|
|
33
|
+
tasks: TasksExportTask[];
|
|
34
|
+
meta: TasksExportMeta;
|
|
35
|
+
};
|
|
36
|
+
export declare function canonicalTasksPayload(tasks: TasksExportTask[]): string;
|
|
37
|
+
export declare function computeTasksChecksum(tasks: TasksExportTask[]): string;
|
|
38
|
+
export declare function buildTasksExportSnapshot(opts: {
|
|
39
|
+
cwd: string;
|
|
40
|
+
rootOverride?: string | null;
|
|
41
|
+
}): Promise<TasksExportSnapshot>;
|
|
42
|
+
export declare function writeTasksExport(opts: {
|
|
43
|
+
cwd: string;
|
|
44
|
+
rootOverride?: string | null;
|
|
45
|
+
}): Promise<{
|
|
46
|
+
path: string;
|
|
47
|
+
snapshot: TasksExportSnapshot;
|
|
48
|
+
}>;
|
|
49
|
+
//# sourceMappingURL=tasks-export.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tasks-export.d.ts","sourceRoot":"","sources":["../src/tasks-export.ts"],"names":[],"mappings":"AAYA,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAWxD;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,cAAc,EAAE,CAAC,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,QAAQ,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACjD,QAAQ,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC7C,WAAW,EAAE,CAAC,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,OAAO,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,KAAK,EAAE,eAAe,EAAE,CAAC;IACzB,IAAI,EAAE,eAAe,CAAC;CACvB,CAAC;AAEF,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,eAAe,EAAE,GAAG,MAAM,CAEtE;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,eAAe,EAAE,GAAG,MAAM,CAGrE;AAED,wBAAsB,wBAAwB,CAAC,IAAI,EAAE;IACnD,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAqE/B;AAED,wBAAsB,gBAAgB,CAAC,IAAI,EAAE;IAC3C,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B,GAAG,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,mBAAmB,CAAA;CAAE,CAAC,CAW3D"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { loadConfig } from "./config.js";
|
|
5
|
+
import { resolveProject } from "./project-root.js";
|
|
6
|
+
import { listTasks } from "./task-store.js";
|
|
7
|
+
function isRecord(value) {
|
|
8
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
export function canonicalizeJson(value) {
|
|
11
|
+
if (Array.isArray(value))
|
|
12
|
+
return value.map((v) => canonicalizeJson(v));
|
|
13
|
+
if (isRecord(value)) {
|
|
14
|
+
const out = {};
|
|
15
|
+
const keys = Object.keys(value).toSorted((a, b) => a.localeCompare(b));
|
|
16
|
+
for (const key of keys)
|
|
17
|
+
out[key] = canonicalizeJson(value[key]);
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
export function canonicalTasksPayload(tasks) {
|
|
23
|
+
return JSON.stringify(canonicalizeJson({ tasks }));
|
|
24
|
+
}
|
|
25
|
+
export function computeTasksChecksum(tasks) {
|
|
26
|
+
const payload = canonicalTasksPayload(tasks);
|
|
27
|
+
return createHash("sha256").update(payload, "utf8").digest("hex");
|
|
28
|
+
}
|
|
29
|
+
export async function buildTasksExportSnapshot(opts) {
|
|
30
|
+
const resolved = await resolveProject({ cwd: opts.cwd, rootOverride: opts.rootOverride ?? null });
|
|
31
|
+
await loadConfig(resolved.agentplaneDir);
|
|
32
|
+
const tasks = await listTasks({ cwd: opts.cwd, rootOverride: opts.rootOverride ?? null });
|
|
33
|
+
const exportTasks = tasks.map((t) => {
|
|
34
|
+
const fm = t.frontmatter;
|
|
35
|
+
const dependsOn = Array.isArray(fm.depends_on)
|
|
36
|
+
? fm.depends_on.filter((v) => typeof v === "string")
|
|
37
|
+
: [];
|
|
38
|
+
const tags = Array.isArray(fm.tags)
|
|
39
|
+
? fm.tags.filter((v) => typeof v === "string")
|
|
40
|
+
: [];
|
|
41
|
+
const verify = Array.isArray(fm.verify)
|
|
42
|
+
? fm.verify.filter((v) => typeof v === "string")
|
|
43
|
+
: [];
|
|
44
|
+
const commit = isRecord(fm.commit) &&
|
|
45
|
+
typeof fm.commit.hash === "string" &&
|
|
46
|
+
typeof fm.commit.message === "string" &&
|
|
47
|
+
fm.commit.hash.length > 0 &&
|
|
48
|
+
fm.commit.message.length > 0
|
|
49
|
+
? { hash: fm.commit.hash, message: fm.commit.message }
|
|
50
|
+
: null;
|
|
51
|
+
const comments = Array.isArray(fm.comments)
|
|
52
|
+
? fm.comments
|
|
53
|
+
.filter((c) => isRecord(c))
|
|
54
|
+
.filter((c) => typeof c.author === "string" && typeof c.body === "string")
|
|
55
|
+
.map((c) => ({ author: c.author, body: c.body }))
|
|
56
|
+
: [];
|
|
57
|
+
return {
|
|
58
|
+
id: typeof fm.id === "string" ? fm.id : t.id,
|
|
59
|
+
title: typeof fm.title === "string" ? fm.title : "",
|
|
60
|
+
status: typeof fm.status === "string" ? fm.status : "",
|
|
61
|
+
priority: typeof fm.priority === "string" ? fm.priority : "",
|
|
62
|
+
owner: typeof fm.owner === "string" ? fm.owner : "",
|
|
63
|
+
depends_on: dependsOn,
|
|
64
|
+
tags,
|
|
65
|
+
verify,
|
|
66
|
+
commit,
|
|
67
|
+
comments,
|
|
68
|
+
doc_version: 2,
|
|
69
|
+
doc_updated_at: typeof fm.doc_updated_at === "string" ? fm.doc_updated_at : "",
|
|
70
|
+
doc_updated_by: typeof fm.doc_updated_by === "string" ? fm.doc_updated_by : "",
|
|
71
|
+
description: typeof fm.description === "string" ? fm.description : "",
|
|
72
|
+
dirty: false,
|
|
73
|
+
id_source: "generated",
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
const sorted = exportTasks.toSorted((a, b) => a.id.localeCompare(b.id));
|
|
77
|
+
const checksum = computeTasksChecksum(sorted);
|
|
78
|
+
return {
|
|
79
|
+
tasks: sorted,
|
|
80
|
+
meta: {
|
|
81
|
+
schema_version: 1,
|
|
82
|
+
managed_by: "agentplane",
|
|
83
|
+
checksum_algo: "sha256",
|
|
84
|
+
checksum,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export async function writeTasksExport(opts) {
|
|
89
|
+
const resolved = await resolveProject({ cwd: opts.cwd, rootOverride: opts.rootOverride ?? null });
|
|
90
|
+
const loaded = await loadConfig(resolved.agentplaneDir);
|
|
91
|
+
const outPath = path.join(resolved.gitRoot, loaded.config.paths.tasks_path);
|
|
92
|
+
const snapshot = await buildTasksExportSnapshot(opts);
|
|
93
|
+
await mkdir(path.dirname(outPath), { recursive: true });
|
|
94
|
+
await writeFile(outPath, `${JSON.stringify(snapshot, null, 2)}\n`, "utf8");
|
|
95
|
+
return { path: outPath, snapshot };
|
|
96
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { AgentplaneConfig } from "./config.js";
|
|
2
|
+
import { type TasksExportSnapshot } from "./tasks-export.js";
|
|
3
|
+
export type TasksLintResult = {
|
|
4
|
+
errors: string[];
|
|
5
|
+
};
|
|
6
|
+
export declare function lintTasksSnapshot(snapshot: TasksExportSnapshot, config: AgentplaneConfig): TasksLintResult;
|
|
7
|
+
export declare function readTasksExport(opts: {
|
|
8
|
+
cwd: string;
|
|
9
|
+
rootOverride?: string | null;
|
|
10
|
+
}): Promise<{
|
|
11
|
+
snapshot: TasksExportSnapshot;
|
|
12
|
+
path: string;
|
|
13
|
+
config: AgentplaneConfig;
|
|
14
|
+
}>;
|
|
15
|
+
export declare function lintTasksFile(opts: {
|
|
16
|
+
cwd: string;
|
|
17
|
+
rootOverride?: string | null;
|
|
18
|
+
}): Promise<TasksLintResult>;
|
|
19
|
+
//# sourceMappingURL=tasks-lint.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tasks-lint.d.ts","sourceRoot":"","sources":["../src/tasks-lint.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAGpD,OAAO,EAEL,KAAK,mBAAmB,EAEzB,MAAM,mBAAmB,CAAC;AAE3B,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB,CAAC;AA2DF,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,mBAAmB,EAC7B,MAAM,EAAE,gBAAgB,GACvB,eAAe,CA4GjB;AAED,wBAAsB,eAAe,CAAC,IAAI,EAAE;IAC1C,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B,GAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,mBAAmB,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,gBAAgB,CAAA;CAAE,CAAC,CAOrF;AAED,wBAAsB,aAAa,CAAC,IAAI,EAAE;IACxC,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B,GAAG,OAAO,CAAC,eAAe,CAAC,CAG3B"}
|