@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.
- package/LICENSE +201 -0
- package/README.md +140 -0
- package/dist/src/adapters/claude/claude-runner.js +388 -0
- package/dist/src/adapters/claude/prompt-templates.js +57 -0
- package/dist/src/adapters/codex/codex-runner.js +391 -0
- package/dist/src/adapters/cursor/cursor-runner.js +352 -0
- package/dist/src/adapters/docker/docker-cli.js +97 -0
- package/dist/src/adapters/fake/fake-artifact-verifier.js +14 -0
- package/dist/src/adapters/fake/fake-github-pull-request-activity-source.js +73 -0
- package/dist/src/adapters/fake/fake-resource-index.js +21 -0
- package/dist/src/adapters/fake/fake-runner.js +22 -0
- package/dist/src/adapters/fake/fake-ticketing-system.js +166 -0
- package/dist/src/adapters/fake/fake-workspace-manager.js +27 -0
- package/dist/src/adapters/fs/resource-index.js +84 -0
- package/dist/src/adapters/fs/self-update-ledger.js +17 -0
- package/dist/src/adapters/fs/state-store.js +364 -0
- package/dist/src/adapters/git/git-workspace-manager.js +168 -0
- package/dist/src/adapters/github/github-artifact-verifier.js +38 -0
- package/dist/src/adapters/github/github-auth.js +16 -0
- package/dist/src/adapters/github/github-client.js +100 -0
- package/dist/src/adapters/github/github-issues-work-source.js +410 -0
- package/dist/src/adapters/github/github-pull-request-activity-source.js +263 -0
- package/dist/src/adapters/http/ui-assets.js +302 -0
- package/dist/src/adapters/http/ui-data.js +389 -0
- package/dist/src/adapters/http/ui-server.js +151 -0
- package/dist/src/adapters/runner/cli-command.js +43 -0
- package/dist/src/adapters/runner/prompt-templates.js +76 -0
- package/dist/src/adapters/runner/runner-cli-adapter.js +112 -0
- package/dist/src/adapters/runner/runner-registry.js +141 -0
- package/dist/src/adapters/runner/stage-prompt.js +256 -0
- package/dist/src/adapters/runner/transcripts.js +25 -0
- package/dist/src/cli/correlate-command.js +62 -0
- package/dist/src/cli/init-command.js +11 -0
- package/dist/src/cli/locks-command.js +19 -0
- package/dist/src/cli/sandbox-command.js +191 -0
- package/dist/src/cli/sandbox-logging.js +30 -0
- package/dist/src/cli/sandbox-resume.js +77 -0
- package/dist/src/cli/scaffold-assets.js +173 -0
- package/dist/src/cli/self-update-command.js +158 -0
- package/dist/src/cli/startup-preflight.js +127 -0
- package/dist/src/cli/stop-command.js +42 -0
- package/dist/src/cli/ui-command.js +27 -0
- package/dist/src/config/defaults.js +11 -0
- package/dist/src/config/load-config.js +26 -0
- package/dist/src/core/contracts.js +1 -0
- package/dist/src/core/control-plane.js +127 -0
- package/dist/src/core/lifecycle-service.js +8 -0
- package/dist/src/core/policy-engine.js +200 -0
- package/dist/src/core/projection-updater.js +438 -0
- package/dist/src/core/quota-backoff.js +69 -0
- package/dist/src/core/sink-router.js +85 -0
- package/dist/src/core/tick-runner.js +1347 -0
- package/dist/src/domain/branch-naming.js +14 -0
- package/dist/src/domain/resource-uri.js +38 -0
- package/dist/src/domain/runner-routing.js +108 -0
- package/dist/src/domain/schema.js +685 -0
- package/dist/src/domain/sources.js +16 -0
- package/dist/src/domain/stages.js +39 -0
- package/dist/src/domain/types.js +1 -0
- package/dist/src/domain/workflows.js +83 -0
- package/dist/src/lib/clock.js +5 -0
- package/dist/src/lib/event-log.js +21 -0
- package/dist/src/lib/json-file.js +23 -0
- package/dist/src/lib/lock.js +118 -0
- package/dist/src/lib/paths.js +44 -0
- package/dist/src/lib/work-id.js +19 -0
- package/dist/src/main.js +523 -0
- package/dist/src/version.js +1 -0
- package/docker/Dockerfile +54 -0
- package/docker/entrypoint.sh +75 -0
- package/docker/log-command.sh +107 -0
- package/docker/setup.sh +72 -0
- package/package.json +52 -0
- package/prompts/implement.md +49 -0
- 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
|
+
}
|