@anhlp/alp-core 0.0.0-beta.1
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 +202 -0
- package/NOTICE +7 -0
- package/README.md +7 -0
- package/dist/adapter.d.ts +20 -0
- package/dist/adapter.js +30 -0
- package/dist/atomic.d.ts +2 -0
- package/dist/atomic.js +28 -0
- package/dist/backup.d.ts +27 -0
- package/dist/backup.js +582 -0
- package/dist/beads.d.ts +65 -0
- package/dist/beads.js +213 -0
- package/dist/delegation.d.ts +2 -0
- package/dist/delegation.js +32 -0
- package/dist/errors.d.ts +4 -0
- package/dist/errors.js +11 -0
- package/dist/formulas.d.ts +25 -0
- package/dist/formulas.js +201 -0
- package/dist/hook-run.d.ts +44 -0
- package/dist/hook-run.js +60 -0
- package/dist/hooks.d.ts +35 -0
- package/dist/hooks.js +30 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +63 -0
- package/dist/init.d.ts +30 -0
- package/dist/init.js +88 -0
- package/dist/library-edit.d.ts +90 -0
- package/dist/library-edit.js +315 -0
- package/dist/library.d.ts +28 -0
- package/dist/library.js +108 -0
- package/dist/log.d.ts +37 -0
- package/dist/log.js +156 -0
- package/dist/migrations.d.ts +9 -0
- package/dist/migrations.js +95 -0
- package/dist/models.d.ts +11 -0
- package/dist/models.js +44 -0
- package/dist/permissions.d.ts +14 -0
- package/dist/permissions.js +229 -0
- package/dist/profiles.d.ts +27 -0
- package/dist/profiles.js +122 -0
- package/dist/promptsafe.d.ts +1 -0
- package/dist/promptsafe.js +13 -0
- package/dist/providers.d.ts +90 -0
- package/dist/providers.js +101 -0
- package/dist/resolver.d.ts +41 -0
- package/dist/resolver.js +240 -0
- package/dist/safe-path.d.ts +2 -0
- package/dist/safe-path.js +24 -0
- package/dist/shell.d.ts +16 -0
- package/dist/shell.js +22 -0
- package/dist/state-schema.d.ts +9 -0
- package/dist/state-schema.js +54 -0
- package/dist/task-rows.d.ts +47 -0
- package/dist/task-rows.js +45 -0
- package/dist/tasks.d.ts +128 -0
- package/dist/tasks.js +835 -0
- package/dist/teams.d.ts +69 -0
- package/dist/teams.js +159 -0
- package/dist/template-path.d.ts +2 -0
- package/dist/template-path.js +10 -0
- package/dist/trust.d.ts +8 -0
- package/dist/trust.js +47 -0
- package/dist/types.d.ts +51 -0
- package/dist/upgrade.d.ts +10 -0
- package/dist/upgrade.js +200 -0
- package/dist/user-settings.d.ts +6 -0
- package/dist/user-settings.js +72 -0
- package/dist/validation.d.ts +17 -0
- package/dist/validation.js +295 -0
- package/dist/verify.d.ts +15 -0
- package/dist/verify.js +123 -0
- package/dist/workflow.d.ts +14 -0
- package/dist/workflow.js +24 -0
- package/package.json +309 -0
- package/src/adapter.d.ts +20 -0
- package/src/adapter.js +29 -0
- package/src/atomic.d.ts +2 -0
- package/src/atomic.js +28 -0
- package/src/backup.d.ts +27 -0
- package/src/backup.js +570 -0
- package/src/beads.js +236 -0
- package/src/delegation.js +28 -0
- package/src/errors.js +7 -0
- package/src/formulas.d.ts +25 -0
- package/src/formulas.js +202 -0
- package/src/hook-run.js +57 -0
- package/src/hooks.js +36 -0
- package/src/index.js +31 -0
- package/src/init.js +92 -0
- package/src/library-edit.js +346 -0
- package/src/library.js +117 -0
- package/src/log.d.ts +37 -0
- package/src/log.js +177 -0
- package/src/migrations.d.ts +9 -0
- package/src/migrations.js +111 -0
- package/src/models.js +45 -0
- package/src/permissions.d.ts +14 -0
- package/src/permissions.js +242 -0
- package/src/profiles.js +139 -0
- package/src/promptsafe.d.ts +1 -0
- package/src/promptsafe.js +19 -0
- package/src/providers.js +122 -0
- package/src/resolver.js +238 -0
- package/src/safe-path.js +20 -0
- package/src/shell.js +28 -0
- package/src/state-schema.d.ts +9 -0
- package/src/state-schema.js +60 -0
- package/src/task-rows.js +44 -0
- package/src/tasks.d.ts +128 -0
- package/src/tasks.js +881 -0
- package/src/teams.js +170 -0
- package/src/template-path.js +8 -0
- package/src/trust.js +59 -0
- package/src/types.d.ts +51 -0
- package/src/upgrade.js +195 -0
- package/src/user-settings.d.ts +6 -0
- package/src/user-settings.js +76 -0
- package/src/validation.d.ts +17 -0
- package/src/validation.js +302 -0
- package/src/verify.d.ts +15 -0
- package/src/verify.js +127 -0
- package/src/workflow.js +28 -0
- package/templates/ALP.md +52 -0
- package/templates/agents/lead/AGENT.md +90 -0
- package/templates/agents/main/AGENT.md +150 -0
- package/templates/agents/oracle/AGENT.md +44 -0
- package/templates/agents/peer/AGENT.md +55 -0
- package/templates/agents/reviewer/AGENT.md +80 -0
- package/templates/agents/supervisor/AGENT.md +72 -0
- package/templates/providers/antigravity.json +6 -0
- package/templates/providers/opencode.json +7 -0
- package/templates/role-skills.json +8 -0
- package/templates/skills/bug-loop/SKILL.md +44 -0
- package/templates/skills/bug-loop/references/test-proof.md +25 -0
- package/templates/skills/goal-griller/SKILL.md +49 -0
- package/templates/skills/prompt-leverage/SKILL.md +37 -0
- package/templates/skills/prompt-leverage/references/brief-template.md +27 -0
- package/templates/skills/sequence-execution-plan/SKILL.md +42 -0
- package/templates/skills/sequence-execution-plan/references/plan-template.md +22 -0
- package/templates/skills/smart-commits/SKILL.md +40 -0
- package/templates/skills/xia/SKILL.md +42 -0
- package/templates/teams/cafe/HOUSE_RULES.md +1 -0
- package/templates/teams/cafe/team.json +15 -0
- package/templates/teams/pho/HOUSE_RULES.md +1 -0
- package/templates/teams/pho/team.json +14 -0
package/dist/resolver.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// packages/core/src/resolver.js
|
|
2
|
+
import { shippedTemplateRoot } from "./template-path.js";
|
|
3
|
+
import { readFile, readdir, stat } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { AlpError } from "./errors.js";
|
|
6
|
+
import { validateAgentConfig, validateHook, validateSettings, normalizeMcp, validateResolvedAgent } from "./validation.js";
|
|
7
|
+
import { librarySkills } from "./library.js";
|
|
8
|
+
import { AlpError as AlpError2 } from "./errors.js";
|
|
9
|
+
async function optionalText(file) {
|
|
10
|
+
try {
|
|
11
|
+
return await readFile(file, "utf8");
|
|
12
|
+
} catch (error) {
|
|
13
|
+
if (error.code === "ENOENT") return void 0;
|
|
14
|
+
throw error;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
async function jsonObject(file, code) {
|
|
18
|
+
const text = await optionalText(file);
|
|
19
|
+
if (text === void 0) return {};
|
|
20
|
+
try {
|
|
21
|
+
const value = JSON.parse(text);
|
|
22
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("expected a JSON object");
|
|
23
|
+
return value;
|
|
24
|
+
} catch (cause) {
|
|
25
|
+
throw new AlpError(code, `${file}: ${cause.message}`, { cause });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async function entries(directory) {
|
|
29
|
+
try {
|
|
30
|
+
return (await readdir(directory, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (error.code === "ENOENT") return [];
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
var BUILTIN_AGENTS = ["main", "lead", "peer", "oracle", "reviewer", "supervisor"];
|
|
37
|
+
var templateRoot = () => {
|
|
38
|
+
try {
|
|
39
|
+
return shippedTemplateRoot();
|
|
40
|
+
} catch {
|
|
41
|
+
return void 0;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
var validName = (name) => typeof name === "string" && /^[\w.-]+$/.test(name) && name !== "." && name !== "..";
|
|
45
|
+
async function builtinText(relative, templates) {
|
|
46
|
+
if (templates) return templates[relative];
|
|
47
|
+
const root = templateRoot();
|
|
48
|
+
if (!root) return void 0;
|
|
49
|
+
try {
|
|
50
|
+
return await readFile(new URL(relative, root), "utf8");
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (error.code === "ENOENT" || error.code === "ENOTDIR") return void 0;
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function agentSources(root, { library, templates } = {}) {
|
|
57
|
+
const found = /* @__PURE__ */ new Map();
|
|
58
|
+
for (const name of BUILTIN_AGENTS) if (await builtinText(`agents/${name}/AGENT.md`, templates) !== void 0) found.set(name, { source: "builtin" });
|
|
59
|
+
const layer = async (directory, source) => {
|
|
60
|
+
for (const entry of await entries(directory)) {
|
|
61
|
+
if (!entry.isDirectory() || !validName(entry.name)) continue;
|
|
62
|
+
if (source === "library" && await optionalText(path.join(directory, entry.name, "AGENT.md")) === void 0) continue;
|
|
63
|
+
const previous = found.get(entry.name);
|
|
64
|
+
found.set(entry.name, { source, directory: path.join(directory, entry.name), ...previous ? { overrides: previous.source } : {} });
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
if (library) await layer(path.join(library, "agents"), "library");
|
|
68
|
+
if (root !== void 0) await layer(path.join(path.resolve(root), ".alp", "agents"), "project");
|
|
69
|
+
return found;
|
|
70
|
+
}
|
|
71
|
+
async function discoverAgents(root, options = {}) {
|
|
72
|
+
return [...(await agentSources(root, options)).keys()].sort((a, b) => a.localeCompare(b));
|
|
73
|
+
}
|
|
74
|
+
var KINDS = {
|
|
75
|
+
skills: (name) => path.join(name, "SKILL.md"),
|
|
76
|
+
mcp: (name) => `${name}.json`,
|
|
77
|
+
hooks: (name) => `${name}.json`,
|
|
78
|
+
// ACP providers (ALPD §46) live only in the library.
|
|
79
|
+
providers: (name) => `${name}.json`
|
|
80
|
+
};
|
|
81
|
+
var LIBRARY_KINDS = ["agents", ...Object.keys(KINDS)];
|
|
82
|
+
async function libraryEntries(kind, projectRoot, { library, templates } = {}) {
|
|
83
|
+
const root = projectRoot === void 0 ? void 0 : path.resolve(projectRoot);
|
|
84
|
+
if (kind === "agents") {
|
|
85
|
+
const rows = [];
|
|
86
|
+
for (const [name, source] of await agentSources(root, { library, templates })) {
|
|
87
|
+
const text = source.directory ? await optionalText(path.join(source.directory, "agent.json")) : await builtinText(`agents/${name}/agent.json`, templates);
|
|
88
|
+
let description;
|
|
89
|
+
try {
|
|
90
|
+
description = text === void 0 ? void 0 : JSON.parse(text).description;
|
|
91
|
+
} catch {
|
|
92
|
+
}
|
|
93
|
+
rows.push({ name, source: source.source, ...source.directory ? { path: source.directory } : {}, ...source.overrides ? { overrides: source.overrides } : {}, ...typeof description === "string" ? { description } : {} });
|
|
94
|
+
}
|
|
95
|
+
return rows.sort((a, b) => a.name.localeCompare(b.name));
|
|
96
|
+
}
|
|
97
|
+
const file = KINDS[kind];
|
|
98
|
+
if (!file) throw new AlpError("INVALID_KIND", `Unknown library kind '${kind}'; use one of ${LIBRARY_KINDS.join(", ")}`);
|
|
99
|
+
const found = /* @__PURE__ */ new Map();
|
|
100
|
+
const layer = async (directory, source) => {
|
|
101
|
+
for (const entry of await entries(directory)) {
|
|
102
|
+
const name = kind === "skills" ? entry.isDirectory() ? entry.name : void 0 : entry.isFile() && entry.name.endsWith(".json") ? entry.name.slice(0, -5) : void 0;
|
|
103
|
+
if (!name || !validName(name)) continue;
|
|
104
|
+
const entryPath = path.join(directory, file(name));
|
|
105
|
+
if (!await stat(entryPath).then((info) => info.isFile(), () => false)) continue;
|
|
106
|
+
const previous = found.get(name);
|
|
107
|
+
found.set(name, { name, source, path: entryPath, ...previous ? { overrides: previous.source } : {} });
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
if (library) await layer(path.join(library, kind), "library");
|
|
111
|
+
if (root !== void 0 && kind !== "providers") await layer(path.join(root, ".alp", kind), "project");
|
|
112
|
+
const users = /* @__PURE__ */ new Map();
|
|
113
|
+
const use = (name, agent) => users.set(name, [.../* @__PURE__ */ new Set([...users.get(name) ?? [], agent])]);
|
|
114
|
+
for (const [agent, source] of await agentSources(root, { library, templates })) {
|
|
115
|
+
const text = source.directory ? await optionalText(path.join(source.directory, "agent.json")) : await builtinText(`agents/${agent}/agent.json`, templates);
|
|
116
|
+
try {
|
|
117
|
+
const config = text === void 0 ? {} : JSON.parse(text);
|
|
118
|
+
for (const name of kind === "providers" ? [config.provider].filter(Boolean) : config[kind] ?? []) use(name, agent);
|
|
119
|
+
} catch {
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (kind === "skills" && library) {
|
|
123
|
+
try {
|
|
124
|
+
for (const [role, names] of Object.entries(JSON.parse(await optionalText(path.join(library, "role-skills.json")) ?? "{}"))) for (const name of Array.isArray(names) ? names : []) use(name, role);
|
|
125
|
+
} catch {
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return [...found.values()].map((row) => ({ ...row, ...users.has(row.name) ? { usedBy: users.get(row.name).sort() } : {} })).sort((a, b) => a.name.localeCompare(b.name));
|
|
129
|
+
}
|
|
130
|
+
async function namedEntry(root, library, kind, name, file) {
|
|
131
|
+
const candidates = [path.join(root, ".alp", kind, file(name)), ...library ? [path.join(library, kind, file(name))] : []];
|
|
132
|
+
for (const candidate of candidates) if (await stat(candidate).then((info) => info.isFile(), () => false)) return candidate;
|
|
133
|
+
return void 0;
|
|
134
|
+
}
|
|
135
|
+
async function resolveAgent(projectRoot, { agent, library, templates } = {}) {
|
|
136
|
+
const root = path.resolve(projectRoot);
|
|
137
|
+
const settingsPath = path.join(root, ".alp", "settings.json");
|
|
138
|
+
const settings = await jsonObject(settingsPath, "INVALID_SETTINGS");
|
|
139
|
+
const projectRuntime = validateSettings(settings, settingsPath);
|
|
140
|
+
const name = agent ?? settings.defaultAgent ?? "main";
|
|
141
|
+
if (typeof name !== "string" || !name.trim() || name === "." || name === ".." || /[\\/\x00]/.test(name)) {
|
|
142
|
+
throw new AlpError("INVALID_AGENT", "Agent name must be a single nonempty directory name");
|
|
143
|
+
}
|
|
144
|
+
const source = (await agentSources(root, { library, templates })).get(name);
|
|
145
|
+
if (!source) throw new AlpError("AGENT_NOT_FOUND", `Agent '${name}' not found: it is not built in, nor under ${library ? `${path.join(library, "agents")} or ` : ""}${path.join(root, ".alp", "agents")}`);
|
|
146
|
+
const directory = source.directory;
|
|
147
|
+
const agentPath = directory ? path.join(directory, "AGENT.md") : `built-in agents/${name}/AGENT.md`;
|
|
148
|
+
const instructions = directory ? await optionalText(agentPath) : await builtinText(`agents/${name}/AGENT.md`, templates);
|
|
149
|
+
if (instructions === void 0) throw new AlpError("AGENT_INSTRUCTIONS_MISSING", `Missing ${agentPath}`);
|
|
150
|
+
const configPath = directory ? path.join(directory, "agent.json") : `built-in agents/${name}/agent.json`;
|
|
151
|
+
const configText = directory ? await optionalText(configPath) : await builtinText(`agents/${name}/agent.json`, templates);
|
|
152
|
+
let config = {};
|
|
153
|
+
if (configText !== void 0) {
|
|
154
|
+
try {
|
|
155
|
+
config = JSON.parse(configText);
|
|
156
|
+
} catch (cause) {
|
|
157
|
+
throw new AlpError("INVALID_AGENT_CONFIG", `${configPath}: ${cause.message}`, { cause });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
validateAgentConfig(config, configPath);
|
|
161
|
+
const skills = [];
|
|
162
|
+
for (const skill of library ? await librarySkills(library, name) : []) {
|
|
163
|
+
skills.push({ name: skill.name, path: await namedEntry(root, void 0, "skills", skill.name, (entry) => path.join(entry, "SKILL.md")) ?? skill.path });
|
|
164
|
+
}
|
|
165
|
+
const addSkill = (skillName, skillPath) => {
|
|
166
|
+
const shadowed = skills.findIndex((skill) => skill.name === skillName);
|
|
167
|
+
if (shadowed >= 0) skills.splice(shadowed, 1);
|
|
168
|
+
skills.push({ name: skillName, path: skillPath });
|
|
169
|
+
};
|
|
170
|
+
for (const skillName of config.skills ?? []) {
|
|
171
|
+
const skillPath = await namedEntry(root, library, "skills", skillName, (entry) => path.join(entry, "SKILL.md"));
|
|
172
|
+
if (!skillPath) throw new AlpError("SKILL_NOT_FOUND", `${configPath}: skill '${skillName}' is neither in ${path.join(root, ".alp", "skills")} nor in the library`);
|
|
173
|
+
addSkill(skillName, skillPath);
|
|
174
|
+
}
|
|
175
|
+
if (directory) {
|
|
176
|
+
for (const entry of await entries(path.join(directory, "skills"))) {
|
|
177
|
+
if (!entry.isDirectory()) continue;
|
|
178
|
+
const skillPath = path.join(directory, "skills", entry.name, "SKILL.md");
|
|
179
|
+
try {
|
|
180
|
+
if ((await stat(skillPath)).isFile()) addSkill(entry.name, skillPath);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
if (error.code !== "ENOENT") throw error;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
187
|
+
const hooks = directory ? (await entries(path.join(directory, "hooks"))).filter((e) => e.isFile()).map((e) => ({ name: e.name, path: path.join(directory, "hooks", e.name) })) : [];
|
|
188
|
+
for (const hookName of config.hooks ?? []) {
|
|
189
|
+
const hookPath = await namedEntry(root, library, "hooks", hookName, (entry) => `${entry}.json`);
|
|
190
|
+
if (!hookPath) throw new AlpError("HOOK_NOT_FOUND", `${configPath}: hook '${hookName}' is neither in ${path.join(root, ".alp", "hooks")} nor in the library`);
|
|
191
|
+
validateHook(await jsonObject(hookPath, "INVALID_HOOK"), hookPath);
|
|
192
|
+
hooks.push({ name: hookName, path: hookPath });
|
|
193
|
+
}
|
|
194
|
+
const servers = {};
|
|
195
|
+
for (const serverName of config.mcp ?? []) {
|
|
196
|
+
const serverPath = await namedEntry(root, library, "mcp", serverName, (entry) => `${entry}.json`);
|
|
197
|
+
if (!serverPath) throw new AlpError("MCP_NOT_FOUND", `${configPath}: MCP server '${serverName}' is neither in ${path.join(root, ".alp", "mcp")} nor in the library`);
|
|
198
|
+
const definition = await jsonObject(serverPath, "INVALID_MCP");
|
|
199
|
+
Object.assign(servers, normalizeMcp({ mcpServers: { [serverName]: definition } }, path.dirname(serverPath), serverPath).mcpServers);
|
|
200
|
+
}
|
|
201
|
+
const mcpPath = directory ? path.join(directory, ".mcp.json") : void 0;
|
|
202
|
+
const own = mcpPath ? normalizeMcp(await jsonObject(mcpPath, "INVALID_MCP"), directory, mcpPath).mcpServers : {};
|
|
203
|
+
for (const [serverName, server] of Object.entries(own)) {
|
|
204
|
+
if (Object.hasOwn(servers, serverName)) throw new AlpError("INVALID_MCP", `${mcpPath}: MCP server '${serverName}' is also named in ${configPath}`);
|
|
205
|
+
servers[serverName] = server;
|
|
206
|
+
}
|
|
207
|
+
const runtime = {
|
|
208
|
+
...config.provider !== void 0 ? { provider: config.provider } : {},
|
|
209
|
+
...config.model !== void 0 ? { model: config.model } : {},
|
|
210
|
+
...config.thinking !== void 0 ? { reasoning: config.thinking } : {},
|
|
211
|
+
...projectRuntime
|
|
212
|
+
};
|
|
213
|
+
return validateResolvedAgent({
|
|
214
|
+
name,
|
|
215
|
+
projectRoot: root,
|
|
216
|
+
source: source.source,
|
|
217
|
+
instructions: { project: await optionalText(path.join(root, "ALP.md")) ?? "", agent: instructions },
|
|
218
|
+
skills,
|
|
219
|
+
hooks,
|
|
220
|
+
runtime,
|
|
221
|
+
projectRuntime: { ...projectRuntime },
|
|
222
|
+
...config.mode !== void 0 ? { mode: config.mode } : {},
|
|
223
|
+
...config.description !== void 0 ? { description: config.description } : {},
|
|
224
|
+
...typeof config.context === "number" ? { context: config.context } : {},
|
|
225
|
+
...config.autocompact !== void 0 ? { autocompact: config.autocompact } : {},
|
|
226
|
+
mcp: { mcpServers: servers }
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
export {
|
|
230
|
+
AlpError2 as AlpError,
|
|
231
|
+
BUILTIN_AGENTS,
|
|
232
|
+
LIBRARY_KINDS,
|
|
233
|
+
agentSources,
|
|
234
|
+
builtinText,
|
|
235
|
+
discoverAgents,
|
|
236
|
+
jsonObject,
|
|
237
|
+
libraryEntries,
|
|
238
|
+
optionalText,
|
|
239
|
+
resolveAgent
|
|
240
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// packages/core/src/safe-path.js
|
|
2
|
+
import { lstat } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { AlpError } from "./errors.js";
|
|
5
|
+
async function assertNoSymlinks(directory, relative) {
|
|
6
|
+
const root = path.resolve(directory);
|
|
7
|
+
const target = path.resolve(root, relative);
|
|
8
|
+
const tail = path.relative(root, target);
|
|
9
|
+
if (tail === ".." || tail.startsWith(`..${path.sep}`) || path.isAbsolute(tail)) {
|
|
10
|
+
throw new AlpError("UNSAFE_PATH", "Path escapes its scope");
|
|
11
|
+
}
|
|
12
|
+
let current = root;
|
|
13
|
+
for (const part of ["", ...tail.split(path.sep).filter(Boolean)]) {
|
|
14
|
+
if (part) current = path.join(current, part);
|
|
15
|
+
const info = await lstat(current).catch((error) => {
|
|
16
|
+
if (error.code !== "ENOENT") throw error;
|
|
17
|
+
});
|
|
18
|
+
if (!info) return;
|
|
19
|
+
if (info.isSymbolicLink()) throw new AlpError("UNSAFE_PATH", `Refusing symlink in scoped path: ${current}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export {
|
|
23
|
+
assertNoSymlinks
|
|
24
|
+
};
|
package/dist/shell.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Starts `command` through the platform's shell: /bin/sh -c in its own process group, or on
|
|
3
|
+
* Windows cmd.exe /d /s /c with the line quoted whole (cmd strips only the outermost quotes,
|
|
4
|
+
* and Node would otherwise escape the quotes inside it).
|
|
5
|
+
* @param {string} command
|
|
6
|
+
* @param {import('node:child_process').SpawnOptions} options
|
|
7
|
+
*/
|
|
8
|
+
export function spawnShell(command: string, options: import("node:child_process").SpawnOptions): import("child_process").ChildProcess;
|
|
9
|
+
/**
|
|
10
|
+
* Signals a command `spawnShell` started and everything it started. Windows has no signals and
|
|
11
|
+
* cmd.exe's children outlive it, so there the whole tree is ended at once by the first signal
|
|
12
|
+
* and no cleanup runs.
|
|
13
|
+
* @param {import('node:child_process').ChildProcess} child
|
|
14
|
+
* @param {NodeJS.Signals} name
|
|
15
|
+
*/
|
|
16
|
+
export function signalShell(child: import("node:child_process").ChildProcess, name: NodeJS.Signals): void;
|
package/dist/shell.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// packages/core/src/shell.js
|
|
2
|
+
import { execFile, spawn } from "node:child_process";
|
|
3
|
+
function spawnShell(command, options) {
|
|
4
|
+
if (process.platform !== "win32") return spawn("/bin/sh", ["-c", command], { ...options, detached: true });
|
|
5
|
+
return spawn(process.env.ComSpec ?? "cmd.exe", ["/d", "/s", "/c", `"${command}"`], { ...options, windowsHide: true, windowsVerbatimArguments: true });
|
|
6
|
+
}
|
|
7
|
+
function signalShell(child, name) {
|
|
8
|
+
if (process.platform === "win32") {
|
|
9
|
+
if (name === "SIGTERM") execFile("taskkill.exe", ["/PID", String(child.pid), "/T", "/F"], { windowsHide: true }, () => {
|
|
10
|
+
});
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
process.kill(-child.pid, name);
|
|
15
|
+
} catch {
|
|
16
|
+
child.kill(name);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export {
|
|
20
|
+
signalShell,
|
|
21
|
+
spawnShell
|
|
22
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const STATE_SCHEMA: number;
|
|
2
|
+
export const OLDEST_STATE_SCHEMA: number;
|
|
3
|
+
export function stateDir(home: string): string;
|
|
4
|
+
export function schemaFile(home: string): string;
|
|
5
|
+
export function migrationFile(home: string): string;
|
|
6
|
+
export type StateSchemaInfo = { schema: number; fresh: boolean; marked: boolean; by?: string };
|
|
7
|
+
export function readStateSchema(home: string): Promise<StateSchemaInfo>;
|
|
8
|
+
export function assertStateSupported(info: StateSchemaInfo, home: string): StateSchemaInfo;
|
|
9
|
+
export function writeStateSchema(home: string, schema: number, by?: string): Promise<void>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// packages/core/src/state-schema.js
|
|
2
|
+
import { readdir, readFile, mkdir } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { AlpError } from "./errors.js";
|
|
5
|
+
import { writeAtomic } from "./atomic.js";
|
|
6
|
+
var STATE_SCHEMA = 2;
|
|
7
|
+
var OLDEST_STATE_SCHEMA = 1;
|
|
8
|
+
var stateDir = (home) => path.join(home, "state");
|
|
9
|
+
var schemaFile = (home) => path.join(stateDir(home), "schema.json");
|
|
10
|
+
var migrationFile = (home) => path.join(stateDir(home), "migration.json");
|
|
11
|
+
var DATA = ["sessions", "timeline", "receipts"];
|
|
12
|
+
async function readStateSchema(home) {
|
|
13
|
+
let text;
|
|
14
|
+
try {
|
|
15
|
+
text = await readFile(schemaFile(home), "utf8");
|
|
16
|
+
} catch (error) {
|
|
17
|
+
if (error?.code !== "ENOENT") throw error;
|
|
18
|
+
for (const name of DATA) {
|
|
19
|
+
const entries = await readdir(path.join(stateDir(home), name)).catch((error2) => {
|
|
20
|
+
if (error2?.code === "ENOENT") return [];
|
|
21
|
+
throw error2;
|
|
22
|
+
});
|
|
23
|
+
if (entries.some((entry) => !entry.endsWith(".tmp"))) return { schema: 1, fresh: false, marked: false };
|
|
24
|
+
}
|
|
25
|
+
return { schema: STATE_SCHEMA, fresh: true, marked: false };
|
|
26
|
+
}
|
|
27
|
+
let marker;
|
|
28
|
+
try {
|
|
29
|
+
marker = JSON.parse(text);
|
|
30
|
+
} catch {
|
|
31
|
+
}
|
|
32
|
+
if (!marker || !Number.isSafeInteger(marker.schema) || marker.schema < 1) {
|
|
33
|
+
throw new AlpError("STATE_SCHEMA_UNREADABLE", `${schemaFile(home)} does not name a state schema; ALP does not open state whose format it cannot tell. Restore a backup, or put back the file`);
|
|
34
|
+
}
|
|
35
|
+
return { schema: marker.schema, fresh: false, marked: true, ...typeof marker.by === "string" ? { by: marker.by } : {} };
|
|
36
|
+
}
|
|
37
|
+
function assertStateSupported(info, home) {
|
|
38
|
+
if (info.schema <= STATE_SCHEMA) return info;
|
|
39
|
+
throw new AlpError("STATE_SCHEMA_TOO_NEW", `${stateDir(home)} holds state schema ${info.schema}${info.by ? `, written by ${info.by}` : ""}; this ALP knows schema ${STATE_SCHEMA} at most and leaves it untouched. Run that ALP version or a newer one, or replace it with a backup made for this version: alp restore FILE --replace-newer (docs/alp/data-recovery.md)`);
|
|
40
|
+
}
|
|
41
|
+
async function writeStateSchema(home, schema, by) {
|
|
42
|
+
await mkdir(stateDir(home), { recursive: true, mode: 448 });
|
|
43
|
+
await writeAtomic(schemaFile(home), JSON.stringify({ schema, by, at: (/* @__PURE__ */ new Date()).toISOString() }) + "\n");
|
|
44
|
+
}
|
|
45
|
+
export {
|
|
46
|
+
OLDEST_STATE_SCHEMA,
|
|
47
|
+
STATE_SCHEMA,
|
|
48
|
+
assertStateSupported,
|
|
49
|
+
migrationFile,
|
|
50
|
+
readStateSchema,
|
|
51
|
+
schemaFile,
|
|
52
|
+
stateDir,
|
|
53
|
+
writeStateSchema
|
|
54
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A project's tasks as task screens show them (ALPD §22): readiness, what blocks or waits,
|
|
3
|
+
* approvals for the user, handoffs and progress. Used by the Paseo Tasks panel and the web app.
|
|
4
|
+
* @param {string} projectRoot
|
|
5
|
+
*/
|
|
6
|
+
export function taskRows(projectRoot: string): Promise<{
|
|
7
|
+
tasks: {
|
|
8
|
+
createdAt: string;
|
|
9
|
+
updatedAt: string;
|
|
10
|
+
progress?: {
|
|
11
|
+
done: number;
|
|
12
|
+
total: number;
|
|
13
|
+
} | undefined;
|
|
14
|
+
paths?: string[] | undefined;
|
|
15
|
+
labels?: string[] | undefined;
|
|
16
|
+
description?: string | undefined;
|
|
17
|
+
closed?: {
|
|
18
|
+
at: string;
|
|
19
|
+
summary?: string | undefined;
|
|
20
|
+
reason: import("./tasks.js").CloseReason;
|
|
21
|
+
} | undefined;
|
|
22
|
+
handoff?: {
|
|
23
|
+
agent?: string | undefined;
|
|
24
|
+
outcome: string;
|
|
25
|
+
summary: string;
|
|
26
|
+
} | undefined;
|
|
27
|
+
approvals?: {
|
|
28
|
+
gate: string;
|
|
29
|
+
note: string;
|
|
30
|
+
}[] | undefined;
|
|
31
|
+
waits?: string[] | undefined;
|
|
32
|
+
blockedBy?: string[] | undefined;
|
|
33
|
+
ready: boolean;
|
|
34
|
+
assignee?: string | undefined;
|
|
35
|
+
parent?: string | undefined;
|
|
36
|
+
id: string;
|
|
37
|
+
title: string;
|
|
38
|
+
type: import("./tasks.js").TaskType;
|
|
39
|
+
priority: number;
|
|
40
|
+
status: import("./tasks.js").TaskStatus;
|
|
41
|
+
rev: number;
|
|
42
|
+
}[];
|
|
43
|
+
unreadable: {
|
|
44
|
+
file: string;
|
|
45
|
+
error: string;
|
|
46
|
+
}[];
|
|
47
|
+
}>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// packages/core/src/task-rows.js
|
|
2
|
+
import { gatesOf, loadTasks, readyTasks, summarize } from "./tasks.js";
|
|
3
|
+
async function taskRows(projectRoot) {
|
|
4
|
+
const { tasks, errors } = await loadTasks(projectRoot);
|
|
5
|
+
const ready = new Set(readyTasks(tasks).map((task) => task.id));
|
|
6
|
+
const children = /* @__PURE__ */ new Map();
|
|
7
|
+
for (const task of tasks) {
|
|
8
|
+
if (!task.parent) continue;
|
|
9
|
+
const count = children.get(task.parent) ?? { done: 0, total: 0 };
|
|
10
|
+
count.total += 1;
|
|
11
|
+
if (task.status === "closed") count.done += 1;
|
|
12
|
+
children.set(task.parent, count);
|
|
13
|
+
}
|
|
14
|
+
const rows = tasks.map((task) => {
|
|
15
|
+
const row = summarize(task, tasks);
|
|
16
|
+
const approvals = task.status === "closed" ? [] : task.gates.filter((gate) => gate.kind === "human" && !gate.resolved).map((gate) => ({ gate: gate.id, note: gate.note ?? "" }));
|
|
17
|
+
const waits = task.status === "closed" ? [] : gatesOf(task, tasks);
|
|
18
|
+
return {
|
|
19
|
+
id: task.id,
|
|
20
|
+
title: task.title,
|
|
21
|
+
type: task.type,
|
|
22
|
+
priority: task.priority,
|
|
23
|
+
status: task.status,
|
|
24
|
+
rev: task.rev,
|
|
25
|
+
...task.parent ? { parent: task.parent } : {},
|
|
26
|
+
...task.assignee ? { assignee: task.assignee.agent } : {},
|
|
27
|
+
ready: ready.has(task.id),
|
|
28
|
+
...row.blockedBy ? { blockedBy: row.blockedBy } : {},
|
|
29
|
+
...waits.length ? { waits } : {},
|
|
30
|
+
...approvals.length ? { approvals } : {},
|
|
31
|
+
...task.handoff ? { handoff: { outcome: String(task.handoff.outcome), summary: String(task.handoff.summary ?? ""), ...typeof task.handoff.agent === "string" ? { agent: task.handoff.agent } : {} } } : {},
|
|
32
|
+
...task.closed ? { closed: { reason: task.closed.reason, ...task.closed.summary ? { summary: task.closed.summary } : {}, at: task.closed.at } } : {},
|
|
33
|
+
...task.description ? { description: task.description.slice(0, 4e3) } : {},
|
|
34
|
+
...task.labels.length ? { labels: task.labels } : {},
|
|
35
|
+
...task.paths.length ? { paths: task.paths } : {},
|
|
36
|
+
...children.has(task.id) ? { progress: children.get(task.id) } : {},
|
|
37
|
+
createdAt: task.createdAt,
|
|
38
|
+
updatedAt: task.updatedAt
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
return { tasks: rows, unreadable: errors };
|
|
42
|
+
}
|
|
43
|
+
export {
|
|
44
|
+
taskRows
|
|
45
|
+
};
|
package/dist/tasks.d.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
export type TaskType = 'task' | 'bug' | 'feature' | 'chore' | 'epic';
|
|
2
|
+
export type TaskStatus = 'open' | 'in_progress' | 'review' | 'closed';
|
|
3
|
+
export type CloseReason = 'done' | 'wontfix' | 'duplicate' | 'superseded';
|
|
4
|
+
|
|
5
|
+
export type TaskLogEntry = { at: string; by: string; event: string; [detail: string]: unknown };
|
|
6
|
+
|
|
7
|
+
export type Task = {
|
|
8
|
+
id: string;
|
|
9
|
+
rev: number;
|
|
10
|
+
title: string;
|
|
11
|
+
description: string;
|
|
12
|
+
type: TaskType;
|
|
13
|
+
priority: number;
|
|
14
|
+
status: TaskStatus;
|
|
15
|
+
labels: string[];
|
|
16
|
+
paths: string[];
|
|
17
|
+
parent: string | null;
|
|
18
|
+
blockedBy: string[];
|
|
19
|
+
discoveredFrom: string | null;
|
|
20
|
+
related: string[];
|
|
21
|
+
gates: TaskGate[];
|
|
22
|
+
assignee: { agent: string; session?: string; assignment?: string; pid?: number; pidStartedAt?: number; epoch?: string; since: string } | null;
|
|
23
|
+
handoff: Record<string, unknown> | null;
|
|
24
|
+
/** The last run of the project's verify commands for this task. */
|
|
25
|
+
verified: TaskVerification | null;
|
|
26
|
+
createdBy: string;
|
|
27
|
+
createdAt: string;
|
|
28
|
+
updatedAt: string;
|
|
29
|
+
closed: { at: string; by: string; reason: CloseReason; summary?: string; unverified?: string } | null;
|
|
30
|
+
log: TaskLogEntry[];
|
|
31
|
+
compacted?: { at: string; by: string; chars: number };
|
|
32
|
+
/** Where an imported task came from. */
|
|
33
|
+
external?: { system: string; id: string };
|
|
34
|
+
/** The formula step a task was poured from. */
|
|
35
|
+
step?: { formula: string; id: string; human?: boolean };
|
|
36
|
+
/** The formula an epic was poured from. */
|
|
37
|
+
formula?: { name: string; version?: number; vars: Record<string, string> };
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type GateKind = 'human' | 'timer' | 'gh:pr' | 'gh:run';
|
|
41
|
+
export type TaskGate = {
|
|
42
|
+
id: string;
|
|
43
|
+
kind: GateKind;
|
|
44
|
+
note?: string;
|
|
45
|
+
until?: string;
|
|
46
|
+
repo?: string;
|
|
47
|
+
ref?: string;
|
|
48
|
+
at: string;
|
|
49
|
+
by: string;
|
|
50
|
+
resolved?: { at: string; by: string; note?: string };
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export type TaskInput = {
|
|
54
|
+
title?: string;
|
|
55
|
+
description?: string;
|
|
56
|
+
type?: TaskType;
|
|
57
|
+
priority?: number | string;
|
|
58
|
+
labels?: string[];
|
|
59
|
+
paths?: string[];
|
|
60
|
+
parent?: string | null;
|
|
61
|
+
blockedBy?: string[];
|
|
62
|
+
related?: string[];
|
|
63
|
+
discoveredFrom?: string | null;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export type TaskLinks = { blockedBy?: string[]; related?: string[]; parent?: string };
|
|
67
|
+
export type TaskSummary = Pick<Task, 'id' | 'title' | 'type' | 'priority' | 'status'> & { labels?: string[]; parent?: string; blockedBy?: string[]; gates?: string[]; assignee?: string; verified?: 'passed' | 'failed' | 'skipped' };
|
|
68
|
+
type WriteOptions = { ifRev?: number };
|
|
69
|
+
|
|
70
|
+
export const TASKS_DIR: string;
|
|
71
|
+
export const TASK_TYPES: TaskType[];
|
|
72
|
+
export const TASK_STATUSES: TaskStatus[];
|
|
73
|
+
export const CLOSE_REASONS: CloseReason[];
|
|
74
|
+
export const GATE_KINDS: GateKind[];
|
|
75
|
+
export const MAX_GATES: number;
|
|
76
|
+
export const TITLE_CHARS: number;
|
|
77
|
+
export const DESCRIPTION_CHARS: number;
|
|
78
|
+
export const NOTE_CHARS: number;
|
|
79
|
+
export const MAX_LABELS: number;
|
|
80
|
+
export const MAX_PATHS: number;
|
|
81
|
+
export const MAX_LINKS: number;
|
|
82
|
+
|
|
83
|
+
export function isTaskId(id: unknown): id is string;
|
|
84
|
+
export function tasksDir(projectRoot: string): string;
|
|
85
|
+
export function loadTasks(projectRoot: string): Promise<{ tasks: Task[]; errors: Array<{ file: string; error: string }> }>;
|
|
86
|
+
export function getTask(projectRoot: string, id: string): Promise<Task>;
|
|
87
|
+
export function blockersOf(task: Task, tasks: Task[]): string[];
|
|
88
|
+
export function readyTasks(tasks: Task[]): Task[];
|
|
89
|
+
export function childrenOf(id: string, tasks: Task[]): Task[];
|
|
90
|
+
export function summarize(task: Task, tasks: Task[]): TaskSummary;
|
|
91
|
+
export function listTasks(tasks: Task[], filter?: { status?: TaskStatus; label?: string; parent?: string; all?: boolean }): Task[];
|
|
92
|
+
export function createTask(projectRoot: string, input: TaskInput & { title: string }, by: string): Promise<Task>;
|
|
93
|
+
export function updateTask(projectRoot: string, id: string, input: TaskInput & { note?: string }, by: string, options?: WriteOptions): Promise<Task>;
|
|
94
|
+
export function linkTask(projectRoot: string, id: string, change: { add?: TaskLinks; remove?: TaskLinks }, by: string, options?: WriteOptions): Promise<Task>;
|
|
95
|
+
export function startTask(projectRoot: string, id: string, assignee: { agent: string; session?: string; assignment?: string; pid?: number; pidStartedAt?: number; epoch?: string }, by: string, options?: WriteOptions): Promise<Task>;
|
|
96
|
+
export function closeTask(projectRoot: string, id: string, close: { reason?: CloseReason; summary?: string; unverified?: string }, by: string, options?: WriteOptions): Promise<Task>;
|
|
97
|
+
export function verificationFailed(task: Task): boolean;
|
|
98
|
+
export function recordVerification(projectRoot: string, id: string, verification: { passed: boolean; where?: string; commands?: Array<{ step: string; command: string; exitCode: number; ms: number; output?: string; timedOut?: boolean }>; skipped?: string }, by: string): Promise<Task>;
|
|
99
|
+
export function reopenTask(projectRoot: string, id: string, reopen: { note?: string }, by: string, options?: WriteOptions): Promise<Task>;
|
|
100
|
+
export function submitTask(projectRoot: string, id: string, submit: { assignment: string; handoff: { outcome: string; summary: string } & Record<string, unknown>; agent: string }, by: string): Promise<Task>;
|
|
101
|
+
export function releaseOrphans(projectRoot: string, isOrphan: (assignee: { agent: string; assignment: string; pid?: number; pidStartedAt?: number; epoch?: string }) => boolean, describe: (assignee: { agent: string; assignment: string }) => Promise<string | undefined>, by: string): Promise<Task[]>;
|
|
102
|
+
export function orphanedEntry(task: Task): { at: string; by: string; event: 'orphaned'; agent: string; assignment: string; note?: string } | undefined;
|
|
103
|
+
export function retakeTask(projectRoot: string, id: string, assignee: { assignment: string; pid?: number; pidStartedAt?: number; epoch?: string }, by: string): Promise<Task>;
|
|
104
|
+
export function releaseTask(projectRoot: string, id: string, release: { assignment: string; handoff?: ({ outcome: string; summary: string } & Record<string, unknown>) | null; agent: string; reason: string }, by: string): Promise<Task>;
|
|
105
|
+
export function taskDigest(tasks: Task[], errors?: Array<{ file: string; error: string }>): string;
|
|
106
|
+
export function startRefusal(task: Task, tasks: Task[]): string | undefined;
|
|
107
|
+
export function gateOpen(gate: TaskGate, now?: number): boolean;
|
|
108
|
+
export function describeGate(gate: TaskGate): string;
|
|
109
|
+
export function gatesOf(task: Task, tasks: Task[]): string[];
|
|
110
|
+
export function addGate(projectRoot: string, id: string, gate: { kind: GateKind; note?: string; until?: string; ref?: string | number }, by: string): Promise<Task>;
|
|
111
|
+
export function resolveGate(projectRoot: string, id: string, gateId: string, resolve: { by: string; note?: string; remove?: boolean; ifRev?: number }): Promise<Task>;
|
|
112
|
+
export function checkGates(projectRoot: string, gh: (args: string[], options: { cwd: string }) => Promise<string>): Promise<{ cleared: Array<{ task: string; gate: string; detail: string }>; pending: Array<{ task: string; gate: string; detail: string }>; errors: Array<{ task: string; gate: string; error: string }> }>;
|
|
113
|
+
export function compactTasks(projectRoot: string, options: { days?: number; dryRun?: boolean }, by: string): Promise<Array<{ id: string; before: number; after: number }>>;
|
|
114
|
+
export type TaskBatch = {
|
|
115
|
+
tasks: Task[];
|
|
116
|
+
errors: Array<{ file: string; error: string }>;
|
|
117
|
+
add(input: TaskInput & { title: string }, by: string, options?: { id?: string; createdAt?: string; event?: string; details?: Record<string, unknown>; extra?: Partial<Task> & Record<string, unknown> }): Task;
|
|
118
|
+
touch(task: Task, by?: string, event?: string, details?: Record<string, unknown>): void;
|
|
119
|
+
link(task: Task, kind: 'blockedBy' | 'parent' | 'discoveredFrom' | 'related', other: string): string | undefined;
|
|
120
|
+
};
|
|
121
|
+
export function batch<T>(projectRoot: string, work: (batch: TaskBatch) => T | Promise<T>, options?: { dryRun?: boolean }): Promise<T>;
|
|
122
|
+
|
|
123
|
+
export type TaskVerification = { at: string; by: string; passed: boolean; where?: string; commands: Array<{ step: string; command: string; exitCode: number; ms: number; output?: string; timedOut?: boolean }>; skipped?: string };
|
|
124
|
+
|
|
125
|
+
export type EpicReport = { id: string; title: string; status: TaskStatus; tasks: number; closed: number; durationMs: number; reworked: number; verification: { passed: number; failed: number; skipped: number; none: number }; unverified: string[]; lines: string[]; text: string };
|
|
126
|
+
export function epicReport(id: string, tasks: Task[]): EpicReport;
|
|
127
|
+
export function landedParents(tasks: Task[]): Array<{ task: Task; children: number }>;
|
|
128
|
+
export function withTaskLock<T>(projectRoot: string, work: () => Promise<T>): Promise<T>;
|