@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.
@@ -0,0 +1,167 @@
1
+ import { readFile } 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 { computeTasksChecksum, } from "./tasks-export.js";
6
+ function isRecord(value) {
7
+ return !!value && typeof value === "object" && !Array.isArray(value);
8
+ }
9
+ function isStringArray(value) {
10
+ return Array.isArray(value) && value.every((v) => typeof v === "string" && v.trim().length > 0);
11
+ }
12
+ function isCommentsArray(value) {
13
+ return (Array.isArray(value) &&
14
+ value.every((v) => isRecord(v) &&
15
+ typeof v.author === "string" &&
16
+ v.author.trim().length > 0 &&
17
+ typeof v.body === "string" &&
18
+ v.body.trim().length > 0));
19
+ }
20
+ function hasCycle(dependsOn) {
21
+ const visiting = new Set();
22
+ const visited = new Set();
23
+ const stack = [];
24
+ function dfs(id) {
25
+ if (visited.has(id))
26
+ return null;
27
+ if (visiting.has(id)) {
28
+ const idx = stack.indexOf(id);
29
+ return idx === -1 ? [id] : [...stack.slice(idx), id];
30
+ }
31
+ visiting.add(id);
32
+ stack.push(id);
33
+ const deps = dependsOn.get(id) ?? [];
34
+ for (const dep of deps) {
35
+ const cycle = dfs(dep);
36
+ if (cycle)
37
+ return cycle;
38
+ }
39
+ stack.pop();
40
+ visiting.delete(id);
41
+ visited.add(id);
42
+ return null;
43
+ }
44
+ for (const id of dependsOn.keys()) {
45
+ const cycle = dfs(id);
46
+ if (cycle)
47
+ return cycle;
48
+ }
49
+ return null;
50
+ }
51
+ export function lintTasksSnapshot(snapshot, config) {
52
+ const errors = [];
53
+ if (!isRecord(snapshot) || !Array.isArray(snapshot.tasks) || !isRecord(snapshot.meta)) {
54
+ return { errors: ["tasks.json must have { tasks: [], meta: {} }"] };
55
+ }
56
+ if (snapshot.meta.schema_version !== 1)
57
+ errors.push("tasks.json meta.schema_version must be 1");
58
+ if (typeof snapshot.meta.managed_by !== "string" || snapshot.meta.managed_by.length === 0) {
59
+ errors.push("tasks.json meta.managed_by must be non-empty");
60
+ }
61
+ if (snapshot.meta.checksum_algo !== "sha256") {
62
+ errors.push("tasks.json meta.checksum_algo must be 'sha256'");
63
+ }
64
+ if (typeof snapshot.meta.checksum !== "string" || snapshot.meta.checksum.length === 0) {
65
+ errors.push("tasks.json meta.checksum is missing/empty");
66
+ }
67
+ else {
68
+ const expected = computeTasksChecksum(snapshot.tasks);
69
+ if (snapshot.meta.checksum !== expected) {
70
+ errors.push("tasks.json meta.checksum does not match tasks payload (manual edit?)");
71
+ }
72
+ }
73
+ const byId = new Map();
74
+ for (const t of snapshot.tasks) {
75
+ if (!isRecord(t)) {
76
+ errors.push("tasks.json tasks[] items must be objects");
77
+ continue;
78
+ }
79
+ const id = t.id;
80
+ if (typeof id !== "string" || id.length === 0) {
81
+ errors.push("tasks.json task.id must be non-empty");
82
+ continue;
83
+ }
84
+ if (byId.has(id)) {
85
+ errors.push(`duplicate task id: ${id}`);
86
+ continue;
87
+ }
88
+ byId.set(id, t);
89
+ if (typeof t.title !== "string" || t.title.length === 0)
90
+ errors.push(`${id}: title must be non-empty`);
91
+ if (!["TODO", "DOING", "DONE", "BLOCKED"].includes(String(t.status))) {
92
+ errors.push(`${id}: status must be TODO|DOING|DONE|BLOCKED`);
93
+ }
94
+ if (!["low", "normal", "med", "high"].includes(String(t.priority))) {
95
+ errors.push(`${id}: priority must be low|normal|med|high`);
96
+ }
97
+ if (typeof t.owner !== "string" || t.owner.trim().length === 0)
98
+ errors.push(`${id}: owner must be non-empty`);
99
+ if (!isStringArray(t.depends_on))
100
+ errors.push(`${id}: depends_on must be a string[]`);
101
+ if (!isStringArray(t.tags))
102
+ errors.push(`${id}: tags must be a string[]`);
103
+ if (!Array.isArray(t.verify) ||
104
+ t.verify.some((v) => typeof v !== "string" || v.trim().length === 0)) {
105
+ errors.push(`${id}: verify must be a string[]`);
106
+ }
107
+ if (!isCommentsArray(t.comments))
108
+ errors.push(`${id}: comments must be {author,body}[]`);
109
+ if (t.doc_version !== 2)
110
+ errors.push(`${id}: doc_version must be 2`);
111
+ if (typeof t.doc_updated_at !== "string" || Number.isNaN(Date.parse(t.doc_updated_at))) {
112
+ errors.push(`${id}: doc_updated_at must be ISO date-time`);
113
+ }
114
+ if (typeof t.doc_updated_by !== "string" || t.doc_updated_by.trim().length === 0) {
115
+ errors.push(`${id}: doc_updated_by must be non-empty`);
116
+ }
117
+ if (typeof t.description !== "string")
118
+ errors.push(`${id}: description must be string`);
119
+ if (typeof t.dirty !== "boolean")
120
+ errors.push(`${id}: dirty must be boolean`);
121
+ if (typeof t.id_source !== "string" || t.id_source.trim().length === 0) {
122
+ errors.push(`${id}: id_source must be non-empty`);
123
+ }
124
+ if (String(t.status) === "DONE" &&
125
+ (!isRecord(t.commit) ||
126
+ typeof t.commit.hash !== "string" ||
127
+ typeof t.commit.message !== "string")) {
128
+ errors.push(`${id}: DONE tasks must have commit {hash,message}`);
129
+ }
130
+ const requiredTags = new Set(config.tasks.verify.required_tags);
131
+ const tagList = Array.isArray(t.tags) ? t.tags : [];
132
+ const needsVerify = tagList.some((tag) => requiredTags.has(tag));
133
+ if (needsVerify) {
134
+ const verifyList = Array.isArray(t.verify) ? t.verify : [];
135
+ if (verifyList.length === 0)
136
+ errors.push(`${id}: verify is required for tags: ${[...requiredTags].join(", ")}`);
137
+ }
138
+ }
139
+ // Depends-on must reference known tasks (only after tasks parsed).
140
+ for (const [id, t] of byId.entries()) {
141
+ const deps = Array.isArray(t.depends_on) ? t.depends_on : [];
142
+ for (const dep of deps) {
143
+ if (!byId.has(dep))
144
+ errors.push(`${id}: depends_on references missing task: ${dep}`);
145
+ }
146
+ }
147
+ const depMap = new Map();
148
+ for (const [id, t] of byId.entries()) {
149
+ depMap.set(id, Array.isArray(t.depends_on) ? t.depends_on : []);
150
+ }
151
+ const cycle = hasCycle(depMap);
152
+ if (cycle)
153
+ errors.push(`depends_on cycle detected: ${cycle.join(" -> ")}`);
154
+ return { errors };
155
+ }
156
+ export async function readTasksExport(opts) {
157
+ const resolved = await resolveProject({ cwd: opts.cwd, rootOverride: opts.rootOverride ?? null });
158
+ const loaded = await loadConfig(resolved.agentplaneDir);
159
+ const filePath = path.join(resolved.gitRoot, loaded.config.paths.tasks_path);
160
+ const raw = await readFile(filePath, "utf8");
161
+ const parsed = JSON.parse(raw);
162
+ return { snapshot: parsed, path: filePath, config: loaded.config };
163
+ }
164
+ export async function lintTasksFile(opts) {
165
+ const { snapshot, config } = await readTasksExport(opts);
166
+ return lintTasksSnapshot(snapshot, config);
167
+ }
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@agentplaneorg/core",
3
+ "version": "0.1.0",
4
+ "description": "Core utilities and models for the Agent Plane CLI.",
5
+ "keywords": [
6
+ "agentplane",
7
+ "agent-plane",
8
+ "core",
9
+ "tasks",
10
+ "workflow",
11
+ "cli"
12
+ ],
13
+ "license": "MIT",
14
+ "author": "Basilisk Labs",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/basilisk-labs/agentplane.git"
18
+ },
19
+ "homepage": "https://github.com/basilisk-labs/agentplane",
20
+ "bugs": {
21
+ "url": "https://github.com/basilisk-labs/agentplane/issues"
22
+ },
23
+ "type": "module",
24
+ "engines": {
25
+ "node": ">=20"
26
+ },
27
+ "exports": {
28
+ ".": "./dist/index.js"
29
+ },
30
+ "types": "./dist/index.d.ts",
31
+ "files": [
32
+ "dist",
33
+ "README.md",
34
+ "LICENSE"
35
+ ],
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "scripts": {
40
+ "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true}); require('node:fs').rmSync('tsconfig.tsbuildinfo',{force:true});\"",
41
+ "build": "tsc -b",
42
+ "typecheck": "tsc -b",
43
+ "prepack": "bun run clean && bun run build"
44
+ },
45
+ "dependencies": {
46
+ "yaml": "^2.8.2"
47
+ }
48
+ }