@atolis-hq/wake 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 (75) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +140 -0
  3. package/dist/src/adapters/claude/claude-runner.js +388 -0
  4. package/dist/src/adapters/claude/prompt-templates.js +57 -0
  5. package/dist/src/adapters/codex/codex-runner.js +391 -0
  6. package/dist/src/adapters/cursor/cursor-runner.js +352 -0
  7. package/dist/src/adapters/docker/docker-cli.js +97 -0
  8. package/dist/src/adapters/fake/fake-artifact-verifier.js +14 -0
  9. package/dist/src/adapters/fake/fake-github-pull-request-activity-source.js +73 -0
  10. package/dist/src/adapters/fake/fake-resource-index.js +21 -0
  11. package/dist/src/adapters/fake/fake-runner.js +22 -0
  12. package/dist/src/adapters/fake/fake-ticketing-system.js +166 -0
  13. package/dist/src/adapters/fake/fake-workspace-manager.js +27 -0
  14. package/dist/src/adapters/fs/resource-index.js +84 -0
  15. package/dist/src/adapters/fs/self-update-ledger.js +17 -0
  16. package/dist/src/adapters/fs/state-store.js +364 -0
  17. package/dist/src/adapters/git/git-workspace-manager.js +168 -0
  18. package/dist/src/adapters/github/github-artifact-verifier.js +38 -0
  19. package/dist/src/adapters/github/github-auth.js +16 -0
  20. package/dist/src/adapters/github/github-client.js +100 -0
  21. package/dist/src/adapters/github/github-issues-work-source.js +410 -0
  22. package/dist/src/adapters/github/github-pull-request-activity-source.js +263 -0
  23. package/dist/src/adapters/http/ui-assets.js +302 -0
  24. package/dist/src/adapters/http/ui-data.js +389 -0
  25. package/dist/src/adapters/http/ui-server.js +151 -0
  26. package/dist/src/adapters/runner/cli-command.js +43 -0
  27. package/dist/src/adapters/runner/prompt-templates.js +76 -0
  28. package/dist/src/adapters/runner/runner-cli-adapter.js +112 -0
  29. package/dist/src/adapters/runner/runner-registry.js +141 -0
  30. package/dist/src/adapters/runner/stage-prompt.js +256 -0
  31. package/dist/src/adapters/runner/transcripts.js +25 -0
  32. package/dist/src/cli/correlate-command.js +62 -0
  33. package/dist/src/cli/init-command.js +11 -0
  34. package/dist/src/cli/locks-command.js +19 -0
  35. package/dist/src/cli/sandbox-command.js +191 -0
  36. package/dist/src/cli/sandbox-logging.js +30 -0
  37. package/dist/src/cli/sandbox-resume.js +77 -0
  38. package/dist/src/cli/scaffold-assets.js +173 -0
  39. package/dist/src/cli/self-update-command.js +158 -0
  40. package/dist/src/cli/startup-preflight.js +127 -0
  41. package/dist/src/cli/stop-command.js +42 -0
  42. package/dist/src/cli/ui-command.js +27 -0
  43. package/dist/src/config/defaults.js +11 -0
  44. package/dist/src/config/load-config.js +26 -0
  45. package/dist/src/core/contracts.js +1 -0
  46. package/dist/src/core/control-plane.js +127 -0
  47. package/dist/src/core/lifecycle-service.js +8 -0
  48. package/dist/src/core/policy-engine.js +200 -0
  49. package/dist/src/core/projection-updater.js +438 -0
  50. package/dist/src/core/quota-backoff.js +69 -0
  51. package/dist/src/core/sink-router.js +85 -0
  52. package/dist/src/core/tick-runner.js +1347 -0
  53. package/dist/src/domain/branch-naming.js +14 -0
  54. package/dist/src/domain/resource-uri.js +38 -0
  55. package/dist/src/domain/runner-routing.js +108 -0
  56. package/dist/src/domain/schema.js +685 -0
  57. package/dist/src/domain/sources.js +16 -0
  58. package/dist/src/domain/stages.js +39 -0
  59. package/dist/src/domain/types.js +1 -0
  60. package/dist/src/domain/workflows.js +83 -0
  61. package/dist/src/lib/clock.js +5 -0
  62. package/dist/src/lib/event-log.js +21 -0
  63. package/dist/src/lib/json-file.js +23 -0
  64. package/dist/src/lib/lock.js +118 -0
  65. package/dist/src/lib/paths.js +44 -0
  66. package/dist/src/lib/work-id.js +19 -0
  67. package/dist/src/main.js +523 -0
  68. package/dist/src/version.js +1 -0
  69. package/docker/Dockerfile +54 -0
  70. package/docker/entrypoint.sh +75 -0
  71. package/docker/log-command.sh +107 -0
  72. package/docker/setup.sh +72 -0
  73. package/package.json +52 -0
  74. package/prompts/implement.md +49 -0
  75. package/prompts/refine.md +44 -0
