@anhlp/alp-core 0.0.0-beta.1 → 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.
Files changed (57) hide show
  1. package/dist/hook-run.d.ts +1 -1
  2. package/dist/hook-run.js +9 -3
  3. package/dist/index.d.ts +0 -5
  4. package/dist/index.js +0 -10
  5. package/dist/library-edit.js +1 -11
  6. package/dist/library.js +2 -4
  7. package/dist/permissions.js +6 -9
  8. package/dist/providers.d.ts +0 -12
  9. package/dist/providers.js +0 -18
  10. package/dist/task-rows.d.ts +0 -1
  11. package/dist/task-rows.js +0 -1
  12. package/dist/tasks.d.ts +1 -2
  13. package/dist/tasks.js +10 -25
  14. package/dist/trust.js +2 -3
  15. package/dist/user-settings.js +5 -6
  16. package/dist/verify.js +15 -3
  17. package/package.json +6 -53
  18. package/src/hook-run.js +4 -4
  19. package/src/index.js +0 -5
  20. package/src/library-edit.js +1 -14
  21. package/src/library.js +2 -4
  22. package/src/permissions.js +6 -9
  23. package/src/providers.js +0 -28
  24. package/src/task-rows.js +1 -1
  25. package/src/tasks.d.ts +1 -2
  26. package/src/tasks.js +9 -28
  27. package/src/trust.js +2 -3
  28. package/src/user-settings.js +5 -6
  29. package/src/verify.js +6 -3
  30. package/LICENSE +0 -202
  31. package/NOTICE +0 -7
  32. package/dist/atomic.d.ts +0 -2
  33. package/dist/atomic.js +0 -28
  34. package/dist/backup.d.ts +0 -27
  35. package/dist/backup.js +0 -582
  36. package/dist/log.d.ts +0 -37
  37. package/dist/log.js +0 -156
  38. package/dist/migrations.d.ts +0 -9
  39. package/dist/migrations.js +0 -95
  40. package/dist/safe-path.d.ts +0 -2
  41. package/dist/safe-path.js +0 -24
  42. package/dist/shell.d.ts +0 -16
  43. package/dist/shell.js +0 -22
  44. package/dist/state-schema.d.ts +0 -9
  45. package/dist/state-schema.js +0 -54
  46. package/src/atomic.d.ts +0 -2
  47. package/src/atomic.js +0 -28
  48. package/src/backup.d.ts +0 -27
  49. package/src/backup.js +0 -570
  50. package/src/log.d.ts +0 -37
  51. package/src/log.js +0 -177
  52. package/src/migrations.d.ts +0 -9
  53. package/src/migrations.js +0 -111
  54. package/src/safe-path.js +0 -20
  55. package/src/shell.js +0 -28
  56. package/src/state-schema.d.ts +0 -9
  57. package/src/state-schema.js +0 -60
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Runs a hook's command once (ALPD §43): through /bin/sh (cmd.exe on Windows) in `cwd`, with the event as JSON
2
+ * Runs a hook's command once (ALPD §43): through /bin/sh in `cwd`, with the event as JSON
3
3
  * on stdin and `env` added to ALP's environment. It runs in its own process group, so
4
4
  * past its timeout the whole group gets SIGTERM, then SIGKILL two seconds later; a
5
5
  * shell's children do not outlive it. Output beyond 64 KiB per stream is cut.
package/dist/hook-run.js CHANGED
@@ -1,13 +1,19 @@
1
1
  // packages/core/src/hook-run.js
2
- import { signalShell, spawnShell } from "./shell.js";
2
+ import { spawn } from "node:child_process";
3
3
  var OUTPUT_LIMIT = 64 * 1024;
4
4
  var GRACE_MS = 2e3;
