@elu-labs/elu 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/LICENSE +21 -0
  2. package/LICENSES/Apache-2.0.txt +202 -0
  3. package/README.md +327 -2
  4. package/SECURITY.md +333 -0
  5. package/THIRD_PARTY_NOTICES.md +62 -0
  6. package/bin/elu-code.mjs +209 -0
  7. package/bin/elu.cjs +10 -0
  8. package/bin/start.cjs +6 -0
  9. package/docs/peer-network.md +257 -0
  10. package/docs/permissions.md +285 -0
  11. package/docs/vendors/antigravity.md +131 -0
  12. package/docs/vendors/claude.md +124 -0
  13. package/docs/vendors/codex.md +64 -0
  14. package/docs/vendors/grok.md +151 -0
  15. package/lib/acp-client.mjs +100 -0
  16. package/lib/adapters/agy.mjs +283 -0
  17. package/lib/adapters/claude.mjs +311 -0
  18. package/lib/adapters/codex.mjs +137 -0
  19. package/lib/adapters/grok.mjs +384 -0
  20. package/lib/adapters/index.mjs +13 -0
  21. package/lib/agents.mjs +77 -0
  22. package/lib/agy-hook.mjs +53 -0
  23. package/lib/approval-server.mjs +85 -0
  24. package/lib/approvals.mjs +128 -0
  25. package/lib/awake.mjs +45 -0
  26. package/lib/board.mjs +261 -0
  27. package/lib/bot-models.mjs +71 -0
  28. package/lib/bot-schedules.mjs +291 -0
  29. package/lib/bots.mjs +361 -0
  30. package/lib/changes.mjs +168 -0
  31. package/lib/checks.mjs +102 -0
  32. package/lib/conductor.mjs +337 -0
  33. package/lib/config.mjs +147 -0
  34. package/lib/conformance.mjs +109 -0
  35. package/lib/default-bots.mjs +150 -0
  36. package/lib/detect.mjs +108 -0
  37. package/lib/diff.mjs +217 -0
  38. package/lib/editor.mjs +165 -0
  39. package/lib/format.mjs +51 -0
  40. package/lib/git.mjs +173 -0
  41. package/lib/init.mjs +129 -0
  42. package/lib/install.mjs +130 -0
  43. package/lib/kanban.mjs +180 -0
  44. package/lib/mcp.mjs +79 -0
  45. package/lib/memory.mjs +35 -0
  46. package/lib/models.mjs +144 -0
  47. package/lib/modes.mjs +95 -0
  48. package/lib/never-list.mjs +110 -0
  49. package/lib/never-shims.mjs +311 -0
  50. package/lib/paste.mjs +56 -0
  51. package/lib/peer/a2a-http.mjs +166 -0
  52. package/lib/peer/a2a.mjs +357 -0
  53. package/lib/peer/asks.mjs +329 -0
  54. package/lib/peer/canonical.mjs +72 -0
  55. package/lib/peer/cli.mjs +154 -0
  56. package/lib/peer/daemon.mjs +525 -0
  57. package/lib/peer/identity.mjs +81 -0
  58. package/lib/peer/index.mjs +291 -0
  59. package/lib/peer/intake.mjs +207 -0
  60. package/lib/peer/invite.mjs +76 -0
  61. package/lib/peer/paths.mjs +7 -0
  62. package/lib/peer/peerdata.mjs +21 -0
  63. package/lib/peer/pictures.mjs +295 -0
  64. package/lib/peer/plans.mjs +182 -0
  65. package/lib/peer/relay.mjs +31 -0
  66. package/lib/peer/replies.mjs +128 -0
  67. package/lib/peer/roster.mjs +256 -0
  68. package/lib/peer/rpc.mjs +88 -0
  69. package/lib/peer/secrets.mjs +88 -0
  70. package/lib/peer/settings.mjs +45 -0
  71. package/lib/peer/shares.mjs +120 -0
  72. package/lib/peer/status.mjs +84 -0
  73. package/lib/peer/taskstore.mjs +108 -0
  74. package/lib/peer/transport.mjs +120 -0
  75. package/lib/pictures.mjs +104 -0
  76. package/lib/plan.mjs +238 -0
  77. package/lib/policy.mjs +98 -0
  78. package/lib/process.mjs +92 -0
  79. package/lib/project-settings.mjs +55 -0
  80. package/lib/project-trust.mjs +230 -0
  81. package/lib/projects.mjs +668 -0
  82. package/lib/publish.mjs +105 -0
  83. package/lib/runner.mjs +874 -0
  84. package/lib/sandbox.mjs +102 -0
  85. package/lib/schedules.mjs +270 -0
  86. package/lib/selection.mjs +202 -0
  87. package/lib/shell.mjs +101 -0
  88. package/lib/state.mjs +247 -0
  89. package/lib/terminal-bots.mjs +239 -0
  90. package/lib/terminal-commands.mjs +165 -0
  91. package/lib/terminal-plan.mjs +37 -0
  92. package/lib/terminal-visuals.mjs +268 -0
  93. package/lib/terminal.mjs +3159 -0
  94. package/lib/unit-file.mjs +17 -0
  95. package/lib/usage.mjs +249 -0
  96. package/lib/util.mjs +86 -0
  97. package/lib/vendor-check.mjs +96 -0
  98. package/lib/vendors.mjs +191 -0
  99. package/lib/workspace.mjs +168 -0
  100. package/lib/yaml.mjs +624 -0
  101. package/npm-shrinkwrap.json +827 -0
  102. package/package.json +70 -4