@@ -0,0 +1,168 @@
1
+ import { execFile as nodeExecFile } from 'node:child_process';
2
+ import { access, mkdir, rm } from 'node:fs/promises';
3
+ import { dirname, join } from 'node:path';
4
+ import { promisify } from 'node:util';
5
+ import { createWakePaths } from '../../lib/paths.js';
6
+ import { branchNameForIssue } from '../../domain/branch-naming.js';
7
+ const execFile = promisify(nodeExecFile);
8
+ // Re-exported for existing callers (this adapter's own use below, plus
9
+ // adapters/runner/stage-prompt.ts and tests) — the canonical definition now
10
+ // lives in domain/branch-naming.ts so core/ can use it without importing a
11
+ // concrete adapter.
12
+ export { branchNameForIssue };
13
+ async function git(args, cwd) {
14
+ const result = await execFile('git', args, {
15
+ cwd,
16
+ env: process.env,
17
+ encoding: 'utf8',
18
+ maxBuffer: 1024 * 1024 * 16,
19
+ });
20
+ return { stdout: result.stdout.trim(), stderr: result.stderr.trim() };
21
+ }
22
+ async function detectDefaultBranch(repoPath) {
23
+ await git(['remote', 'set-head', 'origin', '--auto'], repoPath);
24
+ const { stdout } = await git(['symbolic-ref', '--short', 'refs/remotes/origin/HEAD'], repoPath);
25
+ const defaultBranch = stdout.replace(/^origin\//, '');
26
+ if (defaultBranch.length === 0) {
27
+ throw new Error(`Unable to detect default branch for ${repoPath}`);
28
+ }
29
+ return defaultBranch;
30
+ }
31
+ async function pathExists(path) {
32
+ try {
33
+ await access(path);
34
+ return true;
35
+ }
36
+ catch {
37
+ return false;
38
+ }
39
+ }
40
+ export function defaultRemoteUrlForRepo(repo) {
41
+ return `https://github.com/${repo}.git`;
42
+ }
43
+ export function buildWorkspaceCloneArgs(input) {
44
+ return [
45
+ 'clone',
46
+ '--no-local',
47
+ '--branch',
48
+ input.defaultBranch,
49
+ input.sourceRepoPath,
50
+ input.workspacePath,
51
+ ];
52
+ }
53
+ async function tryUpdateFromDefaultBranch(workspacePath) {
54
+ try {
55
+ const { stdout: status } = await git(['status', '--porcelain'], workspacePath);
56
+ if (status.length > 0) {
57
+ return { mergeConflictDetected: false };
58
+ }
59
+ await git(['fetch', 'origin'], workspacePath);
60
+ const defaultBranch = await detectDefaultBranch(workspacePath);
61
+ const { stdout: count } = await git(['rev-list', '--count', `HEAD..origin/${defaultBranch}`], workspacePath);
62
+ if (parseInt(count.trim(), 10) === 0) {
63
+ return { mergeConflictDetected: false };
64
+ }
65
+ const { stdout: upstreamChanges } = await git([
66
+ 'log',
67
+ '--date=short',
68
+ '--pretty=format:%h %ad %an <%ae>%n %s',
69
+ `HEAD..origin/${defaultBranch}`,
70
+ ], workspacePath);
71
+ // Probe for conflicts without touching the index or worktree. A real merge
72
+ // would need committer identity even with --no-commit on some Git versions.
73
+ try {
74
+ await git(['merge-tree', '--write-tree', 'HEAD', `origin/${defaultBranch}`], workspacePath);
75
+ await git([
76
+ '-c',
77
+ 'user.email=wake@example.invalid',
78
+ '-c',
79
+ 'user.name=Wake',
80
+ 'merge',
81
+ '--no-edit',
82
+ `origin/${defaultBranch}`,
83
+ ], workspacePath);
84
+ return {
85
+ mergeConflictDetected: false,
86
+ ...(upstreamChanges.length === 0 ? {} : { upstreamChanges }),
87
+ };
88
+ }
89
+ catch {
90
+ return { mergeConflictDetected: true };
91
+ }
92
+ }
93
+ catch {
94
+ // Fetch or branch detection failed — leave workspace as-is, no conflict reported
95
+ return { mergeConflictDetected: false };
96
+ }
97
+ }
98
+ export function createGitWorkspaceManager(options) {
99
+ const paths = createWakePaths(options.wakeRoot);
100
+ const remoteUrlForRepo = options.remoteUrlForRepo ?? defaultRemoteUrlForRepo;
101
+ async function ensureCanonicalClone(repo) {
102
+ const repoPath = paths.repoRoot(repo);
103
+ const remoteUrl = remoteUrlForRepo(repo);
104
+ if (await pathExists(repoPath)) {
105
+ // The canonical clone is only ever touched by one tick at a time, so an
106
+ // index.lock found here can't be a live concurrent writer - it's a leftover
107
+ // from a process that was killed mid-git-operation (e.g. a container
108
+ // restart). Left in place it wedges every future tick on this repo with
109
+ // "Unable to create index.lock: File exists", so clear it defensively
110
+ // before running any git command against the clone.
111
+ await rm(join(repoPath, '.git', 'index.lock'), { force: true });
112
+ await git(['fetch', 'origin'], repoPath);
113
+ const defaultBranch = await detectDefaultBranch(repoPath);
114
+ await git(['checkout', defaultBranch], repoPath);
115
+ await git(['reset', '--hard', `origin/${defaultBranch}`], repoPath);
116
+ await git(['clean', '-fdx'], repoPath);
117
+ return { repoPath, defaultBranch };
118
+ }
119
+ else {
120
+ await mkdir(dirname(repoPath), { recursive: true });
121
+ await git(['clone', remoteUrl, repoPath], dirname(repoPath));
122
+ const defaultBranch = await detectDefaultBranch(repoPath);
123
+ return { repoPath, defaultBranch };
124
+ }
125
+ }
126
+ return {
127
+ async prepareWorkspace({ workId, repo, issueNumber, }) {
128
+ // The workspace is keyed by work item; `repo` still drives the clone and
129
+ // `issueNumber` still names the branch, which stays human-readable and
130
+ // provider-facing (spec D2).
131
+ const workspacePath = paths.workspaceDir(workId);
132
+ if (await pathExists(workspacePath)) {
133
+ const updateResult = await tryUpdateFromDefaultBranch(workspacePath);
134
+ return { workspacePath, ...updateResult };
135
+ }
136
+ const { repoPath, defaultBranch } = await ensureCanonicalClone(repo);
137
+ const remoteUrl = remoteUrlForRepo(repo);
138
+ await mkdir(dirname(workspacePath), { recursive: true });
139
+ await git(buildWorkspaceCloneArgs({
140
+ sourceRepoPath: repoPath,
141
+ workspacePath,
142
+ defaultBranch,
143
+ }), dirname(workspacePath));
144
+ const branch = branchNameForIssue(issueNumber);
145
+ await git(['remote', 'set-url', 'origin', remoteUrl], workspacePath);
146
+ await git(['checkout', '-B', branch], workspacePath);
147
+ return { workspacePath, mergeConflictDetected: false };
148
+ },
149
+ async prepareReadOnlyClone({ repo }) {
150
+ // Refine only reads the issue and, at most, the canonical clone -
151
+ // it never gets a per-issue branch/workspace of its own (only
152
+ // 'implement' pays that cost).
153
+ const { repoPath } = await ensureCanonicalClone(repo);
154
+ return { workspacePath: repoPath };
155
+ },
156
+ async cleanupWorkspace({ workspacePath }) {
157
+ // On Windows, a just-exited git subprocess (or AV/indexer) can hold a brief
158
+ // handle on files it touched; a bare rm races that and fails EBUSY/EPERM.
159
+ // maxRetries/retryDelay make fs.rm retry with backoff instead of throwing.
160
+ await rm(workspacePath, {
161
+ recursive: true,
162
+ force: true,
163
+ maxRetries: 5,
164
+ retryDelay: 200,
165
+ });
166
+ },
167
+ };
168
+ }
@@ -0,0 +1,38 @@
1
+ import { buildResourceUri } from '../../domain/resource-uri.js';
2
+ const githubPrUrlPattern = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/;
3
+ export function createGitHubArtifactVerifier(deps) {
4
+ return {
5
+ async verify(artifact, context) {
6
+ if (artifact.kind !== 'pr') {
7
+ return null;
8
+ }
9
+ const match = githubPrUrlPattern.exec(artifact.url);
10
+ if (match === null) {
11
+ return null;
12
+ }
13
+ const [, owner, repo, numberStr] = match;
14
+ if (owner === undefined || repo === undefined || numberStr === undefined) {
15
+ return null;
16
+ }
17
+ // GitHub owner/repo names are case-insensitive, so the URL's rendered
18
+ // casing (owner/repo, as linked by the agent) can legitimately differ
19
+ // from the configured context.repo string.
20
+ if (`${owner}/${repo}`.toLowerCase() !== context.repo.toLowerCase()) {
21
+ return null;
22
+ }
23
+ try {
24
+ const pr = await deps.client.getPullRequest(owner, repo, Number(numberStr));
25
+ if (pr.head.ref !== context.branch) {
26
+ return null;
27
+ }
28
+ // Built from context.repo, not the URL's owner/repo casing, so this
29
+ // resourceUri exact-matches the one discoverPullRequests/pollWatchedPr
30
+ // build from the same configured repo string.
31
+ return { resourceUri: buildResourceUri('github', 'pr', `${context.repo}#${numberStr}`) };
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ },
37
+ };
38
+ }
@@ -0,0 +1,16 @@
1
+ import { execFile as nodeExecFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ const execFile = promisify(nodeExecFile);
4
+ export async function resolveGitHubToken(deps) {
5
+ try {
6
+ const result = await (deps?.execFile ?? execFile)('gh', ['auth', 'token']);
7
+ const token = String(result.stdout).trim();
8
+ if (token.length === 0) {
9
+ throw new Error('empty token');
10
+ }
11
+ return token;
12
+ }
13
+ catch (error) {
14
+ throw new Error(`Failed to resolve GitHub token via gh auth token: ${error instanceof Error ? error.message : String(error)}`);
15
+ }
16
+ }
@@ -0,0 +1,100 @@
1
+ import { Octokit } from '@octokit/rest';
2
+ export function createGitHubClient(token) {
3
+ const octokit = new Octokit({ auth: token });
4
+ return {
5
+ // `maxResults` is a hard cap on issues returned, not just a page size:
6
+ // octokit.paginate otherwise walks every page regardless of page size,
7
+ // which burns GitHub's rate limit (a "fourth budget") on repos with many
8
+ // issues. Stop paginating as soon as the cap is reached.
9
+ async listIssues(owner, repo, maxResults, since) {
10
+ const perPage = Math.min(maxResults, 100);
11
+ const results = [];
12
+ for await (const { data } of octokit.paginate.iterator(octokit.rest.issues.listForRepo, {
13
+ owner,
14
+ repo,
15
+ state: 'all',
16
+ per_page: perPage,
17
+ ...(since === undefined ? {} : { since }),
18
+ })) {
19
+ results.push(...data);
20
+ if (results.length >= maxResults) {
21
+ break;
22
+ }
23
+ }
24
+ return results.slice(0, maxResults);
25
+ },
26
+ async listComments(owner, repo, issueNumber, perPage) {
27
+ return octokit.paginate(octokit.rest.issues.listComments, {
28
+ owner,
29
+ repo,
30
+ issue_number: issueNumber,
31
+ per_page: perPage,
32
+ });
33
+ },
34
+ async createComment(owner, repo, issueNumber, body) {
35
+ return octokit.rest.issues.createComment({
36
+ owner,
37
+ repo,
38
+ issue_number: issueNumber,
39
+ body,
40
+ });
41
+ },
42
+ async setLabels(owner, repo, issueNumber, labels) {
43
+ return octokit.rest.issues.setLabels({
44
+ owner,
45
+ repo,
46
+ issue_number: issueNumber,
47
+ labels,
48
+ });
49
+ },
50
+ async getPullRequest(owner, repo, pullNumber) {
51
+ const { data } = await octokit.rest.pulls.get({
52
+ owner,
53
+ repo,
54
+ pull_number: pullNumber,
55
+ });
56
+ return data;
57
+ },
58
+ async listPullRequests(owner, repo, maxResults) {
59
+ const perPage = Math.min(maxResults, 100);
60
+ const results = [];
61
+ for await (const { data } of octokit.paginate.iterator(octokit.rest.pulls.list, {
62
+ owner,
63
+ repo,
64
+ state: 'open',
65
+ per_page: perPage,
66
+ })) {
67
+ results.push(...data);
68
+ if (results.length >= maxResults) {
69
+ break;
70
+ }
71
+ }
72
+ return results.slice(0, maxResults);
73
+ },
74
+ async listReviews(owner, repo, pullNumber, perPage) {
75
+ return octokit.paginate(octokit.rest.pulls.listReviews, {
76
+ owner,
77
+ repo,
78
+ pull_number: pullNumber,
79
+ per_page: perPage,
80
+ });
81
+ },
82
+ async listReviewComments(owner, repo, pullNumber, perPage) {
83
+ return octokit.paginate(octokit.rest.pulls.listReviewComments, {
84
+ owner,
85
+ repo,
86
+ pull_number: pullNumber,
87
+ per_page: perPage,
88
+ });
89
+ },
90
+ async replyToReviewComment(owner, repo, pullNumber, commentId, body) {
91
+ return octokit.rest.pulls.createReplyForReviewComment({
92
+ owner,
93
+ repo,
94
+ pull_number: pullNumber,
95
+ comment_id: commentId,
96
+ body,
97
+ });
98
+ },
99
+ };
100
+ }