@agentplaneorg/core 0.1.1 → 0.1.3

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/README.md CHANGED
@@ -1,5 +1,68 @@
1
1
  # @agentplaneorg/core
2
2
 
3
- Internal core library used by the `agentplane` CLI.
3
+ [![npm](https://img.shields.io/npm/v/@agentplaneorg/core.svg)](https://www.npmjs.com/package/@agentplaneorg/core)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/basilisk-labs/agentplane/blob/main/LICENSE)
5
+ [![Node.js 20+](https://img.shields.io/badge/Node.js-20%2B-3c873a.svg)](https://github.com/basilisk-labs/agentplane/blob/main/docs/user/prerequisites.mdx)
4
6
 
5
- This package is intended to be published alongside `agentplane` to satisfy runtime dependencies.
7
+ Core utilities and models used by the `agentplane` CLI. This package exposes the reusable building blocks
8
+ for project discovery, config handling, task readme parsing, and task export/linting.
9
+
10
+ If you are an end-user, install the CLI instead: https://www.npmjs.com/package/agentplane
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ npm install @agentplaneorg/core
16
+ ```
17
+
18
+ ## Requirements
19
+
20
+ - Node.js >= 20
21
+ - ESM-only (`type: module`)
22
+
23
+ ## Usage
24
+
25
+ ```ts
26
+ import {
27
+ resolveProject,
28
+ loadConfig,
29
+ listTasks,
30
+ readTask,
31
+ buildTasksExportSnapshot,
32
+ } from "@agentplaneorg/core";
33
+
34
+ const project = await resolveProject(process.cwd());
35
+ const config = await loadConfig(project.root);
36
+ const tasks = await listTasks(project.root);
37
+ const task = await readTask(project.root, tasks[0]?.id ?? "");
38
+ const snapshot = await buildTasksExportSnapshot(project.root);
39
+
40
+ console.log(config.data.workflow_mode, task?.id, snapshot.meta.version);
41
+ ```
42
+
43
+ ## Exported Modules
44
+
45
+ | Area | Highlights |
46
+ | ----------------- | -------------------------------------------------------------- |
47
+ | Project discovery | `resolveProject`, `findGitRoot` |
48
+ | Config | `loadConfig`, `saveConfig`, `setByDottedKey`, `validateConfig` |
49
+ | Task README | `parseTaskReadme`, `renderTaskReadme` |
50
+ | Task store | `createTask`, `listTasks`, `readTask`, `setTaskDocSection` |
51
+ | Exports | `buildTasksExportSnapshot`, `writeTasksExport`, checksums |
52
+ | Linting | `lintTasksFile`, `lintTasksSnapshot` |
53
+ | Git | `getStagedFiles`, `getUnstagedFiles`, base branch helpers |
54
+ | Commit policy | `validateCommitSubject`, `extractTaskSuffix` |
55
+
56
+ ## Stability
57
+
58
+ This package is versioned alongside the CLI and is primarily used by `agentplane`. The API is stable for
59
+ current use cases, but expect changes as the CLI evolves.
60
+
61
+ ## Docs
62
+
63
+ - Repository: https://github.com/basilisk-labs/agentplane
64
+ - Developer docs: https://github.com/basilisk-labs/agentplane/tree/main/docs
65
+
66
+ ## License
67
+
68
+ MIT
@@ -1 +1 @@
1
- {"version":3,"file":"task-readme.d.ts","sourceRoot":"","sources":["../src/task-readme.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAQF,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB,CAYlE;AAiED,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CA8BlF;AAED,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE3F"}
1
+ {"version":3,"file":"task-readme.d.ts","sourceRoot":"","sources":["../src/task-readme.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAqBF,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB,CAYlE;AAiED,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CA8BlF;AAED,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE3F"}
@@ -2,11 +2,25 @@ import { parse as parseYaml } from "yaml";
2
2
  function isRecord(value) {
3
3
  return !!value && typeof value === "object" && !Array.isArray(value);
4
4
  }
5
+ function stripLeadingFrontmatterBlocks(body) {
6
+ let next = body.replaceAll("\r\n", "\n");
7
+ while (true) {
8
+ const trimmed = next.replace(/^\n+/, "");
9
+ if (!trimmed.startsWith("---\n"))
10
+ break;
11
+ const end = trimmed.indexOf("\n---\n", 4);
12
+ if (end === -1)
13
+ break;
14
+ next = trimmed.slice(end + 5);
15
+ }
16
+ return next;
17
+ }
5
18
  function splitFrontmatter(markdown) {
6
19
  const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(markdown);
7
20
  if (!match)
8
21
  return { frontmatterText: null, body: markdown };
9
- return { frontmatterText: match[1] ?? null, body: markdown.slice(match[0].length) };
22
+ const body = stripLeadingFrontmatterBlocks(markdown.slice(match[0].length));
23
+ return { frontmatterText: match[1] ?? null, body };
10
24
  }
11
25
  export function parseTaskReadme(markdown) {
12
26
  const { frontmatterText, body } = splitFrontmatter(markdown);
@@ -1 +1 @@
1
- {"version":3,"file":"task-store.d.ts","sourceRoot":"","sources":["../src/task-store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"task-store.d.ts","sourceRoot":"","sources":["../src/task-store.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC;AAC/D,MAAM,MAAM,YAAY,GAAG,KAAK,GAAG,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;AAE7D,MAAM,MAAM,eAAe,GAAG;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,UAAU,CAAC;IACnB,QAAQ,EAAE,YAAY,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,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,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CACnD,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,eAAe,CAAC;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAUF,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,EAAE,CAgBtF;AA8BD,wBAAsB,WAAW,CAAC,IAAI,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAAG,OAAO,CAAC;IAC9F,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,qBAAqB,EAAE,MAAM,CAAC;CAC/B,CAAC,CASD;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAEvE;AA0MD,wBAAsB,UAAU,CAAC,IAAI,EAAE;IACrC,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,YAAY,CAAC;IACvB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,CAuC9C;AAED,wBAAsB,iBAAiB,CAAC,IAAI,EAAE;IAC5C,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,GAAG,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,CA+BlC;AAED,wBAAsB,QAAQ,CAAC,IAAI,EAAE;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG,OAAO,CAAC,UAAU,CAAC,CActB;AAED,wBAAsB,SAAS,CAAC,IAAI,EAAE;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CA0BxB"}
@@ -6,6 +6,9 @@ import { parseTaskReadme, renderTaskReadme } from "./task-readme.js";
6
6
  function nowIso() {
7
7
  return new Date().toISOString();
8
8
  }
9
+ function isRecord(value) {
10
+ return !!value && typeof value === "object" && !Array.isArray(value);
11
+ }
9
12
  export function validateTaskDocMetadata(frontmatter) {
10
13
  const errors = [];
11
14
  if (frontmatter.doc_version !== 2)
@@ -107,6 +110,146 @@ function setMarkdownSection(body, section, text) {
107
110
  const out = [...lines.slice(0, start + 1), ...replacement, ...lines.slice(nextHeading)];
108
111
  return `${out.join("\n")}\n`;
109
112
  }
113
+ function normalizeDocSectionName(section) {
114
+ return section.trim().replaceAll(/\s+/g, " ").toLowerCase();
115
+ }
116
+ function getLastCommentAuthor(frontmatter) {
117
+ const comments = frontmatter.comments;
118
+ if (!Array.isArray(comments))
119
+ return null;
120
+ const entries = comments;
121
+ for (let i = entries.length - 1; i >= 0; i -= 1) {
122
+ const entry = entries[i];
123
+ if (!isRecord(entry))
124
+ continue;
125
+ const author = entry.author;
126
+ if (typeof author === "string") {
127
+ const trimmed = author.trim();
128
+ if (trimmed)
129
+ return trimmed;
130
+ }
131
+ }
132
+ return null;
133
+ }
134
+ function resolveDocUpdatedBy(frontmatter, updatedBy) {
135
+ if (updatedBy != null) {
136
+ const explicit = updatedBy.trim();
137
+ if (explicit.length === 0) {
138
+ throw new Error("doc_updated_by must be a non-empty string");
139
+ }
140
+ return explicit;
141
+ }
142
+ const lastAuthor = getLastCommentAuthor(frontmatter);
143
+ if (lastAuthor)
144
+ return lastAuthor;
145
+ const existing = frontmatter.doc_updated_by;
146
+ if (typeof existing === "string") {
147
+ const trimmed = existing.trim();
148
+ if (trimmed)
149
+ return trimmed;
150
+ }
151
+ return "agentplane";
152
+ }
153
+ function splitCombinedHeadingLines(doc) {
154
+ const lines = doc.replaceAll("\r\n", "\n").split("\n");
155
+ const out = [];
156
+ let inFence = false;
157
+ for (const line of lines) {
158
+ const trimmed = line.trimStart();
159
+ if (trimmed.startsWith("```")) {
160
+ inFence = !inFence;
161
+ out.push(line);
162
+ continue;
163
+ }
164
+ if (!inFence && line.includes("## ")) {
165
+ const matches = [...line.matchAll(/##\s+/g)];
166
+ if (matches.length > 1 && matches[0]?.index === 0) {
167
+ let start = 0;
168
+ for (let i = 1; i < matches.length; i += 1) {
169
+ const idx = matches[i]?.index ?? 0;
170
+ const chunk = line.slice(start, idx).trimEnd();
171
+ if (chunk)
172
+ out.push(chunk);
173
+ start = idx;
174
+ }
175
+ const last = line.slice(start).trimEnd();
176
+ if (last)
177
+ out.push(last);
178
+ continue;
179
+ }
180
+ }
181
+ out.push(line);
182
+ }
183
+ return out;
184
+ }
185
+ function normalizeDocSections(doc, required) {
186
+ const lines = splitCombinedHeadingLines(doc);
187
+ const sections = new Map();
188
+ const order = [];
189
+ const pendingSeparator = new Set();
190
+ let currentKey = null;
191
+ for (const line of lines) {
192
+ const match = /^##\s+(.*)$/.exec(line.trim());
193
+ if (match) {
194
+ const title = match[1]?.trim() ?? "";
195
+ const key = normalizeDocSectionName(title);
196
+ if (key) {
197
+ const existing = sections.get(key);
198
+ if (existing) {
199
+ if (existing.lines.some((entry) => entry.trim() !== "")) {
200
+ pendingSeparator.add(key);
201
+ }
202
+ }
203
+ else {
204
+ sections.set(key, { title, lines: [] });
205
+ order.push(key);
206
+ }
207
+ currentKey = key;
208
+ continue;
209
+ }
210
+ }
211
+ if (currentKey) {
212
+ const entry = sections.get(currentKey);
213
+ if (!entry)
214
+ continue;
215
+ if (pendingSeparator.has(currentKey) && line.trim() !== "") {
216
+ entry.lines.push("");
217
+ pendingSeparator.delete(currentKey);
218
+ }
219
+ entry.lines.push(line);
220
+ }
221
+ }
222
+ const out = [];
223
+ const seen = new Set(order);
224
+ for (const key of order) {
225
+ const section = sections.get(key);
226
+ if (!section)
227
+ continue;
228
+ out.push(`## ${section.title}`);
229
+ if (section.lines.length > 0) {
230
+ out.push(...section.lines);
231
+ }
232
+ else {
233
+ out.push("");
234
+ }
235
+ out.push("");
236
+ }
237
+ for (const requiredSection of required) {
238
+ const requiredKey = normalizeDocSectionName(requiredSection);
239
+ if (seen.has(requiredKey))
240
+ continue;
241
+ out.push(`## ${requiredSection}`, "", "");
242
+ }
243
+ return `${out.join("\n").trimEnd()}\n`;
244
+ }
245
+ function ensureDocSections(doc, required) {
246
+ const trimmed = doc.trim();
247
+ if (!trimmed) {
248
+ const blocks = required.map((section) => `## ${section}\n`);
249
+ return `${blocks.join("\n").trimEnd()}\n`;
250
+ }
251
+ return normalizeDocSections(doc, required);
252
+ }
110
253
  export async function createTask(opts) {
111
254
  const { tasksDir, idSuffixLengthDefault } = await getTasksDir({
112
255
  cwd: opts.cwd,
@@ -154,16 +297,15 @@ export async function setTaskDocSection(opts) {
154
297
  const readmePath = taskReadmePath(tasksDir, opts.taskId);
155
298
  const original = await readFile(readmePath, "utf8");
156
299
  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");
300
+ const updatedBy = resolveDocUpdatedBy(parsed.frontmatter, opts.updatedBy);
160
301
  const nextFrontmatter = {
161
302
  ...parsed.frontmatter,
162
303
  doc_version: 2,
163
304
  doc_updated_at: nowIso(),
164
305
  doc_updated_by: updatedBy,
165
306
  };
166
- const nextBody = setMarkdownSection(parsed.body, opts.section, opts.text);
307
+ const baseDoc = ensureDocSections(parsed.body, loaded.config.tasks.doc.required_sections);
308
+ const nextBody = ensureDocSections(setMarkdownSection(baseDoc, opts.section, opts.text), loaded.config.tasks.doc.required_sections);
167
309
  const nextText = renderTaskReadme(nextFrontmatter, nextBody);
168
310
  await writeFile(readmePath, nextText, "utf8");
169
311
  return { readmePath };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentplaneorg/core",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Core utilities and models for the Agent Plane CLI.",
5
5
  "keywords": [
6
6
  "agentplane",