@anhlp/alp-core 0.0.0-stage → 0.10.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/README.md +6 -2
- package/dist/adapter.d.ts +20 -0
- package/dist/adapter.js +30 -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 +66 -0
- package/dist/hooks.d.ts +35 -0
- package/dist/hooks.js +30 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +53 -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 +305 -0
- package/dist/library.d.ts +28 -0
- package/dist/library.js +106 -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 +226 -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 +78 -0
- package/dist/providers.js +83 -0
- package/dist/resolver.d.ts +41 -0
- package/dist/resolver.js +240 -0
- package/dist/task-rows.d.ts +46 -0
- package/dist/task-rows.js +44 -0
- package/dist/tasks.d.ts +127 -0
- package/dist/tasks.js +820 -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 +46 -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 +71 -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 +135 -0
- package/dist/workflow.d.ts +14 -0
- package/dist/workflow.js +24 -0
- package/package.json +260 -4
- package/src/adapter.d.ts +20 -0
- package/src/adapter.js +29 -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 +26 -0
- package/src/init.js +92 -0
- package/src/library-edit.js +333 -0
- package/src/library.js +115 -0
- package/src/models.js +45 -0
- package/src/permissions.d.ts +14 -0
- package/src/permissions.js +239 -0
- package/src/profiles.js +139 -0
- package/src/promptsafe.d.ts +1 -0
- package/src/promptsafe.js +19 -0
- package/src/providers.js +94 -0
- package/src/resolver.js +238 -0
- package/src/task-rows.js +44 -0
- package/src/tasks.d.ts +127 -0
- package/src/tasks.js +862 -0
- package/src/teams.js +170 -0
- package/src/template-path.js +8 -0
- package/src/trust.js +58 -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 +75 -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 +130 -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/README.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @anhlp/alp-core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Filesystem configuration, agent resolution, teams, tasks, permissions and shipped templates.
|
|
4
|
+
|
|
5
|
+
Build: `npm run build --workspace=@anhlp/alp-core`. Test: `npm run test --workspace=@anhlp/alp-core`.
|
|
6
|
+
|
|
7
|
+
Public imports use the package root and its explicitly exported subpaths. See [architecture, provider extension, compatibility and release instructions](../../docs/modular-monorepo.md).
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ResolvedAgent } from './types.js';
|
|
2
|
+
export type CapabilitySupport = 'native' | 'emulated' | 'unsupported';
|
|
3
|
+
export interface AdapterCapabilities {
|
|
4
|
+
instructions: CapabilitySupport;
|
|
5
|
+
skills: CapabilitySupport;
|
|
6
|
+
hooks: CapabilitySupport;
|
|
7
|
+
mcp: CapabilitySupport;
|
|
8
|
+
}
|
|
9
|
+
export interface CompiledAgent<T = unknown> {
|
|
10
|
+
adapterId: string;
|
|
11
|
+
agentName: string;
|
|
12
|
+
projectRoot: string;
|
|
13
|
+
material: T;
|
|
14
|
+
}
|
|
15
|
+
export interface AlpRuntimeAdapter<T = unknown> {
|
|
16
|
+
id: string;
|
|
17
|
+
capabilities(): AdapterCapabilities;
|
|
18
|
+
compile(agent: ResolvedAgent): Promise<CompiledAgent<T>>;
|
|
19
|
+
}
|
|
20
|
+
export function compileAgent<T>(adapter: AlpRuntimeAdapter<T>, agent: ResolvedAgent): Promise<CompiledAgent<T>>;
|
package/dist/adapter.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// packages/core/src/adapter.js
|
|
2
|
+
import { AlpError } from "./errors.js";
|
|
3
|
+
import { validateResolvedAgent } from "./validation.js";
|
|
4
|
+
async function compileAgent(adapter, agent) {
|
|
5
|
+
if (!adapter || typeof adapter.id !== "string" || !adapter.id.trim() || typeof adapter.capabilities !== "function" || typeof adapter.compile !== "function") {
|
|
6
|
+
throw new AlpError("INVALID_ADAPTER", "Adapter requires id, capabilities(), and compile()");
|
|
7
|
+
}
|
|
8
|
+
validateResolvedAgent(agent);
|
|
9
|
+
const capabilities = adapter.capabilities();
|
|
10
|
+
for (const key of ["instructions", "skills", "hooks", "mcp"]) {
|
|
11
|
+
if (!["native", "emulated", "unsupported"].includes(capabilities?.[key])) throw new AlpError("INVALID_CAPABILITIES", `${adapter.id}: invalid ${key} capability`);
|
|
12
|
+
}
|
|
13
|
+
const required = {
|
|
14
|
+
instructions: Boolean(agent.instructions.project || agent.instructions.agent),
|
|
15
|
+
skills: agent.skills.length > 0,
|
|
16
|
+
hooks: agent.hooks.length > 0,
|
|
17
|
+
mcp: Object.keys(agent.mcp.mcpServers).length > 0
|
|
18
|
+
};
|
|
19
|
+
for (const [key, present] of Object.entries(required)) {
|
|
20
|
+
if (present && capabilities[key] === "unsupported") throw new AlpError("UNSUPPORTED_CAPABILITY", `${adapter.id} does not support ${key} required by '${agent.name}'`);
|
|
21
|
+
}
|
|
22
|
+
const compiled = await adapter.compile(structuredClone(agent));
|
|
23
|
+
if (!compiled || compiled.adapterId !== adapter.id || compiled.agentName !== agent.name || compiled.projectRoot !== agent.projectRoot || !Object.hasOwn(compiled, "material")) {
|
|
24
|
+
throw new AlpError("INVALID_COMPILED_AGENT", `${adapter.id}: invalid compiled agent envelope`);
|
|
25
|
+
}
|
|
26
|
+
return compiled;
|
|
27
|
+
}
|
|
28
|
+
export {
|
|
29
|
+
compileAgent
|
|
30
|
+
};
|
package/dist/beads.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** Tasks as beads issues, sorted by id like bd export. */
|
|
2
|
+
export function exportBeads(tasks: any): {
|
|
3
|
+
metadata?: {
|
|
4
|
+
alp: {
|
|
5
|
+
formula?: any;
|
|
6
|
+
step?: any;
|
|
7
|
+
compacted?: any;
|
|
8
|
+
closed?: any;
|
|
9
|
+
handoff?: any;
|
|
10
|
+
gates?: any;
|
|
11
|
+
paths?: any;
|
|
12
|
+
status?: string | undefined;
|
|
13
|
+
};
|
|
14
|
+
} | undefined;
|
|
15
|
+
external_ref?: any;
|
|
16
|
+
source_system?: any;
|
|
17
|
+
defer_until?: any;
|
|
18
|
+
closed_at?: any;
|
|
19
|
+
close_reason?: any;
|
|
20
|
+
created_at: any;
|
|
21
|
+
created_by: any;
|
|
22
|
+
updated_at: any;
|
|
23
|
+
dependencies?: {
|
|
24
|
+
issue_id: any;
|
|
25
|
+
depends_on_id: any;
|
|
26
|
+
type: any;
|
|
27
|
+
}[] | undefined;
|
|
28
|
+
labels?: any;
|
|
29
|
+
assignee?: any;
|
|
30
|
+
status: any;
|
|
31
|
+
priority: any;
|
|
32
|
+
issue_type: any;
|
|
33
|
+
description?: any;
|
|
34
|
+
id: any;
|
|
35
|
+
title: any;
|
|
36
|
+
}[];
|
|
37
|
+
/** Parses JSONL, keeping the line number of every record or error. */
|
|
38
|
+
export function parseJsonl(text: any): {
|
|
39
|
+
records: any[];
|
|
40
|
+
errors: any[];
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Upserts beads issues into the project's tasks, as bd import does: an issue
|
|
44
|
+
* whose id is an ALP task id, or that ALP imported before (external beads id),
|
|
45
|
+
* updates that task; any other becomes a new task. Missing records are never deleted.
|
|
46
|
+
* @returns {Promise<{ created: Array<{ id: string, from: string }>, updated: Array<{ id: string, from: string }>, skipped: Array<{ line: number, reason: string }>, warnings: string[] }>}
|
|
47
|
+
*/
|
|
48
|
+
export function importBeads(projectRoot: any, records: any, { dryRun, by }?: {
|
|
49
|
+
dryRun?: boolean | undefined;
|
|
50
|
+
by?: string | undefined;
|
|
51
|
+
}): Promise<{
|
|
52
|
+
created: Array<{
|
|
53
|
+
id: string;
|
|
54
|
+
from: string;
|
|
55
|
+
}>;
|
|
56
|
+
updated: Array<{
|
|
57
|
+
id: string;
|
|
58
|
+
from: string;
|
|
59
|
+
}>;
|
|
60
|
+
skipped: Array<{
|
|
61
|
+
line: number;
|
|
62
|
+
reason: string;
|
|
63
|
+
}>;
|
|
64
|
+
warnings: string[];
|
|
65
|
+
}>;
|
package/dist/beads.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// packages/core/src/beads.js
|
|
2
|
+
import { DESCRIPTION_CHARS, MAX_LABELS, TASK_TYPES, batch, isTaskId } from "./tasks.js";
|
|
3
|
+
var DEPENDENCY = { blockedBy: "blocks", parent: "parent-child", discoveredFrom: "discovered-from", related: "related" };
|
|
4
|
+
var RELATION = Object.fromEntries(Object.entries(DEPENDENCY).map(([relation, type]) => [type, relation]));
|
|
5
|
+
var LABEL = /^[\w.:/-]{1,50}$/;
|
|
6
|
+
function exportBeads(tasks) {
|
|
7
|
+
return [...tasks].sort((a, b) => a.id.localeCompare(b.id)).map((task) => {
|
|
8
|
+
const dependencies = [
|
|
9
|
+
...task.blockedBy.map((id) => [id, "blocks"]),
|
|
10
|
+
...task.parent ? [[task.parent, "parent-child"]] : [],
|
|
11
|
+
...task.discoveredFrom ? [[task.discoveredFrom, "discovered-from"]] : [],
|
|
12
|
+
...task.related.map((id) => [id, "related"])
|
|
13
|
+
].map(([dependsOn, type]) => ({ issue_id: task.id, depends_on_id: dependsOn, type }));
|
|
14
|
+
const timers = task.gates.filter((gate) => gate.kind === "timer" && !gate.resolved).map((gate) => gate.until).sort();
|
|
15
|
+
const alp = {
|
|
16
|
+
...task.status === "review" ? { status: "review" } : {},
|
|
17
|
+
...task.paths.length ? { paths: task.paths } : {},
|
|
18
|
+
...task.gates.length ? { gates: task.gates } : {},
|
|
19
|
+
...task.handoff ? { handoff: task.handoff } : {},
|
|
20
|
+
...task.closed ? { closed: task.closed } : {},
|
|
21
|
+
...task.compacted ? { compacted: task.compacted } : {},
|
|
22
|
+
...task.step ? { step: task.step } : {},
|
|
23
|
+
...task.formula ? { formula: task.formula } : {}
|
|
24
|
+
};
|
|
25
|
+
return {
|
|
26
|
+
id: task.id,
|
|
27
|
+
title: task.title,
|
|
28
|
+
...task.description ? { description: task.description } : {},
|
|
29
|
+
status: task.status === "review" ? "in_progress" : task.status,
|
|
30
|
+
priority: task.priority,
|
|
31
|
+
issue_type: task.type,
|
|
32
|
+
...task.assignee ? { assignee: task.assignee.agent } : {},
|
|
33
|
+
...task.labels.length ? { labels: task.labels } : {},
|
|
34
|
+
...dependencies.length ? { dependencies } : {},
|
|
35
|
+
created_at: task.createdAt,
|
|
36
|
+
created_by: task.createdBy,
|
|
37
|
+
updated_at: task.updatedAt,
|
|
38
|
+
...task.closed ? { closed_at: task.closed.at, close_reason: task.closed.summary ?? task.closed.reason } : {},
|
|
39
|
+
...timers.length ? { defer_until: timers.at(-1) } : {},
|
|
40
|
+
...task.external ? { external_ref: task.external.id, source_system: task.external.system } : {},
|
|
41
|
+
...Object.keys(alp).length ? { metadata: { alp } } : {}
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function parseJsonl(text) {
|
|
46
|
+
const records = [];
|
|
47
|
+
const errors = [];
|
|
48
|
+
text.split("\n").forEach((line, index) => {
|
|
49
|
+
if (!line.trim()) return;
|
|
50
|
+
try {
|
|
51
|
+
const record = JSON.parse(line);
|
|
52
|
+
if (!record || typeof record !== "object" || Array.isArray(record)) throw new Error("not a JSON object");
|
|
53
|
+
records.push({ line: index + 1, record });
|
|
54
|
+
} catch (error) {
|
|
55
|
+
errors.push({ line: index + 1, reason: error.message });
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
return { records, errors };
|
|
59
|
+
}
|
|
60
|
+
var sections = (record) => [
|
|
61
|
+
typeof record.description === "string" ? record.description : "",
|
|
62
|
+
...[["design", "Design"], ["acceptance_criteria", "Acceptance criteria"], ["notes", "Notes"]].filter(([key]) => typeof record[key] === "string" && record[key].trim()).map(([key, title]) => `## ${title}
|
|
63
|
+
|
|
64
|
+
${record[key].trim()}`)
|
|
65
|
+
].filter(Boolean).join("\n\n");
|
|
66
|
+
function alpOf(record) {
|
|
67
|
+
let metadata = record.metadata;
|
|
68
|
+
if (typeof metadata === "string") {
|
|
69
|
+
try {
|
|
70
|
+
metadata = JSON.parse(metadata);
|
|
71
|
+
} catch {
|
|
72
|
+
return {};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return metadata?.alp && typeof metadata.alp === "object" && !Array.isArray(metadata.alp) ? metadata.alp : {};
|
|
76
|
+
}
|
|
77
|
+
var clip = (text, limit) => text.length > limit ? `${text.slice(0, limit - 1)}\u2026` : text;
|
|
78
|
+
function taskFields(record) {
|
|
79
|
+
const type = TASK_TYPES.includes(record.issue_type) ? record.issue_type : "task";
|
|
80
|
+
const labels = [
|
|
81
|
+
...Array.isArray(record.labels) ? record.labels.filter((label) => typeof label === "string" && LABEL.test(label)) : [],
|
|
82
|
+
...record.issue_type && type !== record.issue_type && LABEL.test(`beads:${record.issue_type}`) ? [`beads:${record.issue_type}`] : []
|
|
83
|
+
];
|
|
84
|
+
const priority = Number.isInteger(record.priority) ? Math.min(4, Math.max(0, record.priority)) : 2;
|
|
85
|
+
const alp = alpOf(record);
|
|
86
|
+
return {
|
|
87
|
+
title: clip(String(record.title).trim(), 200),
|
|
88
|
+
description: clip(sections(record), DESCRIPTION_CHARS),
|
|
89
|
+
type,
|
|
90
|
+
priority,
|
|
91
|
+
labels: [...new Set(labels)].slice(0, MAX_LABELS),
|
|
92
|
+
...Array.isArray(alp.paths) ? { paths: alp.paths.filter((entry) => typeof entry === "string") } : {}
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function byParent(entries, parentOf) {
|
|
96
|
+
const ordered = [];
|
|
97
|
+
const placed = /* @__PURE__ */ new Set();
|
|
98
|
+
const visit = (entry, trail = /* @__PURE__ */ new Set()) => {
|
|
99
|
+
if (placed.has(entry.record.id) || trail.has(entry.record.id)) return;
|
|
100
|
+
trail.add(entry.record.id);
|
|
101
|
+
const parent = entries.find((candidate) => candidate.record.id === parentOf(entry.record));
|
|
102
|
+
if (parent) visit(parent, trail);
|
|
103
|
+
placed.add(entry.record.id);
|
|
104
|
+
ordered.push(entry);
|
|
105
|
+
};
|
|
106
|
+
for (const entry of entries) visit(entry);
|
|
107
|
+
return ordered;
|
|
108
|
+
}
|
|
109
|
+
async function importBeads(projectRoot, records, { dryRun = false, by = "user" } = {}) {
|
|
110
|
+
const report = { created: [], updated: [], skipped: [], warnings: [] };
|
|
111
|
+
const usable = [];
|
|
112
|
+
for (const { line, record } of records) {
|
|
113
|
+
if (record._type !== void 0 && record._type !== "issue") report.skipped.push({ line, reason: `${record._type} record, not an issue` });
|
|
114
|
+
else if (typeof record.title !== "string" || !record.title.trim()) report.skipped.push({ line, reason: "no title" });
|
|
115
|
+
else if (typeof record.id !== "string" || !record.id.trim()) report.skipped.push({ line, reason: "no id" });
|
|
116
|
+
else if (record.status === "tombstone") report.skipped.push({ line, reason: `${record.id} is deleted (tombstone)` });
|
|
117
|
+
else if (record.ephemeral === true || record.wisp === true) report.skipped.push({ line, reason: `${record.id} is ephemeral` });
|
|
118
|
+
else usable.push({ line, record });
|
|
119
|
+
}
|
|
120
|
+
const dependencies = (record) => (Array.isArray(record.dependencies) ? record.dependencies : []).filter((dep) => dep && typeof dep.depends_on_id === "string" && (dep.issue_id === void 0 || dep.issue_id === record.id));
|
|
121
|
+
const parentOf = (record) => dependencies(record).find((dep) => dep.type === "parent-child")?.depends_on_id ?? (typeof record.parent === "string" ? record.parent : void 0);
|
|
122
|
+
return batch(projectRoot, ({ tasks, add, touch, link }) => {
|
|
123
|
+
const ids = /* @__PURE__ */ new Map();
|
|
124
|
+
const find = (foreign) => ids.get(foreign) ?? tasks.find((task) => task.id === foreign && isTaskId(foreign))?.id ?? tasks.find((task) => task.external?.system === "beads" && task.external.id === foreign)?.id;
|
|
125
|
+
for (const { line, record } of byParent(usable, parentOf)) {
|
|
126
|
+
try {
|
|
127
|
+
const values = taskFields(record);
|
|
128
|
+
const alp = alpOf(record);
|
|
129
|
+
const existing = find(record.id);
|
|
130
|
+
let task;
|
|
131
|
+
let before;
|
|
132
|
+
if (existing) {
|
|
133
|
+
task = tasks.find((entry) => entry.id === existing);
|
|
134
|
+
before = JSON.stringify(task);
|
|
135
|
+
Object.assign(task, values);
|
|
136
|
+
} else {
|
|
137
|
+
const parent = parentOf(record) ? find(parentOf(record)) : void 0;
|
|
138
|
+
if (parentOf(record) && !parent) report.warnings.push(`${record.id}: parent ${parentOf(record)} is not in the file or the project`);
|
|
139
|
+
task = add({ ...values, ...parent ? { parent } : {} }, by, {
|
|
140
|
+
id: record.id,
|
|
141
|
+
createdAt: typeof record.created_at === "string" ? record.created_at : void 0,
|
|
142
|
+
event: "imported",
|
|
143
|
+
details: { from: record.id },
|
|
144
|
+
extra: {
|
|
145
|
+
...typeof record.created_by === "string" && record.created_by ? { createdBy: record.created_by } : {},
|
|
146
|
+
...Array.isArray(alp.gates) ? { gates: alp.gates } : {},
|
|
147
|
+
...alp.step ? { step: alp.step } : {},
|
|
148
|
+
...alp.formula ? { formula: alp.formula } : {}
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
if (task.id !== record.id) task.external = { system: "beads", id: record.id };
|
|
152
|
+
report.created.push({ id: task.id, from: record.id });
|
|
153
|
+
}
|
|
154
|
+
ids.set(record.id, task.id);
|
|
155
|
+
applyStatus(task, record, alp, by, report);
|
|
156
|
+
const deferred = typeof record.defer_until === "string" && Date.parse(record.defer_until) > Date.now() ? new Date(record.defer_until).toISOString() : void 0;
|
|
157
|
+
if (deferred && !task.gates.some((gate) => gate.kind === "timer" && Math.abs(Date.parse(gate.until) - Date.parse(deferred)) < 1e3)) {
|
|
158
|
+
const next = Math.max(0, ...task.gates.map((gate) => Number(gate.id.slice(1)) || 0)) + 1;
|
|
159
|
+
task.gates = [...task.gates, { id: `g${next}`, kind: "timer", until: deferred, at: (/* @__PURE__ */ new Date()).toISOString(), by }];
|
|
160
|
+
}
|
|
161
|
+
if (existing && JSON.stringify(task) !== before) {
|
|
162
|
+
touch(task, by, "imported", { from: record.id });
|
|
163
|
+
report.updated.push({ id: task.id, from: record.id });
|
|
164
|
+
}
|
|
165
|
+
} catch (error) {
|
|
166
|
+
report.skipped.push({ line, reason: `${record.id}: ${error.message}` });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
for (const { record } of usable) {
|
|
170
|
+
const id = ids.get(record.id);
|
|
171
|
+
if (!id) continue;
|
|
172
|
+
const task = tasks.find((entry) => entry.id === id);
|
|
173
|
+
for (const dep of dependencies(record)) {
|
|
174
|
+
const relation = RELATION[dep.type];
|
|
175
|
+
if (!relation) {
|
|
176
|
+
report.warnings.push(`${record.id}: dependency type ${dep.type} has no ALP relation; skipped`);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const other = find(dep.depends_on_id);
|
|
180
|
+
if (!other) {
|
|
181
|
+
report.warnings.push(`${record.id}: ${dep.type} ${dep.depends_on_id} is not in the file or the project; skipped`);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const before = JSON.stringify(task);
|
|
185
|
+
const refused = link(task, relation, other);
|
|
186
|
+
if (refused) report.warnings.push(`${record.id}: ${dep.type} ${dep.depends_on_id} skipped: ${refused}`);
|
|
187
|
+
else if (JSON.stringify(task) !== before) touch(task);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return report;
|
|
191
|
+
}, { dryRun });
|
|
192
|
+
}
|
|
193
|
+
function applyStatus(task, record, alp, by, report) {
|
|
194
|
+
const status = record.status ?? "open";
|
|
195
|
+
if (status === "closed") {
|
|
196
|
+
if (task.status === "closed") return;
|
|
197
|
+
task.status = "closed";
|
|
198
|
+
task.assignee = null;
|
|
199
|
+
task.closed = alp.closed && typeof alp.closed === "object" ? alp.closed : { at: typeof record.closed_at === "string" ? record.closed_at : (/* @__PURE__ */ new Date()).toISOString(), by: "beads", reason: "done", ...typeof record.close_reason === "string" && record.close_reason ? { summary: record.close_reason } : {} };
|
|
200
|
+
} else if (alp.status === "review" && alp.handoff) {
|
|
201
|
+
task.status = "review";
|
|
202
|
+
task.handoff = alp.handoff;
|
|
203
|
+
} else if (status === "in_progress" && task.status === "open") {
|
|
204
|
+
report.warnings.push(`${record.id}: was in progress${record.assignee ? ` with ${record.assignee}` : ""} in beads; imported as open`);
|
|
205
|
+
} else if (!["open", "in_progress", "blocked", "deferred"].includes(status)) {
|
|
206
|
+
report.warnings.push(`${record.id}: beads status ${status} imported as open`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
export {
|
|
210
|
+
exportBeads,
|
|
211
|
+
importBeads,
|
|
212
|
+
parseJsonl
|
|
213
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// packages/core/src/delegation.js
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { jsonObject } from "./resolver.js";
|
|
4
|
+
import { AlpError } from "./errors.js";
|
|
5
|
+
var name = (value) => typeof value === "string" && value.trim() && ![".", ".."].includes(value) && !/[\\/\x00]/.test(value);
|
|
6
|
+
async function resolveDelegation(projectRoot) {
|
|
7
|
+
const source = path.join(projectRoot, ".alp", "settings.json");
|
|
8
|
+
const settings = await jsonObject(source, "INVALID_SETTINGS");
|
|
9
|
+
const graph = settings.delegation === void 0 ? {} : settings.delegation;
|
|
10
|
+
const invalid = (message) => {
|
|
11
|
+
throw new AlpError("INVALID_DELEGATION", `${source}: ${message}`);
|
|
12
|
+
};
|
|
13
|
+
if (!graph || typeof graph !== "object" || Array.isArray(graph)) invalid("delegation must map agent names to target arrays");
|
|
14
|
+
for (const [owner, targets] of Object.entries(graph)) {
|
|
15
|
+
if (!name(owner) || !Array.isArray(targets) || !targets.every(name) || new Set(targets).size !== targets.length) invalid(`invalid targets for '${owner}'`);
|
|
16
|
+
}
|
|
17
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
18
|
+
const visited = /* @__PURE__ */ new Set();
|
|
19
|
+
function visit(owner) {
|
|
20
|
+
if (visiting.has(owner)) invalid(`delegation cycle at '${owner}'`);
|
|
21
|
+
if (visited.has(owner)) return;
|
|
22
|
+
visiting.add(owner);
|
|
23
|
+
for (const target of Object.hasOwn(graph, owner) ? graph[owner] : []) visit(target);
|
|
24
|
+
visiting.delete(owner);
|
|
25
|
+
visited.add(owner);
|
|
26
|
+
}
|
|
27
|
+
for (const owner of Object.keys(graph)) visit(owner);
|
|
28
|
+
return graph;
|
|
29
|
+
}
|
|
30
|
+
export {
|
|
31
|
+
resolveDelegation
|
|
32
|
+
};
|
package/dist/errors.d.ts
ADDED
package/dist/errors.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Task } from './tasks.js';
|
|
2
|
+
|
|
3
|
+
export type FormulaStep = { id: string; title: string; description?: string; type: string; needs: string[]; priority?: number; labels?: string[]; paths?: string[] };
|
|
4
|
+
export type Formula = {
|
|
5
|
+
formula: string;
|
|
6
|
+
title?: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
version?: number;
|
|
9
|
+
priority?: number;
|
|
10
|
+
vars: Record<string, { description?: string; required: boolean; default?: string }>;
|
|
11
|
+
steps: FormulaStep[];
|
|
12
|
+
};
|
|
13
|
+
/** Parses TOML text; the core has none of its own (e.g. smol-toml's parse). */
|
|
14
|
+
export type TomlParser = (text: string) => unknown;
|
|
15
|
+
type Parsers = { toml?: TomlParser };
|
|
16
|
+
export type FormulaEntry = { name: string; file: string; formula?: Formula; error?: string };
|
|
17
|
+
|
|
18
|
+
export const MAX_STEPS: number;
|
|
19
|
+
export function formulaDirs(projectRoot: string, home?: string): string[];
|
|
20
|
+
export function listFormulas(projectRoot: string, home?: string, parsers?: Parsers): Promise<FormulaEntry[]>;
|
|
21
|
+
export function findFormula(projectRoot: string, home: string | undefined, name: string, parsers?: Parsers): Promise<FormulaEntry & { formula: Formula }>;
|
|
22
|
+
export function loadFormula(file: string, parsers?: Parsers): Promise<Formula>;
|
|
23
|
+
export function validateFormula(data: unknown): Formula;
|
|
24
|
+
export function resolveVars(formula: Formula, given?: Record<string, string>): Record<string, string>;
|
|
25
|
+
export function pourFormula(projectRoot: string, formula: Formula, given: Record<string, string> | undefined, by: string, options?: { dryRun?: boolean; parent?: string }): Promise<{ epic: Task; tasks: Task[] }>;
|
package/dist/formulas.js
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// packages/core/src/formulas.js
|
|
2
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { AlpError } from "./errors.js";
|
|
5
|
+
import { DESCRIPTION_CHARS, TASK_TYPES, batch } from "./tasks.js";
|
|
6
|
+
var fail = (message) => {
|
|
7
|
+
throw new AlpError("INVALID_FORMULA", message);
|
|
8
|
+
};
|
|
9
|
+
var NAME = /^[\w.-]{1,64}$/;
|
|
10
|
+
var STEP_ID = /^[\w-]{1,64}$/;
|
|
11
|
+
var VAR = /\{\{\s*([\w-]+)\s*\}\}/g;
|
|
12
|
+
var MAX_STEPS = 50;
|
|
13
|
+
function formulaDirs(projectRoot, home) {
|
|
14
|
+
return [
|
|
15
|
+
path.join(path.resolve(projectRoot), ".alp", "formulas"),
|
|
16
|
+
...home ? [path.join(home, "formulas")] : [],
|
|
17
|
+
path.join(path.resolve(projectRoot), ".beads", "formulas")
|
|
18
|
+
];
|
|
19
|
+
}
|
|
20
|
+
var FILE = /^(.+)\.formula\.(toml|json)$/;
|
|
21
|
+
async function listFormulas(projectRoot, home, { toml } = {}) {
|
|
22
|
+
const found = /* @__PURE__ */ new Map();
|
|
23
|
+
for (const dir of formulaDirs(projectRoot, home)) {
|
|
24
|
+
let names;
|
|
25
|
+
try {
|
|
26
|
+
names = await readdir(dir);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (error.code === "ENOENT") continue;
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
for (const name of names.sort()) {
|
|
32
|
+
const match = FILE.exec(name);
|
|
33
|
+
if (!match || found.has(match[1])) continue;
|
|
34
|
+
const file = path.join(dir, name);
|
|
35
|
+
try {
|
|
36
|
+
const formula = await loadFormula(file, { toml });
|
|
37
|
+
found.set(match[1], { name: match[1], file, formula });
|
|
38
|
+
} catch (error) {
|
|
39
|
+
found.set(match[1], { name: match[1], file, error: error.message });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return [...found.values()];
|
|
44
|
+
}
|
|
45
|
+
async function findFormula(projectRoot, home, name, { toml } = {}) {
|
|
46
|
+
if (!NAME.test(name ?? "")) fail("A formula name has letters, digits, dots, dashes and underscores");
|
|
47
|
+
const entry = (await listFormulas(projectRoot, home, { toml })).find((candidate) => candidate.name === name);
|
|
48
|
+
if (!entry) fail(`No formula ${name} in ${formulaDirs(projectRoot, home).join(", ")}`);
|
|
49
|
+
if (entry.error) fail(`${entry.file}: ${entry.error}`);
|
|
50
|
+
return entry;
|
|
51
|
+
}
|
|
52
|
+
async function loadFormula(file, { toml } = {}) {
|
|
53
|
+
const text = await readFile(file, "utf8");
|
|
54
|
+
if (!file.endsWith(".json") && !toml) fail("reading TOML formulas needs a TOML parser");
|
|
55
|
+
let data;
|
|
56
|
+
try {
|
|
57
|
+
data = file.endsWith(".json") ? JSON.parse(text) : toml(text);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
fail(`cannot parse: ${error.message.split("\n")[0]}`);
|
|
60
|
+
}
|
|
61
|
+
return validateFormula(data);
|
|
62
|
+
}
|
|
63
|
+
var optionalText = (value, name, limit) => {
|
|
64
|
+
if (value === void 0) return void 0;
|
|
65
|
+
if (typeof value !== "string" || value.length > limit) fail(`${name} must be text of at most ${limit} characters`);
|
|
66
|
+
return value;
|
|
67
|
+
};
|
|
68
|
+
function validateFormula(data) {
|
|
69
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) fail("A formula is an object");
|
|
70
|
+
if (typeof data.formula !== "string" || !NAME.test(data.formula)) fail("formula must name it: letters, digits, dots, dashes and underscores");
|
|
71
|
+
const vars = {};
|
|
72
|
+
if (data.vars !== void 0) {
|
|
73
|
+
if (!data.vars || typeof data.vars !== "object" || Array.isArray(data.vars)) fail("vars must be a table of variables");
|
|
74
|
+
for (const [name, spec] of Object.entries(data.vars)) {
|
|
75
|
+
if (!/^[\w-]{1,64}$/.test(name)) fail(`Variable ${name} has an invalid name`);
|
|
76
|
+
const value = spec && typeof spec === "object" ? spec : {};
|
|
77
|
+
vars[name] = {
|
|
78
|
+
...typeof value.description === "string" ? { description: value.description } : {},
|
|
79
|
+
required: value.required === true,
|
|
80
|
+
...value.default !== void 0 ? { default: String(value.default) } : {}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (!Array.isArray(data.steps) || !data.steps.length || data.steps.length > MAX_STEPS) fail(`steps must list 1 to ${MAX_STEPS} steps`);
|
|
85
|
+
const ids = /* @__PURE__ */ new Set();
|
|
86
|
+
const steps = data.steps.map((step, index) => {
|
|
87
|
+
if (!step || typeof step !== "object") fail(`Step ${index + 1} is not a table`);
|
|
88
|
+
if (typeof step.id !== "string" || !STEP_ID.test(step.id)) fail(`Step ${index + 1} needs an id of letters, digits, dashes and underscores`);
|
|
89
|
+
if (ids.has(step.id)) fail(`Two steps have id ${step.id}`);
|
|
90
|
+
ids.add(step.id);
|
|
91
|
+
if (typeof step.title !== "string" || !step.title.trim()) fail(`Step ${step.id} needs a title`);
|
|
92
|
+
const needs = step.needs === void 0 ? [] : step.needs;
|
|
93
|
+
if (!Array.isArray(needs) || !needs.every((need) => typeof need === "string")) fail(`Step ${step.id}: needs must list step ids`);
|
|
94
|
+
if (step.priority !== void 0 && (!Number.isInteger(step.priority) || step.priority < 0 || step.priority > 4)) fail(`Step ${step.id}: priority must be 0 to 4`);
|
|
95
|
+
return {
|
|
96
|
+
id: step.id,
|
|
97
|
+
title: step.title,
|
|
98
|
+
...optionalText(step.description, `Step ${step.id} description`, DESCRIPTION_CHARS) ? { description: step.description } : {},
|
|
99
|
+
type: typeof step.type === "string" ? step.type : "task",
|
|
100
|
+
needs: [...new Set(needs)],
|
|
101
|
+
...step.priority !== void 0 ? { priority: step.priority } : {},
|
|
102
|
+
...Array.isArray(step.labels) ? { labels: step.labels.filter((label) => typeof label === "string") } : {},
|
|
103
|
+
...Array.isArray(step.paths) ? { paths: step.paths.filter((entry) => typeof entry === "string") } : {}
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
for (const step of steps) for (const need of step.needs) if (!ids.has(need)) fail(`Step ${step.id} needs unknown step ${need}`);
|
|
107
|
+
const state = /* @__PURE__ */ new Map();
|
|
108
|
+
const visit = (id, trail) => {
|
|
109
|
+
if (state.get(id) === "done") return;
|
|
110
|
+
if (state.get(id) === "open") fail(`Steps form a cycle: ${[...trail, id].join(" \u2192 ")}`);
|
|
111
|
+
state.set(id, "open");
|
|
112
|
+
for (const need of steps.find((step) => step.id === id).needs) visit(need, [...trail, id]);
|
|
113
|
+
state.set(id, "done");
|
|
114
|
+
};
|
|
115
|
+
for (const step of steps) visit(step.id, []);
|
|
116
|
+
return {
|
|
117
|
+
formula: data.formula,
|
|
118
|
+
...optionalText(data.title, "title", 200) ? { title: data.title } : {},
|
|
119
|
+
...optionalText(data.description, "description", DESCRIPTION_CHARS) ? { description: data.description } : {},
|
|
120
|
+
...Number.isInteger(data.version) ? { version: data.version } : {},
|
|
121
|
+
...Number.isInteger(data.priority) && data.priority >= 0 && data.priority <= 4 ? { priority: data.priority } : {},
|
|
122
|
+
vars,
|
|
123
|
+
steps
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function resolveVars(formula, given = {}) {
|
|
127
|
+
const unknown = Object.keys(given).filter((name) => !(name in formula.vars));
|
|
128
|
+
if (unknown.length) fail(`${formula.formula} has no variable ${unknown.join(", ")}; it takes ${Object.keys(formula.vars).join(", ") || "none"}`);
|
|
129
|
+
const values = {};
|
|
130
|
+
for (const [name, spec] of Object.entries(formula.vars)) {
|
|
131
|
+
const value = given[name] ?? spec.default;
|
|
132
|
+
if (value === void 0 && spec.required) fail(`${formula.formula} needs --var ${name}=\u2026${spec.description ? ` (${spec.description})` : ""}`);
|
|
133
|
+
if (value !== void 0) values[name] = String(value);
|
|
134
|
+
}
|
|
135
|
+
return values;
|
|
136
|
+
}
|
|
137
|
+
function fill(text, values, where) {
|
|
138
|
+
return text.replace(VAR, (whole, name) => {
|
|
139
|
+
if (!(name in values)) fail(`${where} uses {{${name}}}, which has no value`);
|
|
140
|
+
return values[name];
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
async function pourFormula(projectRoot, formula, given, by, { dryRun = false, parent } = {}) {
|
|
144
|
+
const values = resolveVars(formula, given);
|
|
145
|
+
const summary = Object.entries(values).map(([name, value]) => `${name}=${value}`).join(", ");
|
|
146
|
+
const title = formula.title ? fill(formula.title, values, "title") : `${formula.formula}${summary ? ` (${summary})` : ""}`;
|
|
147
|
+
const steps = formula.steps.map((step) => ({
|
|
148
|
+
...step,
|
|
149
|
+
title: fill(step.title, values, `Step ${step.id}`),
|
|
150
|
+
...step.description ? { description: fill(step.description, values, `Step ${step.id}`) } : {}
|
|
151
|
+
}));
|
|
152
|
+
return batch(projectRoot, ({ add, link }) => {
|
|
153
|
+
const epic = add({
|
|
154
|
+
title: title.slice(0, 200),
|
|
155
|
+
type: "epic",
|
|
156
|
+
priority: formula.priority ?? 2,
|
|
157
|
+
labels: [`formula:${formula.formula}`.slice(0, 50)],
|
|
158
|
+
...formula.description ? { description: fill(formula.description, values, "description") } : {},
|
|
159
|
+
...parent ? { parent } : {}
|
|
160
|
+
}, by, { event: "poured", details: { formula: formula.formula }, extra: { formula: { name: formula.formula, ...formula.version ? { version: formula.version } : {}, vars: values } } });
|
|
161
|
+
const made = /* @__PURE__ */ new Map();
|
|
162
|
+
for (const step of steps) {
|
|
163
|
+
const human = step.type === "human";
|
|
164
|
+
const type = TASK_TYPES.includes(step.type) && step.type !== "epic" ? step.type : "task";
|
|
165
|
+
const task = add({
|
|
166
|
+
title: step.title.slice(0, 200),
|
|
167
|
+
type,
|
|
168
|
+
priority: step.priority ?? formula.priority ?? 2,
|
|
169
|
+
parent: epic.id,
|
|
170
|
+
...step.description ? { description: step.description } : {},
|
|
171
|
+
...step.labels ? { labels: step.labels } : {},
|
|
172
|
+
...step.paths ? { paths: step.paths } : {}
|
|
173
|
+
}, by, {
|
|
174
|
+
event: "poured",
|
|
175
|
+
details: { formula: formula.formula, step: step.id },
|
|
176
|
+
extra: {
|
|
177
|
+
step: { formula: formula.formula, id: step.id, ...human ? { human: true } : {} },
|
|
178
|
+
...human ? { gates: [{ id: "g1", kind: "human", note: `Your step: ${step.title}`.slice(0, 2e3), at: (/* @__PURE__ */ new Date()).toISOString(), by }] } : {}
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
made.set(step.id, task);
|
|
182
|
+
}
|
|
183
|
+
for (const step of steps) {
|
|
184
|
+
for (const need of step.needs) {
|
|
185
|
+
const refused = link(made.get(step.id), "blockedBy", made.get(need).id);
|
|
186
|
+
if (refused) fail(`Step ${step.id} cannot need ${need}: ${refused}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return { epic, tasks: [...made.values()] };
|
|
190
|
+
}, { dryRun });
|
|
191
|
+
}
|
|
192
|
+
export {
|
|
193
|
+
MAX_STEPS,
|
|
194
|
+
findFormula,
|
|
195
|
+
formulaDirs,
|
|
196
|
+
listFormulas,
|
|
197
|
+
loadFormula,
|
|
198
|
+
pourFormula,
|
|
199
|
+
resolveVars,
|
|
200
|
+
validateFormula
|
|
201
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs a hook's command once (ALPD §43): through /bin/sh in `cwd`, with the event as JSON
|
|
3
|
+
* on stdin and `env` added to ALP's environment. It runs in its own process group, so
|
|
4
|
+
* past its timeout the whole group gets SIGTERM, then SIGKILL two seconds later; a
|
|
5
|
+
* shell's children do not outlive it. Output beyond 64 KiB per stream is cut.
|
|
6
|
+
* @param {{ command: string, timeoutSec?: number }} hook
|
|
7
|
+
* @param {unknown} payload
|
|
8
|
+
* @param {{ cwd: string, env?: Record<string, string>, timeoutMs?: number }} options
|
|
9
|
+
* @returns {Promise<{ exitCode: number | null, signal: string | null, timedOut: boolean, stdout: string, stderr: string, durationMs: number }>}
|
|
10
|
+
*/
|
|
11
|
+
export function runHook(hook: {
|
|
12
|
+
command: string;
|
|
13
|
+
timeoutSec?: number;
|
|
14
|
+
}, payload: unknown, { cwd, env, timeoutMs }?: {
|
|
15
|
+
cwd: string;
|
|
16
|
+
env?: Record<string, string>;
|
|
17
|
+
timeoutMs?: number;
|
|
18
|
+
}): Promise<{
|
|
19
|
+
exitCode: number | null;
|
|
20
|
+
signal: string | null;
|
|
21
|
+
timedOut: boolean;
|
|
22
|
+
stdout: string;
|
|
23
|
+
stderr: string;
|
|
24
|
+
durationMs: number;
|
|
25
|
+
}>;
|
|
26
|
+
/** A payload like the one ALP sends for an event, for trying a hook out. */
|
|
27
|
+
export function samplePayload(event: any, { project, agent, session, task }?: {
|
|
28
|
+
agent?: string | undefined;
|
|
29
|
+
session?: string | undefined;
|
|
30
|
+
}): {
|
|
31
|
+
branch?: string | undefined;
|
|
32
|
+
files?: never[] | undefined;
|
|
33
|
+
task?: any;
|
|
34
|
+
reason?: string | undefined;
|
|
35
|
+
handoff?: {
|
|
36
|
+
outcome: string;
|
|
37
|
+
summary: string;
|
|
38
|
+
} | undefined;
|
|
39
|
+
event: any;
|
|
40
|
+
sample: boolean;
|
|
41
|
+
project: any;
|
|
42
|
+
session: string;
|
|
43
|
+
agent: string;
|
|
44
|
+
};
|