5
5
  function runHook(hook, payload, { cwd, env = {}, timeoutMs } = {}) {
6
6
  const limit = timeoutMs ?? (hook.timeoutSec ?? 60) * 1e3;
7
7
  const started = Date.now();
8
8
  return new Promise((resolve, reject) => {
9
- const child = spawnShell(hook.command, { cwd, env: { ...process.env, ...env }, stdio: ["pipe", "pipe", "pipe"] });
10
- const signal = (name) => signalShell(child, name);
9
+ const child = spawn("/bin/sh", ["-c", hook.command], { cwd, env: { ...process.env, ...env }, stdio: ["pipe", "pipe", "pipe"], detached: true });
10
+ const signal = (name) => {
11
+ try {
12
+ process.kill(-child.pid, name);
13
+ } catch {
14
+ child.kill(name);
15
+ }
16
+ };
11
17
  const output = { stdout: "", stderr: "" };
12
18
  for (const stream of ["stdout", "stderr"]) {
13
19
  child[stream].setEncoding("utf8");
package/dist/index.d.ts CHANGED
@@ -1,6 +1,4 @@
1
1
  export * as adapter from "./adapter.js";
2
- export * as atomic from "./atomic.js";
3
- export * as backup from "./backup.js";
4
2
  export * as beads from "./beads.js";
5
3
  export * as delegation from "./delegation.js";
6
4
  export * as errors from "./errors.js";
@@ -10,15 +8,12 @@ export * as hooks from "./hooks.js";
10
8
  export * as init from "./init.js";
11
9
  export * as library_edit from "./library-edit.js";
12
10
  export * as library from "./library.js";
13
- export * as log from "./log.js";
14
- export * as migrations from "./migrations.js";
15
11
  export * as models from "./models.js";
16
12
  export * as permissions from "./permissions.js";
17
13
  export * as profiles from "./profiles.js";
18
14
  export * as promptsafe from "./promptsafe.js";
19
15
  export * as providers from "./providers.js";
20
16
  export * as resolver from "./resolver.js";
21
- export * as state_schema from "./state-schema.js";
22
17
  export * as task_rows from "./task-rows.js";
23
18
  export * as tasks from "./tasks.js";
24
19
  export * as teams from "./teams.js";
package/dist/index.js CHANGED
@@ -1,7 +1,5 @@
1
1
  // packages/core/src/index.js
2
2
  import * as adapter from "./adapter.js";
3
- import * as atomic from "./atomic.js";
4
- import * as backup from "./backup.js";
5
3
  import * as beads from "./beads.js";
6
4
  import * as delegation from "./delegation.js";
7
5
  import * as errors from "./errors.js";
@@ -11,15 +9,12 @@ import * as hooks from "./hooks.js";
11
9
  import * as init from "./init.js";
12
10
  import * as library_edit from "./library-edit.js";
13
11
  import * as library from "./library.js";
14
- import * as log from "./log.js";
15
- import * as migrations from "./migrations.js";
16
12
  import * as models from "./models.js";
17
13
  import * as permissions from "./permissions.js";
18
14
  import * as profiles from "./profiles.js";
19
15
  import * as promptsafe from "./promptsafe.js";
20
16
  import * as providers from "./providers.js";
21
17
  import * as resolver from "./resolver.js";
22
- import * as state_schema from "./state-schema.js";
23
18
  import * as task_rows from "./task-rows.js";
24
19
  import * as tasks from "./tasks.js";
25
20
  import * as teams from "./teams.js";
@@ -31,8 +26,6 @@ import * as verify from "./verify.js";
31
26
  import * as workflow from "./workflow.js";
32
27
  export {
33
28
  adapter,
34
- atomic,
35
- backup,
36
29
  beads,
37
30
  delegation,
38
31
  errors,
@@ -42,15 +35,12 @@ export {
42
35
  init,
43
36
  library,
44
37
  library_edit,
45
- log,
46
- migrations,
47
38
  models,
48
39
  permissions,
49
40
  profiles,
50
41
  promptsafe,
51
42
  providers,
52
43
  resolver,
53
- state_schema,
54
44
  task_rows,
55
45
  tasks,
56
46
  teams,
@@ -2,9 +2,7 @@
2
2
  import { mkdir, rename, rm, stat, writeFile } from "node:fs/promises";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
4
  import path from "node:path";
5
- import { moveFile } from "./atomic.js";
6
5
  import { AlpError } from "./errors.js";
7
- import { assertNoSymlinks } from "./safe-path.js";
8
6
  import { agentSources, builtinText, jsonObject, libraryEntries, optionalText } from "./resolver.js";
9
7
  import { teamSources, validateTeam } from "./teams.js";
10
8
  import { normalizeMcp, validateAgentConfig, validateHook } from "./validation.js";
@@ -39,13 +37,7 @@ function base(scope, { root, library }, kind) {
39
37
  return fail("INVALID_SCOPE", `Scope must be library or project, not '${scope}'`);
40
38
  }
41
39
  var revisionOf = (content) => content === void 0 ? null : createHash("sha256").update(JSON.stringify(content)).digest("hex").slice(0, 16);
42
- async function safeEntryPaths(directory, kind, name) {
43
- for (const [file] of FILES[kind](name)) {
44
- await assertNoSymlinks(directory, path.join(kind, file));
45
- }
46
- }
47
40
  async function readLayer(kind, name, directory) {
48
- await safeEntryPaths(directory, kind, name);
49
41
  const content = {};
50
42
  let found = false;
51
43
  for (const [file, key, type] of FILES[kind](name)) {
@@ -233,20 +225,18 @@ async function writeAtomic(file, text) {
233
225
  await mkdir(path.dirname(file), { recursive: true });
234
226
  const temporary = `${file}.${randomUUID().slice(0, 8)}.tmp`;
235
227
  await writeFile(temporary, text);
236
- await moveFile(temporary, file);
228
+ await rename(temporary, file);
237
229
  }
238
230
  var exists = (target) => stat(target).then(() => true, () => false);
239
231
  async function saveEntry(kind, name, content, { scope, root, library, templates, revision } = {}) {
240
232
  kindOf(kind);
241
233
  nameOf(name);
242
234
  const directory = base(scope, { root, library }, kind);
243
- await safeEntryPaths(directory, kind, name);
244
235
  await check(kind, name, content, { scope, root, library, templates });
245
236
  const current = await readLayer(kind, name, directory);
246
237
  if (revision !== void 0 && revision !== revisionOf(current)) {
247
238
  fail("REVISION_CONFLICT", current === void 0 ? `${kind} '${name}' was removed from the ${scope} since you opened it` : revision === null ? `${kind} '${name}' already exists in the ${scope}` : `${kind} '${name}' changed in the ${scope} since you opened it; reload it and apply your change again`);
248
239
  }
249
- if (scope === "library") await mkdir(directory, { recursive: true, mode: 448 });
250
240
  for (const [file, key, type] of FILES[kind](name)) {
251
241
  const target = path.join(directory, kind, file);
252
242
  const value = content[key];
package/dist/library.js CHANGED
@@ -1,7 +1,6 @@
1
1
  // packages/core/src/library.js
2
2
  import { shippedTemplateRoot } from "./template-path.js";
3
- import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
4
- import { moveFile } from "./atomic.js";
3
+ import { mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
5
4
  import { createHash, randomUUID } from "node:crypto";
6
5
  import os from "node:os";
7
6
  import path from "node:path";
@@ -35,7 +34,6 @@ async function shippedFiles({ templateRoot, templates }) {
35
34
  }
36
35
  async function seedLibrary(home, options = {}) {
37
36
  const shipped = await shippedFiles(options);
38
- await mkdir(home, { recursive: true, mode: 448 });
39
37
  const manifestPath = path.join(home, MANIFEST);
40
38
  const manifestText = await optional(manifestPath);
41
39
  const manifest = manifestText ? JSON.parse(manifestText) : { files: {} };
@@ -63,7 +61,7 @@ async function seedLibrary(home, options = {}) {
63
61
  await mkdir(home, { recursive: true });
64
62
  const temporary = `${manifestPath}.${randomUUID().slice(0, 8)}.tmp`;
65
63
  await writeFile(temporary, JSON.stringify(manifest, null, 2) + "\n");
66
- await moveFile(temporary, manifestPath);
64
+ await rename(temporary, manifestPath);
67
65
  }
68
66
  return { created, updated };
69
67
  }
@@ -1,8 +1,7 @@
1
1
  // packages/core/src/permissions.js
2
2
  import path from "node:path";
3
3
  import { randomUUID } from "node:crypto";
4
- import { readFile, writeFile } from "node:fs/promises";
5
- import { moveFile } from "./atomic.js";
4
+ import { readFile, rename, writeFile } from "node:fs/promises";
6
5
  import { AlpError } from "./errors.js";
7
6
  var BASES = ["read-only", "workspace-write", "full-access"];
8
7
  var RANK = { "read-only": 0, "workspace-write": 1, "full-access": 2 };
@@ -11,8 +10,6 @@ var MAX_RULES = 200;
11
10
  var NAME = /^[\w.-]{1,64}$/;
12
11
  var RULE = /^([A-Za-z][\w-]*)(?:\((.*)\))?$/s;
13
12
  var TOOLS = ["Bash", "Read", "Edit", "Write", "MultiEdit", "NotebookEdit", "Glob", "Grep", "WebFetch", "WebSearch", "Skill"];
14
- var own = (value, key) => Object.hasOwn(value, key) ? value[key] : void 0;
15
- var setOwn = (value, key, entry) => Object.defineProperty(value, key, { value: entry, enumerable: true, writable: true, configurable: true });
16
13
  var object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
17
14
  var fail = (source, message) => {
18
15
  throw new AlpError("INVALID_SETTINGS", `${source}: ${message}`);
@@ -49,7 +46,7 @@ function validatePermissions(value, source) {
49
46
  for (const rule of list) parseRule(rule, `${source}: ${where}.${kind}`);
50
47
  rules[kind] = list.map((rule) => rule.trim());
51
48
  }
52
- setOwn(profiles, name, { ...profile.base ? { base: profile.base } : {}, ...profile.beyondMode ? { beyondMode: profile.beyondMode } : {}, ...profile.workdir ? { workdir: profile.workdir } : {}, ...rules });
49
+ profiles[name] = { ...profile.base ? { base: profile.base } : {}, ...profile.beyondMode ? { beyondMode: profile.beyondMode } : {}, ...profile.workdir ? { workdir: profile.workdir } : {}, ...rules };
53
50
  }
54
51
  }
55
52
  const agents = {};
@@ -57,7 +54,7 @@ function validatePermissions(value, source) {
57
54
  if (!object(value.agents)) fail(source, "permissions.agents must be an object");
58
55
  for (const [agent, profile] of Object.entries(value.agents)) {
59
56
  if (typeof profile !== "string" || !NAME.test(profile)) fail(source, `permissions.agents.${agent} must name a profile`);
60
- setOwn(agents, agent, profile);
57
+ agents[agent] = profile;
61
58
  }
62
59
  }
63
60
  return { profiles, agents };
@@ -82,10 +79,10 @@ async function settingsPermissions(file) {
82
79
  async function profileFor(projectRoot, home, agent) {
83
80
  const project = await settingsPermissions(path.join(projectRoot, ".alp", "settings.json"));
84
81
  const user = home ? await settingsPermissions(path.join(home, "settings.json")) : { profiles: {}, agents: {} };
85
- const name = own(project.agents, agent) ?? own(user.agents, agent);
82
+ const name = project.agents[agent] ?? user.agents[agent];
86
83
  const advisor = ADVISORS.includes(agent);
87
84
  if (!name) return advisor ? { name: "read-only", base: "read-only", allow: [], ask: [], deny: [], beyondMode: "refuse" } : null;
88
- const defined = [own(project.profiles, name), own(user.profiles, name)].filter(Boolean);
85
+ const defined = [project.profiles[name], user.profiles[name]].filter(Boolean);
89
86
  if (!defined.length && !BASES.includes(name)) {
90
87
  throw new AlpError("INVALID_SETTINGS", `permissions.agents.${agent} names profile '${name}', which no settings file defines`);
91
88
  }
@@ -120,7 +117,7 @@ async function addAllowRule(projectRoot, home, profileName, rule) {
120
117
  const temporary = `${file}.${randomUUID().slice(0, 8)}.tmp`;
121
118
  await writeFile(temporary, `${JSON.stringify(settings, null, 2)}
122
119
  `);
123
- await moveFile(temporary, file);
120
+ await rename(temporary, file);
124
121
  return file;
125
122
  }
126
123
  throw new AlpError("INVALID_SETTINGS", `No settings file defines profile ${profileName}`);
@@ -57,18 +57,6 @@ export function acpModel(model: any): {
57
57
  provider: any;
58
58
  model: any;
59
59
  };
60
- /**
61
- * How to start a command without a shell. On Windows a bare name installed with npm is a
62
- * `<name>.cmd` shim, which spawn refuses to run without one; its target runs instead: the
63
- * native executable it calls, or the Node script, under this Node. A bare name with an .exe or
64
- * .com on PATH, any other command, and every command elsewhere are returned as they are.
65
- * @param {string} command @param {string[]} [args] @param {Record<string, string | undefined>} [env]
66
- * @returns {{ command: string, args: string[] }}
67
- */
68
- export function launchCommand(command: string, args?: string[], env?: Record<string, string | undefined>): {
69
- command: string;
70
- args: string[];
71
- };
72
60
  /**
73
61
  * ACP providers (ALPD §46): agents that speak the Agent Client Protocol, such as Gemini
74
62
  * CLI or opencode, defined in the user's library as providers/<id>.json. An agent runs
package/dist/providers.js CHANGED
@@ -1,5 +1,4 @@
1
1
  // packages/core/src/providers.js
2
- import { existsSync, readFileSync } from "node:fs";
3
2
  import { readdir, readFile } from "node:fs/promises";
4
3
  import path from "node:path";
5
4
  import { AlpError } from "./errors.js";
@@ -73,28 +72,11 @@ function acpModel(model) {
73
72
  const slash = model.indexOf("/");
74
73
  return slash < 0 ? { provider: model, model: void 0 } : { provider: model.slice(0, slash), model: model.slice(slash + 1) || void 0 };
75
74
  }
76
- function launchCommand(command, args = [], env = process.env) {
77
- if (process.platform !== "win32" || /[\\/]/.test(command) || path.extname(command)) return { command, args };
78
- const key = Object.keys(env).find((name) => name.toUpperCase() === "PATH");
79
- for (const directory of (key ? env[key] ?? "" : "").split(";").filter(Boolean)) {
80
- if ([".exe", ".com"].some((extension) => existsSync(path.join(directory, command + extension)))) break;
81
- const shim = path.join(directory, `${command}.cmd`);
82
- if (!existsSync(shim)) continue;
83
- const text = readFileSync(shim, "utf8");
84
- const script = /"%_prog%"((?:\s+[^\s"]+)*)\s+"%~?dp0%\\?([^"]+)"\s+%\*/i.exec(text);
85
- if (script) return { command: process.execPath, args: [...script[1].split(/\s+/).filter(Boolean), path.join(directory, script[2]), ...args] };
86
- const native = /^\s*"%~?dp0%\\?([^"]+\.(?:exe|com))"\s+%\*/im.exec(text);
87
- if (native) return { command: path.join(directory, native[1]), args };
88
- break;
89
- }
90
- return { command, args };
91
- }
92
75
  export {
93
76
  BUILTIN_RUNTIMES,
94
77
  PROVIDER_SETTINGS,
95
78
  RESERVED_PROVIDERS,
96
79
  acpModel,
97
- launchCommand,
98
80
  listProviders,
99
81
  loadProvider,
100
82
  validateProvider
@@ -38,7 +38,6 @@ export function taskRows(projectRoot: string): Promise<{
38
38
  type: import("./tasks.js").TaskType;
39
39
  priority: number;
40
40
  status: import("./tasks.js").TaskStatus;
41
- rev: number;
42
41
  }[];
43
42
  unreadable: {
44
43
  file: string;
package/dist/task-rows.js CHANGED
@@ -21,7 +21,6 @@ async function taskRows(projectRoot) {
21
21
  type: task.type,
22
22
  priority: task.priority,
23
23
  status: task.status,
24
- rev: task.rev,
25
24
  ...task.parent ? { parent: task.parent } : {},
26
25
  ...task.assignee ? { assignee: task.assignee.agent } : {},
27
26
  ready: ready.has(task.id),
package/dist/tasks.d.ts CHANGED
@@ -108,7 +108,7 @@ export function gateOpen(gate: TaskGate, now?: number): boolean;
108
108
  export function describeGate(gate: TaskGate): string;
109
109
  export function gatesOf(task: Task, tasks: Task[]): string[];
110
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>;
111
+ export function resolveGate(projectRoot: string, id: string, gateId: string, resolve: { by: string; note?: string; remove?: boolean }): Promise<Task>;
112
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
113
  export function compactTasks(projectRoot: string, options: { days?: number; dryRun?: boolean }, by: string): Promise<Array<{ id: string; before: number; after: number }>>;
114
114
  export type TaskBatch = {
@@ -125,4 +125,3 @@ export type TaskVerification = { at: string; by: string; passed: boolean; where?
125
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
126
  export function epicReport(id: string, tasks: Task[]): EpicReport;
127
127
  export function landedParents(tasks: Task[]): Array<{ task: Task; children: number }>;
128
- export function withTaskLock<T>(projectRoot: string, work: () => Promise<T>): Promise<T>;
package/dist/tasks.js CHANGED
@@ -1,12 +1,10 @@
1
1
  // packages/core/src/tasks.js
2
- import { mkdir, readFile, readdir, rmdir, stat, writeFile } from "node:fs/promises";
3
- import { moveFile } from "./atomic.js";
2
+ import { mkdir, readFile, readdir, rename, rmdir, stat, writeFile } from "node:fs/promises";
4
3
  import { createHash, randomUUID } from "node:crypto";
5
4
  import path from "node:path";
6
5
  import { AlpError } from "./errors.js";
7
- import { assertNoSymlinks } from "./safe-path.js";
8
6
  import { describeVerification } from "./verify.js";
9
- var TASKS_DIR = ".alp/tasks";
7
+ var TASKS_DIR = path.join(".alp", "tasks");
10
8
  var TASK_TYPES = ["task", "bug", "feature", "chore", "epic"];
11
9
  var TASK_STATUSES = ["open", "in_progress", "review", "closed"];
12
10
  var CLOSE_REASONS = ["done", "wontfix", "duplicate", "superseded"];
@@ -33,24 +31,15 @@ var now = () => (/* @__PURE__ */ new Date()).toISOString();
33
31
  var fail = (code, message) => {
34
32
  throw new AlpError(code, message);
35
33
  };
36
- async function taskNames(projectRoot) {
37
- const scope = path.join(path.resolve(projectRoot), ".alp");
38
- await assertNoSymlinks(scope, "tasks");
34
+ async function loadTasks(projectRoot) {
35
+ const dir = tasksDir(projectRoot);
39
36
  let names;
40
37
  try {
41
- names = await readdir(tasksDir(projectRoot));
38
+ names = await readdir(dir);
42
39
  } catch (error) {
43
- if (error.code === "ENOENT") return [];
40
+ if (error.code === "ENOENT") return { tasks: [], errors: [] };
44
41
  throw error;
45
42
  }
46
- for (const name of [...names.filter((name2) => name2.endsWith(".json")), LOCK, ".gitignore"]) {
47
- await assertNoSymlinks(scope, path.join("tasks", name));
48
- }
49
- return names;
50
- }
51
- async function loadTasks(projectRoot) {
52
- const dir = tasksDir(projectRoot);
53
- const names = await taskNames(projectRoot);
54
43
  const tasks = [];
55
44
  const errors = [];
56
45
  for (const name of names.filter((name2) => name2.endsWith(".json")).sort()) {
@@ -222,7 +211,6 @@ async function locked(projectRoot, work) {
222
211
  const previous = queues.get(dir) ?? Promise.resolve();
223
212
  const run = previous.catch(() => {
224
213
  }).then(async () => {
225
- await taskNames(projectRoot);
226
214
  await prepare(dir);
227
215
  const lock = path.join(dir, LOCK);
228
216
  const deadline = Date.now() + LOCK_WAIT_MS;
@@ -252,7 +240,6 @@ async function locked(projectRoot, work) {
252
240
  queues.set(dir, run);
253
241
  return run;
254
242
  }
255
- var withTaskLock = (projectRoot, work) => locked(projectRoot, work);
256
243
  async function prepare(dir) {
257
244
  await mkdir(dir, { recursive: true });
258
245
  await writeFile(path.join(dir, ".gitignore"), GITIGNORE, { flag: "wx" }).catch((error) => {
@@ -261,10 +248,9 @@ async function prepare(dir) {
261
248
  }
262
249
  async function save(projectRoot, task) {
263
250
  const file = taskFile(projectRoot, task.id);
264
- await assertNoSymlinks(path.join(path.resolve(projectRoot), ".alp"), path.join("tasks", `${task.id}.json`));
265
251
  const temporary = `${file}.${randomUUID().slice(0, 8)}.tmp`;
266
252
  await writeFile(temporary, JSON.stringify(task, null, 2) + "\n");
267
- await moveFile(temporary, file);
253
+ await rename(temporary, file);
268
254
  return task;
269
255
  }
270
256
  function newId(tasks, title, parent) {
@@ -630,7 +616,7 @@ function addGate(projectRoot, id, { kind, note, until, ref } = {}, by) {
630
616
  addLog(task, by, "gated", { gate: describeGate(gate) });
631
617
  });
632
618
  }
633
- function resolveGate(projectRoot, id, gateId, { by, note, remove = false, ifRev } = {}) {
619
+ function resolveGate(projectRoot, id, gateId, { by, note, remove = false } = {}) {
634
620
  return mutate(projectRoot, id, (task) => {
635
621
  const gate = (task.gates ?? []).find((entry) => entry.id === gateId) ?? fail("INVALID_TASK", `${id} has no gate ${gateId}`);
636
622
  if (remove) {
@@ -649,7 +635,7 @@ function resolveGate(projectRoot, id, gateId, { by, note, remove = false, ifRev
649
635
  task.closed = { at: now(), by, reason: "done", summary };
650
636
  addLog(task, by, "closed", { reason: "done", summary });
651
637
  }
652
- }, { ifRev });
638
+ });
653
639
  }
654
640
  async function checkGates(projectRoot, gh) {
655
641
  const { tasks } = await loadTasks(projectRoot);
@@ -830,6 +816,5 @@ export {
830
816
  taskDigest,
831
817
  tasksDir,
832
818
  updateTask,
833
- verificationFailed,
834
- withTaskLock
819
+ verificationFailed
835
820
  };
package/dist/trust.js CHANGED
@@ -1,6 +1,5 @@
1
1
  // packages/core/src/trust.js
2
- import { mkdir, readFile, realpath, writeFile } from "node:fs/promises";
3
- import { moveFile } from "./atomic.js";
2
+ import { mkdir, readFile, realpath, rename, writeFile } from "node:fs/promises";
4
3
  import { randomUUID } from "node:crypto";
5
4
  import path from "node:path";
6
5
  var file = (home) => path.join(home, "state", "trust.json");
@@ -18,7 +17,7 @@ async function store(home, value) {
18
17
  await mkdir(path.dirname(file(home)), { recursive: true });
19
18
  const temporary = `${file(home)}.${randomUUID().slice(0, 8)}.tmp`;
20
19
  await writeFile(temporary, JSON.stringify(value, null, 2) + "\n");
21
- await moveFile(temporary, file(home));
20
+ await rename(temporary, file(home));
22
21
  }
23
22
  async function isTrusted(home, projectRoot) {
24
23
  return Object.hasOwn((await load(home)).projects, await key(projectRoot));
@@ -1,6 +1,5 @@
1
1
  // packages/core/src/user-settings.js
2
- import { mkdir, readFile, writeFile } from "node:fs/promises";
3
- import { moveFile } from "./atomic.js";
2
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
4
3
  import { randomUUID } from "node:crypto";
5
4
  import path from "node:path";
6
5
  import { AlpError } from "./errors.js";
@@ -46,20 +45,20 @@ async function setLanguage(home, language) {
46
45
  if (language === null) delete next.language;
47
46
  else next.language = language.trim();
48
47
  validateUserSettings(next, file);
49
- await mkdir(home, { recursive: true, mode: 448 });
48
+ await mkdir(home, { recursive: true });
50
49
  const temporary = `${file}.${randomUUID().slice(0, 8)}.tmp`;
51
50
  await writeFile(temporary, JSON.stringify(next, null, 2) + "\n");
52
- await moveFile(temporary, file);
51
+ await rename(temporary, file);
53
52
  return languageSetting(home);
54
53
  }
55
54
  async function setWebRelay(home, patch) {
56
55
  const { file, settings } = await readSettings(home);
57
56
  const next = { ...settings, web: { ...settings.web, relay: { ...settings.web?.relay, ...patch } } };
58
57
  validateUserSettings(next, file);
59
- await mkdir(home, { recursive: true, mode: 448 });
58
+ await mkdir(home, { recursive: true });
60
59
  const temporary = `${file}.${randomUUID().slice(0, 8)}.tmp`;
61
60
  await writeFile(temporary, JSON.stringify(next, null, 2) + "\n");
62
- await moveFile(temporary, file);
61
+ await rename(temporary, file);
63
62
  return next.web.relay;
64
63
  }
65
64
  export {
package/dist/verify.js CHANGED
@@ -1,8 +1,8 @@
1
1
  // packages/core/src/verify.js
2
+ import { spawn } from "node:child_process";
2
3
  import { readFile } from "node:fs/promises";
3
4
  import path from "node:path";
4
5
  import { AlpError } from "./errors.js";
5
- import { signalShell, spawnShell } from "./shell.js";
6
6
  var VERIFY_STEPS = ["setup", "typecheck", "test"];
7
7
  var VERIFY_TIMEOUT_SEC = 600;
8
8
  var VERIFY_GRACE_MS = 5e3;
@@ -50,11 +50,23 @@ async function verifyConfig(projectRoot) {
50
50
  function runCommand(command, cwd, { timeoutMs, idleMs }, env) {
51
51
  return new Promise((resolve) => {
52
52
  const started = Date.now();
53
- const child = spawnShell(command, { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
53
+ const windows = process.platform === "win32";
54
+ const child = spawn(windows ? process.env.ComSpec ?? "cmd.exe" : "/bin/sh", windows ? ["/d", "/s", "/c", command] : ["-c", command], {
55
+ cwd,
56
+ env,
57
+ stdio: ["ignore", "pipe", "pipe"],
58
+ detached: !windows,
59
+ windowsHide: true
60
+ });
54
61
  let output = "";
55
62
  let stopped;
56
63
  let idle;
57
- const signal = (name) => signalShell(child, name);
64
+ const signal = (name) => {
65
+ try {
66
+ windows ? child.kill() : process.kill(-child.pid, name);
67
+ } catch {
68
+ }
69
+ };
58
70
  const stop = (reason) => {
59
71
  if (stopped) return;
60
72
  stopped = reason;
package/package.json CHANGED
@@ -1,13 +1,12 @@
1
1
  {
2
2
  "name": "@anhlp/alp-core",
3
- "version": "0.0.0-beta.1",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "description": "ALP core module",
6
6
  "files": [
7
7
  "src",
8
8
  "dist",
9
- "templates",
10
- "NOTICE"
9
+ "templates"
11
10
  ],
12
11
  "exports": {
13
12
  "./adapter": {
@@ -18,22 +17,6 @@
18
17
  "types": "./dist/adapter.d.ts",
19
18
  "default": "./src/adapter.js"
20
19
  },
21
- "./atomic": {
22
- "alp-source": {
23
- "types": "./src/atomic.js",
24
- "default": "./src/atomic.js"
25
- },
26
- "types": "./dist/atomic.d.ts",
27
- "default": "./src/atomic.js"
28
- },
29
- "./backup": {
30
- "alp-source": {
31
- "types": "./src/backup.js",
32
- "default": "./src/backup.js"
33
- },
34
- "types": "./dist/backup.d.ts",
35
- "default": "./src/backup.js"
36
- },
37
20
  "./beads": {
38
21
  "alp-source": {
39
22
  "types": "./src/beads.js",
@@ -122,22 +105,6 @@
122
105
  "types": "./dist/library.d.ts",
123
106
  "default": "./src/library.js"
124
107
  },
125
- "./log": {
126
- "alp-source": {
127
- "types": "./src/log.js",
128
- "default": "./src/log.js"
129
- },
130
- "types": "./dist/log.d.ts",
131
- "default": "./src/log.js"
132
- },
133
- "./migrations": {
134
- "alp-source": {
135
- "types": "./src/migrations.js",
136
- "default": "./src/migrations.js"
137
- },
138
- "types": "./dist/migrations.d.ts",
139
- "default": "./src/migrations.js"
140
- },
141
108
  "./models": {
142
109
  "alp-source": {
143
110
  "types": "./src/models.js",
@@ -186,14 +153,6 @@
186
153
  "types": "./dist/resolver.d.ts",
187
154
  "default": "./src/resolver.js"
188
155
  },
189
- "./state-schema": {
190
- "alp-source": {
191
- "types": "./src/state-schema.js",
192
- "default": "./src/state-schema.js"
193
- },
194
- "types": "./dist/state-schema.d.ts",
195
- "default": "./src/state-schema.js"
196
- },
197
156
  "./task-rows": {
198
157
  "alp-source": {
199
158
  "types": "./src/task-rows.js",
@@ -281,10 +240,10 @@
281
240
  }
282
241
  },
283
242
  "engines": {
284
- "node": ">=22.12"
243
+ "node": ">=20"
285
244
  },
286
245
  "dependencies": {
287
- "@anhlp/alp-protocol": "0.0.0-beta.1"
246
+ "@anhlp/alp-protocol": "0.10.0"
288
247
  },
289
248
  "scripts": {
290
249
  "build": "node ../../scripts/build-workspace.mjs core",
@@ -293,17 +252,11 @@
293
252
  },
294
253
  "publishConfig": {
295
254
  "access": "public",
296
- "registry": "https://registry.npmjs.org/",
297
- "tag": "beta"
255
+ "registry": "https://registry.npmjs.org/"
298
256
  },
299
- "license": "Apache-2.0",
300
257
  "repository": {
301
258
  "type": "git",
302
- "url": "git+https://github.com/phucanh08/alp.git",
259
+ "url": "git+https://github.com/phucanh08/alp-paseo.git",
303
260
  "directory": "packages/core"
304
- },
305
- "homepage": "https://github.com/phucanh08/alp/tree/main/packages/core#readme",
306
- "bugs": {
307
- "url": "https://github.com/phucanh08/alp/issues"
308
261
  }
309
262
  }
package/src/hook-run.js CHANGED
@@ -1,10 +1,10 @@
1
- import { signalShell, spawnShell } from './shell.js';
1
+ import { spawn } from 'node:child_process';
2
2
 
3
3
  const OUTPUT_LIMIT = 64 * 1024;
4
4
  const GRACE_MS = 2000;
5
5
 
6
6
  /**
7
- * Runs a hook's command once (ALPD §43): through /bin/sh (cmd.exe on Windows) in `cwd`, with the event as JSON
7
+ * Runs a hook's command once (ALPD §43): through /bin/sh in `cwd`, with the event as JSON
8
8
  * on stdin and `env` added to ALP's environment. It runs in its own process group, so
9
9
  * past its timeout the whole group gets SIGTERM, then SIGKILL two seconds later; a
10
10
  * shell's children do not outlive it. Output beyond 64 KiB per stream is cut.
@@ -17,8 +17,8 @@ export function runHook(hook, payload, { cwd, env = {}, timeoutMs } = {}) {
17
17
  const limit = timeoutMs ?? (hook.timeoutSec ?? 60) * 1000;
18
18
  const started = Date.now();
19
19
  return new Promise((resolve, reject) => {
20
- const child = spawnShell(hook.command, { cwd, env: { ...process.env, ...env }, stdio: ['pipe', 'pipe', 'pipe'] });
21
- const signal = name => signalShell(child, name);
20
+ const child = spawn('/bin/sh', ['-c', hook.command], { cwd, env: { ...process.env, ...env }, stdio: ['pipe', 'pipe', 'pipe'], detached: true });
21
+ const signal = name => { try { process.kill(-child.pid, name); } catch { child.kill(name); } };
22
22
  const output = { stdout: '', stderr: '' };
23
23
  for (const stream of ['stdout', 'stderr']) {
24
24
  child[stream].setEncoding('utf8');