@@ -0,0 +1,105 @@
1
+ import path from 'node:path';
2
+ import { execute, refuse, hash, safeText } from './util.mjs';
3
+ import { git, head, assertTaskGitSafe, taskChanges, assertPublishingHooks, snapshotHooks, hooksSnapshotDigest } from './git.mjs';
4
+ import { findSandboxHelper } from './sandbox.mjs';
5
+ import { evidenceCurrent, pinnedConfig } from './checks.mjs';
6
+ import { saveTask, event } from './state.mjs';
7
+ import { botKey, botReviews, getBot, sessionBots } from './bots.mjs';
8
+
9
+ export function redact(value, limit = 4000) {
10
+ return safeText(value, limit).replace(/-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?(?:-----END [^-]*PRIVATE KEY-----|$)/g, '[redacted]')
11
+ .replace(/\b(?:sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,}|AKIA[A-Z0-9]{16})\b/g, '[redacted]')
12
+ .replace(/\bBearer\s+[^\s"'<>]+/gi, 'Bearer [redacted]')
13
+ .replace(/\b(?:password|secret|token|api[_-]?key)\s*[:=]\s*[^\s,;]+/gi, '[redacted]');
14
+ }
15
+ const markdown = (value) => redact(value).replace(/[\[\]<>`*_!#|]/g, '\\$&').replace(/\n/g, ' ');
16
+ export function prBody(task) {
17
+ const lines = [markdown(task.result?.summary??task.goal??'Changes prepared for owner review.'), '', '<!-- elu-code:evidence -->', 'Checks:', ...(task.evidence?.results ?? []).map((row) => `- ${markdown(row.name)}: ${row.status}`), `- changed lines: ${task.evidence?.changedLines ?? 'unknown'}`, `- review: ${task.reviewStatus ?? 'pending'}`, '', 'Review responses:'];
18
+ if (!task.responses.length) lines.push('- No responses were needed.');
19
+ for (const response of task.responses) lines.push(`- ${markdown(response.id)}: ${response.action} — ${markdown(response.reason)}`);
20
+ lines.push('<!-- /elu-code:evidence -->');
21
+ return `${lines.join('\n')}\n`;
22
+ }
23
+ export function assertReady(task) {
24
+ if (task.state !== 'ready' || !evidenceCurrent(task)) refuse('Checks are missing, failed, or out of date. Run elu check before publishing.');
25
+ if (!['passed', 'skipped'].includes(task.reviewStatus)) refuse('Review has not finished.');
26
+ if (task.reviewStatus === 'passed' && task.reviews.at(-1)?.digest !== task.evidence.digest) refuse('Review is stale for the current change.');
27
+ const lastRound = task.reviews.filter(row => row.round === task.reviews.at(-1)?.round);
28
+ if (lastRound.some(review => review.result.comments.some(row => row.severity === 'must_fix'))) refuse('Must-fix review comments are still open.');
29
+ // Every bot in the chat reviews before publishing. One that joined after the last review
30
+ // has not; one that is no longer saved has nothing to say. A session from before bots did
31
+ // every review has no list of its own, and its last review was saved under the name
32
+ // reviewer: that review stands for @reviewer.
33
+ const earlier = !Array.isArray(task.bots);
34
+ // A bot that has a schedule works by itself and reviews nothing, so nothing waits for it.
35
+ const waiting = sessionBots(task).find(name => botReviews(getBot(name)) && !lastRound.some(review => review.role === botKey(name) || earlier && review.role === name));
36
+ if (waiting) refuse(`@${waiting} has not reviewed this change. It reviews after the next edit; or take @${waiting} out of the session.`);
37
+ if (!taskChanges(task).paths.length) refuse('There are no changes to publish.');
38
+ }
39
+ function gh(root, args) { return execute('gh', args, { cwd: root, env: { ...process.env, GH_PROMPT_DISABLED: '1' } }).stdout; }
40
+ export function destination(repo, task) {
41
+ assertBranch(task);
42
+ const url = git(repo.root, ['remote', 'get-url', '--push', 'origin']).stdout.trim();
43
+ const urls = git(repo.root, ['remote', 'get-url', '--push', '--all', 'origin']).stdout.trim().split('\n');
44
+ if (urls.length !== 1) refuse('origin has more than one push destination.');
45
+ const match = /^(?:git@github\.com:|https:\/\/github\.com\/)([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+?)(?:\.git)?$/.exec(url);
46
+ if (!match) refuse('The draft PR publisher currently supports an explicit GitHub origin only.');
47
+ const name = match[1];
48
+ const data = JSON.parse(gh(repo.root, ['repo', 'view', name, '--json', 'nameWithOwner,defaultBranchRef,url']));
49
+ if (!data.defaultBranchRef?.name) refuse('The remote needs an existing default branch before ELU Code can propose a pull request.');
50
+ if (task.branch === data.defaultBranchRef.name || ['main', 'master'].includes(task.branch)) refuse('Publishing directly to a default branch is forbidden.');
51
+ const prs = JSON.parse(gh(repo.root, ['pr', 'list', '--repo', name, '--head', task.branch, '--state', 'all', '--json', 'number,state,headRefName,baseRefName,isDraft,url']));
52
+ if (prs.length > 1 || prs.some((pr) => pr.state !== 'OPEN' || pr.baseRefName !== data.defaultBranchRef.name || !pr.isDraft)) refuse('The branch has a closed, merged, non-draft, or incompatible pull request.');
53
+ return { url, name, ref: `refs/heads/${task.branch}`, base: data.defaultBranchRef.name, existingPr: prs[0] ?? null };
54
+ }
55
+ // Checked before anything is committed, so a `here` task on main never gets a commit it cannot push.
56
+ function assertBranch(task) {
57
+ if (typeof task.branch !== 'string' || !task.branch) refuse('Check out a branch before publishing.');
58
+ if (['main', 'master'].includes(task.branch)) refuse('Publishing directly to a default branch is forbidden.');
59
+ }
60
+ export async function preparePublish(repo, task) {
61
+ if (task.mode === 'automatic' && task.localChanges?.length) refuse('This workspace includes pre-existing local edits. Keep or commit those edits separately, then start a clean task before publishing. Nothing was pushed.');
62
+ assertBranch(task); assertReady(task); assertTaskGitSafe(repo,task);
63
+ if (task.mode === 'automatic' && !task.hooksDirectory) {
64
+ const { config } = pinnedConfig(repo, task.base, { task });
65
+ task.hooksDirectory = snapshotHooks(repo, task, findSandboxHelper(config)); task.hooksSnapshotDigest = hooksSnapshotDigest(task.hooksDirectory); saveTask(repo, task);
66
+ }
67
+ assertPublishingHooks(repo,task);
68
+ if (head(task.worktree) !== (task.commit ?? task.base)) refuse('The task worktree HEAD changed outside ELU Code.');
69
+ if (!task.commit || git(task.worktree, ['status', '--porcelain']).stdout.trim()) {
70
+ const files = taskChanges(task).paths;
71
+ git(task.worktree, ['-c',`core.hooksPath=${task.hooksDirectory}`,'add', '--', ...files]);
72
+ git(task.worktree, ['-c',`core.hooksPath=${task.hooksDirectory}`, 'commit', '-m', 'Update task implementation']);
73
+ task.commit = head(task.worktree); saveTask(repo, task);
74
+ }
75
+ if (head(task.worktree) !== task.commit || git(task.worktree, ['status', '--porcelain']).stdout.trim()) refuse('The task changed after its commit.');
76
+ const dest = destination(repo, task);
77
+ const body = prBody(task);
78
+ const title = redact(task.goal).split('\n')[0].slice(0, 120);
79
+ const confirmationId = hash(JSON.stringify({ commit: task.commit, destination: dest, title, body, digest: task.evidence.digest }));
80
+ task.pendingPush = { confirmationId, destination: dest, title, body }; saveTask(repo, task);
81
+ // fullAccess: the owner sees that this task ran without the cap and protected-path checks.
82
+ return { needsConfirmation: true, confirmationId, commit: task.commit, destination: dest, title, body, fullAccess: task.permissions === 'full', nextCommand:`elu run ${task.id} --publish --approve-push ${confirmationId}`, message: 'Publishing uses a vetted absolute hooks snapshot, preserving pre-push guards. Approval is required for every push.' };
83
+ }
84
+ export async function publish(repo, task, confirmationId) {
85
+ const previous = task.pendingPush;
86
+ if (!previous || previous.confirmationId !== confirmationId) refuse('There is no matching push approval. Preview publication first.');
87
+ assertReady(task); assertTaskGitSafe(repo,task);
88
+ assertPublishingHooks(repo,task);
89
+ if (head(task.worktree) !== task.commit || git(task.worktree, ['status', '--porcelain']).stdout.trim()) refuse('The task changed after approval.');
90
+ const currentDestination = destination(repo, task);
91
+ const title = redact(task.goal).split('\n')[0].slice(0, 120);
92
+ const digest = hash(JSON.stringify({ commit: task.commit, destination: currentDestination, title, body: prBody(task), digest: task.evidence.digest }));
93
+ if (digest !== confirmationId) refuse('The destination, PR, or evidence changed. Preview publication again.');
94
+ task.pendingPush = null; saveTask(repo, task);
95
+ git(task.worktree, ['-c',`core.hooksPath=${task.hooksDirectory}`, 'push','--no-follow-tags', 'origin', `${task.commit}:${currentDestination.ref}`], { timeout: 120000 });
96
+ task.pushedCommit = task.commit; saveTask(repo, task); event(repo, 'pushed', { taskId: task.id, commit: task.commit });
97
+ const bodyFile = path.join(repo.state, 'tasks', task.id, 'pr-body.md');
98
+ const { atomic } = await import('./util.mjs'); atomic(bodyFile, prBody(task));
99
+ if (currentDestination.existingPr) gh(repo.root, ['pr', 'edit', String(currentDestination.existingPr.number), '--repo', currentDestination.name, '--body-file', bodyFile]);
100
+ else gh(repo.root, ['pr', 'create', '--repo', currentDestination.name, '--draft', '--base', currentDestination.base, '--head', task.branch, '--title', title, '--body-file', bodyFile]);
101
+ const pr = JSON.parse(gh(repo.root, ['pr', 'view', task.branch, '--repo', currentDestination.name, '--json', 'url,isDraft,headRefOid']));
102
+ if (!pr.isDraft || pr.headRefOid !== task.commit) refuse('The resulting PR does not match the approved draft head.');
103
+ task.pr = pr.url; saveTask(repo, task);
104
+ return { commit: task.commit, pushed: true, pr: task.pr, merged: false, deployed: false };
105
+ }