breakaway 1.0.2-main.2 → 1.0.2-main.20
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 +1 -1
- package/package.json +1 -1
- package/scripts/tasks/cli.js +40 -1
- package/scripts/tasks/hook-config.js +32 -4
- package/scripts/tasks/init.js +133 -16
- package/scripts/tasks/message-wait.mjs +3 -3
- package/scripts/tasks/proxy.js +90 -0
- package/scripts/tasks/session-hook.mjs +35 -21
- package/scripts/tasks/session-messages.js +7 -0
- package/scripts/tasks.mjs +38 -2
- package/src/cli-version.js +2 -3
- package/src/install.js +1 -1
package/README.md
CHANGED
|
@@ -171,7 +171,7 @@ pnpm interop # checks sync against real Taskwarrior 3
|
|
|
171
171
|
|
|
172
172
|
breakaway publishes releases and never deploys an install. Every install, the owner's included, deploys a release from its own repository, and this repository holds no Cloudflare credentials. Its website follows the latest stable release: the release moves the `site` branch, and Cloudflare deploys it ([`site/README.md`](https://github.com/TheAnarchoX/breakaway/blob/main/site/README.md#deploy-it)).
|
|
173
173
|
|
|
174
|
-
- **Every merge to `main`**, once CI passes, publishes a GitHub pre-release `vX.Y.Z-main.N` on the `main` channel. It carries the bundle (`breakaway-bundle.tar.gz`: the Worker's files and the web app's `dist`), a `manifest.json` (version, channel, commit, `manual`, and the lowest version it updates from), and `SHA256SUMS`. The notes list the merged pull requests by title.
|
|
174
|
+
- **Every merge to `main`**, once CI passes, publishes a GitHub pre-release `vX.Y.Z-main.N` on the `main` channel. It carries the bundle (`breakaway-bundle.tar.gz`: the Worker's files and the web app's `dist`), a `manifest.json` (version, channel, commit, `manual`, and the lowest version it updates from), its signature `manifest.json.sig` (Ed25519, made with a key only the release workflow holds; the public key is `src/release-key.js`), and `SHA256SUMS`. The notes list the merged pull requests by title.
|
|
175
175
|
- **A stable release** `vX.Y.Z` is the owner's: they run the **Release** workflow with the pre-release to promote. The bundle is that pre-release's, unchanged, and the notes cover everything since the last stable.
|
|
176
176
|
- **The CLI** is on npm as [`breakaway`](https://www.npmjs.com/package/breakaway), staged on npm by the same workflow, with provenance, and live once the owner approves it there with 2FA. npm's trusted publishing can't yet read the OIDC identity of a repository as new as this one ([npm/cli#9969](https://github.com/npm/cli/issues/9969)), so until it can, a token that can stage but never publish by itself stands in, in an environment only `main` can use. Every pre-release goes out under the `next` dist-tag, and a stable release as `latest`. `npx breakaway <command>` is `node scripts/tasks.mjs <command>`.
|
|
177
177
|
- **A major release** is one where an install has to do something by hand: a config or binding change, a Durable Object class or migration, a route or cron. Its notes have a **Manual steps** section and its manifest says `manual: true`, which an install's deploy stops on. A change that needs it sets `manual` and `manualSteps` in `release.json`, and the pull request that ships the steps clears them. Data the Durable Object stores changes forward-only and additively, so an install can always go back one release.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "breakaway",
|
|
3
|
-
"version": "1.0.2-main.
|
|
3
|
+
"version": "1.0.2-main.20",
|
|
4
4
|
"description": "The task board for you and your coding agents: a Cloudflare Worker, its web app, Taskwarrior sync, and the CLI (npx breakaway).",
|
|
5
5
|
"license": "FSL-1.1-Apache-2.0",
|
|
6
6
|
"type": "module",
|
package/scripts/tasks/cli.js
CHANGED
|
@@ -7,6 +7,7 @@ import { CLI_PACKAGE } from './init.js';
|
|
|
7
7
|
/** The subcommands each command knows. Without one, each lists or shows (horizon needs close). */
|
|
8
8
|
export const SUBCOMMANDS = {
|
|
9
9
|
agents: ['next', 'start', 'refine'],
|
|
10
|
+
github: ['fix', 'review'],
|
|
10
11
|
repos: ['add', 'init', 'modify', 'remove', 'setup'],
|
|
11
12
|
routines: ['add', 'modify', 'run', 'trigger', 'revoke', 'pause', 'resume'],
|
|
12
13
|
horizon: ['close'],
|
|
@@ -18,7 +19,6 @@ export const NO_ARGUMENTS = new Set([
|
|
|
18
19
|
'list',
|
|
19
20
|
'next',
|
|
20
21
|
'activity',
|
|
21
|
-
'github',
|
|
22
22
|
'health',
|
|
23
23
|
'connections',
|
|
24
24
|
'export',
|
|
@@ -74,6 +74,45 @@ export function githubRequest(repo, { sync = false } = {}) {
|
|
|
74
74
|
return ['GET', repo ? `github?repo=${encodeURIComponent(repo)}` : 'github', undefined];
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/** What `github fix` accepts for --problem: the same three the pull request page offers. */
|
|
78
|
+
export const FIX_PROBLEMS = ['conflicts', 'failing', 'review'];
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The request behind `npx breakaway github fix <n>` and `github review <n>` (BRK-81): the pull request page's "Fix with an
|
|
82
|
+
* agent" and "Safe to merge?" buttons (`POST github/pulls/<n>/fix` and `/review`). It names the checkout's repository
|
|
83
|
+
* like `github` does. Returns an error message instead when the number or `problem` can't be right.
|
|
84
|
+
* @param {'fix' | 'review'} action
|
|
85
|
+
* @param {string | number | undefined} number
|
|
86
|
+
* @param {{ repo?: string | null, problem?: string, note?: string }} [options]
|
|
87
|
+
* @returns {{ error?: string, request?: [string, string, Record<string, string>] }}
|
|
88
|
+
*/
|
|
89
|
+
export function pullAgentRequest(action, number, { repo = null, problem, note } = {}) {
|
|
90
|
+
const n = String(number ?? '').replace(/^#/u, '');
|
|
91
|
+
if (!/^[1-9]\d{0,8}$/u.test(n)) return { error: `say which pull request: npx breakaway github ${action} <number>` };
|
|
92
|
+
if (problem !== undefined && action !== 'fix') return { error: '--problem is for github fix' };
|
|
93
|
+
if (problem !== undefined && !FIX_PROBLEMS.includes(problem))
|
|
94
|
+
return { error: `--problem is ${FIX_PROBLEMS.join(', ')}` };
|
|
95
|
+
const body = {
|
|
96
|
+
...(repo ? { repo } : {}),
|
|
97
|
+
...(problem ? { problem } : {}),
|
|
98
|
+
...(typeof note === 'string' && note.trim() ? { note } : {}),
|
|
99
|
+
};
|
|
100
|
+
return { request: ['POST', `github/pulls/${n}/${action}`, body] };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* What the CLI says about an answer to those requests: which task and agent took the pull request, or who already has it.
|
|
105
|
+
* @param {'fix' | 'review'} action
|
|
106
|
+
* @param {string | number} number
|
|
107
|
+
* @param {{ task: { wid?: string, short?: string }, run?: { url?: string, agent?: string } | null, already?: string | null }} answer
|
|
108
|
+
*/
|
|
109
|
+
export function pullAgentSummary(action, number, { task, run, already }) {
|
|
110
|
+
const id = task.wid ?? task.short;
|
|
111
|
+
if (!run) return `${id} already has it: ${already}.`;
|
|
112
|
+
const what = action === 'fix' ? `fixing #${number}` : `testing #${number}`;
|
|
113
|
+
return `Started ${run.agent ? `${run.agent}, ` : 'an agent '}${what} on ${id}${run.url ? `: ${run.url}` : ''}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
77
116
|
/**
|
|
78
117
|
* The task an idea becomes (`npx breakaway idea`): its first line, kept short, as the title, and the whole text as its
|
|
79
118
|
* description. Like `add`, it lands in the repository of the checkout it was written in (BRK-71): without one, the
|
|
@@ -3,14 +3,32 @@
|
|
|
3
3
|
* `tasks claim`) and where the board is, found the same way as the CLI (scripts/tasks/settings.js).
|
|
4
4
|
* Each returns null/undefined rather than throwing when something's missing, because the hooks stay quiet.
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import { execFileSync } from 'node:child_process';
|
|
7
|
+
import { existsSync, readFileSync, rmSync } from 'node:fs';
|
|
7
8
|
import { homedir } from 'node:os';
|
|
8
9
|
import { join } from 'node:path';
|
|
9
10
|
import { boardUrl, configDir, parseEnvFile, readSetting } from './settings.js';
|
|
10
11
|
|
|
11
|
-
/** The
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
/** The git root of the current folder, or null outside a repository. */
|
|
13
|
+
function gitRoot() {
|
|
14
|
+
try {
|
|
15
|
+
return (
|
|
16
|
+
execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
17
|
+
encoding: 'utf8',
|
|
18
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
19
|
+
}).trim() || null
|
|
20
|
+
);
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The project's root: as Claude Code gives it to hooks, and where it doesn't (a cloud session can leave
|
|
28
|
+
* CLAUDE_PROJECT_DIR empty, BRK-88), the git root of the folder the hook runs in, where `tasks claim` marks the task.
|
|
29
|
+
*/
|
|
30
|
+
export function projectRoot(env = process.env, root = gitRoot, cwd = process.cwd()) {
|
|
31
|
+
return env.CLAUDE_PROJECT_DIR || root() || cwd;
|
|
14
32
|
}
|
|
15
33
|
|
|
16
34
|
/** A file's text, or null when it can't be read. */
|
|
@@ -55,3 +73,13 @@ export function boardConfig(root = projectRoot()) {
|
|
|
55
73
|
const token = readSetting('TOKEN', { env: process.env, file });
|
|
56
74
|
return { base: url, headers: token ? { Authorization: `Bearer ${token}` } : {} };
|
|
57
75
|
}
|
|
76
|
+
|
|
77
|
+
/** The board says this checkout's task isn't claimed by its agent any more: drop the marker so later calls post nothing (BRK-87). */
|
|
78
|
+
export function dropClaim(claim, root = projectRoot()) {
|
|
79
|
+
try {
|
|
80
|
+
const marker = join(root, '.task-session');
|
|
81
|
+
if (JSON.parse(readFileSync(marker, 'utf8'))?.uuid === claim.uuid) rmSync(marker);
|
|
82
|
+
} catch {
|
|
83
|
+
/* already gone */
|
|
84
|
+
}
|
|
85
|
+
}
|
package/scripts/tasks/init.js
CHANGED
|
@@ -14,10 +14,10 @@ const DEFAULT_DIR = '~/.config/breakaway';
|
|
|
14
14
|
export { routinePrompt };
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
* The CLI on npm (BRK-7), as a repository runs it: `npx breakaway
|
|
18
|
-
*
|
|
17
|
+
* The CLI on npm (BRK-7), as a repository runs it: `npx breakaway`, pinned to the major (BRK-47), so a breaking change
|
|
18
|
+
* never reaches a repository by itself. It was the `next` channel until the first stable release.
|
|
19
19
|
*/
|
|
20
|
-
export const CLI_PACKAGE = 'breakaway@
|
|
20
|
+
export const CLI_PACKAGE = 'breakaway@1';
|
|
21
21
|
/**
|
|
22
22
|
* Where the CLI starts: the command, and the two session hooks. repos init no longer copies them (BRK-7): an old copy
|
|
23
23
|
* is replaced by npx, and these still version the CLI and say which files an old copy holds.
|
|
@@ -49,6 +49,11 @@ const ADAPTED = ['taskrc', 'scripts/task', SKILL];
|
|
|
49
49
|
* sit at the root, so the paths `read` takes are the board's; what is written keeps this folder.
|
|
50
50
|
*/
|
|
51
51
|
const TARGET_DIR = 'tools/tasks/';
|
|
52
|
+
/**
|
|
53
|
+
* The record of what repos init wrote into a repository (BRK-79): `--update` replaces a copied file only when it's
|
|
54
|
+
* listed here, or sits in TARGET_DIR, breakaway's own folder. Anything else at a path it copies to is the repository's.
|
|
55
|
+
*/
|
|
56
|
+
export const MANIFEST = `${TARGET_DIR}copied.json`;
|
|
52
57
|
const GITIGNORE = ['.task/', '.task-session', '.env'];
|
|
53
58
|
/** Pinned to LF so the shell scripts run on a checkout with core.autocrlf=true (BRK-41). */
|
|
54
59
|
const GITATTRIBUTES = ['scripts/task text eol=lf', '.envrc text eol=lf'];
|
|
@@ -134,7 +139,10 @@ export function sessionHooks(pkg = CLI_PACKAGE) {
|
|
|
134
139
|
};
|
|
135
140
|
}
|
|
136
141
|
|
|
137
|
-
/**
|
|
142
|
+
/**
|
|
143
|
+
* settings.json with the session hooks' commands set to the current one, whichever form they had: the copy's, or npx
|
|
144
|
+
* with an earlier channel or version of the package (`breakaway@next` before BRK-47). The rest is untouched.
|
|
145
|
+
*/
|
|
138
146
|
export function rewireHooks(text) {
|
|
139
147
|
let out = text;
|
|
140
148
|
for (const name of ['session', 'wait'])
|
|
@@ -143,7 +151,28 @@ export function rewireHooks(text) {
|
|
|
143
151
|
const now = JSON.stringify(hookCommand(name)).slice(1, -1);
|
|
144
152
|
out = out.split(was).join(now);
|
|
145
153
|
}
|
|
146
|
-
|
|
154
|
+
const current = (_, name) => JSON.stringify(hookCommand(name === 'message-wait' ? 'wait' : name)).slice(1, -1);
|
|
155
|
+
return (
|
|
156
|
+
out
|
|
157
|
+
.replace(/npx --yes breakaway(?:@[^\s"\\]+)? hook (session|wait)\b/gu, current)
|
|
158
|
+
// The old CLI copy's hook scripts, from before BRK-7, quoted or not (BRK-79): its copy is about to go.
|
|
159
|
+
.replace(
|
|
160
|
+
/node (\\")?(?:\$CLAUDE_PROJECT_DIR\/)?(?:tools\/tasks\/cli\/)?scripts\/tasks\/(session-hook|message-wait)\.mjs\1/gu,
|
|
161
|
+
(_, _q, name) => current(_, name === 'session-hook' ? 'session' : name),
|
|
162
|
+
)
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The paths a repository's AGENTS.md says came from the board, in its "Copied files." bullet as any version of repos
|
|
168
|
+
* init wrote it; a folder ends in `/`. None when AGENTS.md is the repository's own (BRK-79).
|
|
169
|
+
*/
|
|
170
|
+
export function declaredCopies(agents) {
|
|
171
|
+
const line = String(agents ?? '')
|
|
172
|
+
.split('\n')
|
|
173
|
+
.find((l) => /^- \*\*Copied files\.\*\*/u.test(l));
|
|
174
|
+
if (!line) return [];
|
|
175
|
+
return [...line.split(/ come from \[/u)[0].matchAll(/`([^`]+)`/gu)].map((m) => m[1]);
|
|
147
176
|
}
|
|
148
177
|
|
|
149
178
|
/** A short SHA-256 of `paths` and their text: changes whenever one of them does. */
|
|
@@ -261,10 +290,20 @@ export function promptSections(given = {}) {
|
|
|
261
290
|
}
|
|
262
291
|
|
|
263
292
|
/** The `tasks` skill for another repository: links into the board's docs point at them on GitHub. */
|
|
264
|
-
export function skillFor(text, board) {
|
|
265
|
-
|
|
293
|
+
export function skillFor(text, board, repo = null) {
|
|
294
|
+
const linked = String(text)
|
|
266
295
|
.replace(/\]\((?:\.\.\/)+((?:docs|tools)\/[^)]+)\)/gu, `](https://github.com/${board}/blob/main/$1)`)
|
|
267
296
|
.replace(/\]\(((?:\.\.\/)+)prompts\//gu, ']($1tools/tasks/prompts/');
|
|
297
|
+
if (!repo) return linked;
|
|
298
|
+
// The skill is breakaway's own: in another repository it names that repository's work, areas, and prompt (BRK-79).
|
|
299
|
+
const prompt = promptPathOf(repo);
|
|
300
|
+
return linked
|
|
301
|
+
.replace(/breakaway's work is on the board/gu, "This repository's work is on the board")
|
|
302
|
+
.replace(/breakaway's areas: [^.\n]+\./gu, `This repository's areas: ${areaList(repo)}.`)
|
|
303
|
+
.replace(
|
|
304
|
+
/\[`prompts\/breakaway\.md`\]\(((?:\.\.\/)+)tools\/tasks\/prompts\/breakaway\.md\)/gu,
|
|
305
|
+
(_, up) => `[\`${prompt}\`](${up}${prompt})`,
|
|
306
|
+
);
|
|
268
307
|
}
|
|
269
308
|
|
|
270
309
|
/** A starter AGENTS.md: how this repository works with the board. The owner adds how to build here. */
|
|
@@ -273,9 +312,9 @@ export function agentsMd(repo, board, dir = DEFAULT_DIR) {
|
|
|
273
312
|
|
|
274
313
|
<!-- Started by \`npx breakaway repos init\` (breakaway's task board). Add how to build here: setup, tests, style, and anything agents must never do. -->
|
|
275
314
|
|
|
276
|
-
- **Work lives on the task board.** This repository's tasks are in the areas ${areaList(repo)}. Use the \`tasks\` skill (\`${SKILL}\`) and the CLI, \`npx breakaway\` (the \`breakaway\` package on npm), to claim, comment, and hand over. It works in this checkout's repository, so \`list\` and \`next\` show only this repository's tasks. The skill is breakaway's
|
|
315
|
+
- **Work lives on the task board.** This repository's tasks are in the areas ${areaList(repo)}. Use the \`tasks\` skill (\`${SKILL}\`) and the CLI, \`npx breakaway\` (the \`breakaway\` package on npm), to claim, comment, and hand over. It works in this checkout's repository, so \`list\` and \`next\` show only this repository's tasks. The skill is breakaway's, written for this repository's areas and prompt: where it names breakaway's own files or rules, the board's part applies and the rest doesn't.
|
|
277
316
|
- **Agents started by the board** follow [\`${promptPathOf(repo)}\`](${promptPathOf(repo)}), which starts with the board's core, \`tools/tasks/prompts/core.md\`.
|
|
278
|
-
- **Copied files.** \`tools/tasks/\`, the release helpers in \`scripts/\`, and \`${SKILL}\` come from [${board}](https://github.com/${board}). Don't edit them here: change them there. \`.claude/settings.json\` holds the session hooks that show a cloud agent's output on its task, and they run through \`npx\`, so this repository carries no copy of the CLI.
|
|
317
|
+
- **Copied files.** \`tools/tasks/\`, the release helpers in \`scripts/\`, and \`${SKILL}\` come from [${board}](https://github.com/${board}). Don't edit them here: change them there. \`${MANIFEST}\` lists every file it copied, and \`repos init --update\` replaces only those: a file it doesn't list is this repository's own, even at a path breakaway copies to. \`.claude/settings.json\` holds the session hooks that show a cloud agent's output on its task, and they run through \`npx\`, so this repository carries no copy of the CLI.
|
|
279
318
|
- **Taskwarrior** (optional): \`scripts/task\`, or plain \`task\` with direnv after \`direnv allow\`, uses the board with this checkout's own \`.task/\` database, in the \`${repo.slug}\` context. \`npx breakaway setup\` connects the machine once.
|
|
280
319
|
- **Changes reach \`${repo.defaultBranch || 'main'}\` through pull requests**, which the owner merges. Never merge, force-push, or rewrite \`${repo.defaultBranch || 'main'}\`.
|
|
281
320
|
- **Never put a secret or token** in a file, task, comment, or pull request. The board's token lives in \`${dir}/tasks.env\` (or \`$BREAKAWAY_HOME/tasks.env\`) or the cloud environment's credentials, never in this repository.
|
|
@@ -338,13 +377,46 @@ export function initPlan({
|
|
|
338
377
|
if (readTarget(path) !== null) skipped.push(path);
|
|
339
378
|
else files.push({ path, content, ...extra });
|
|
340
379
|
};
|
|
341
|
-
|
|
380
|
+
// What an earlier run recorded writing (BRK-79): null for a repository set up before the record existed.
|
|
381
|
+
const recorded = (() => {
|
|
382
|
+
try {
|
|
383
|
+
const files = JSON.parse(readTarget(MANIFEST) ?? 'null')?.files;
|
|
384
|
+
return Array.isArray(files) ? new Set(files) : null;
|
|
385
|
+
} catch {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
})();
|
|
389
|
+
// Set up before the record: the AGENTS.md repos init wrote says the copied files came from the board, and a
|
|
390
|
+
// repository's own AGENTS.md doesn't, so only then are files at the copied paths breakaway's.
|
|
391
|
+
const declared = recorded === null ? declaredCopies(readTarget('AGENTS.md')) : [];
|
|
392
|
+
/** Whether repos init wrote `path` here: in breakaway's own folder, on the record, or in AGENTS.md's copied files. */
|
|
393
|
+
const owns = (path) =>
|
|
394
|
+
path.startsWith(TARGET_DIR) ||
|
|
395
|
+
Boolean(recorded?.has(path)) ||
|
|
396
|
+
declared.some((d) => (d.endsWith('/') ? path.startsWith(d) : path === d));
|
|
397
|
+
const ours = new Set();
|
|
398
|
+
const theirs = [];
|
|
399
|
+
/**
|
|
400
|
+
* A file copied from the board: added when it's missing, and with `update`, replaced when it differs, but only when
|
|
401
|
+
* repos init wrote it (the record lists it, or it's in breakaway's own folder). A repository's own file at a path
|
|
402
|
+
* breakaway copies to is left alone (BRK-79).
|
|
403
|
+
*/
|
|
342
404
|
const copy = (path, content, extra = {}) => {
|
|
343
405
|
const there = readTarget(path);
|
|
344
|
-
if (there === null)
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
else
|
|
406
|
+
if (there === null) {
|
|
407
|
+
files.push({ path, content, ...extra });
|
|
408
|
+
ours.add(path);
|
|
409
|
+
} else if (there === content) {
|
|
410
|
+
(update ? current : skipped).push(path);
|
|
411
|
+
ours.add(path);
|
|
412
|
+
} else if (!update) skipped.push(path);
|
|
413
|
+
else if (owns(path)) {
|
|
414
|
+
files.push({ path, content, ...extra, changed: true });
|
|
415
|
+
ours.add(path);
|
|
416
|
+
} else {
|
|
417
|
+
skipped.push(path);
|
|
418
|
+
theirs.push(path);
|
|
419
|
+
}
|
|
348
420
|
};
|
|
349
421
|
|
|
350
422
|
const prompt = promptPathOf(repo);
|
|
@@ -357,7 +429,14 @@ export function initPlan({
|
|
|
357
429
|
}
|
|
358
430
|
for (const path of COPIED) copy(TARGET_DIR + path, read(path));
|
|
359
431
|
copy(`${TARGET_DIR}taskrc`, withRepoInTaskrc(read('taskrc'), repo.slug, Boolean(repo.isDefault)));
|
|
360
|
-
|
|
432
|
+
// A release helper the repository keeps as its own doesn't bring the files breakaway's version imports (BRK-79).
|
|
433
|
+
const kept = (entry) => {
|
|
434
|
+
const there = readTarget(entry);
|
|
435
|
+
return there === null || there === read(entry) || (update && owns(entry));
|
|
436
|
+
};
|
|
437
|
+
const needed = new Set(importClosure(RELEASE_ENTRIES.filter(kept), read));
|
|
438
|
+
for (const path of importClosure(RELEASE_ENTRIES, read))
|
|
439
|
+
if (needed.has(path) || readTarget(path) !== null) copy(path, read(path));
|
|
361
440
|
if (HOOKS_FROM_COPY) for (const path of importClosure(HOOK_ENTRIES, read)) copy(HOOKS_DIR + path, read(path));
|
|
362
441
|
// An old copy of the CLI is replaced by npx: with update, its files go. The src/ files it imported may be the
|
|
363
442
|
// repository's own by now, so those are only named.
|
|
@@ -389,7 +468,7 @@ export function initPlan({
|
|
|
389
468
|
);
|
|
390
469
|
}
|
|
391
470
|
if (readTarget('.claude/skills') === null) files.push({ path: '.claude/skills', link: '../.agents/skills' });
|
|
392
|
-
copy(SKILL, skillFor(read(SKILL), board));
|
|
471
|
+
copy(SKILL, skillFor(read(SKILL), board, repo));
|
|
393
472
|
add('AGENTS.md', agentsMd(repo, board, configDir));
|
|
394
473
|
if (!skipped.includes('AGENTS.md')) todo.push('AGENTS.md: add how to build in this repository');
|
|
395
474
|
|
|
@@ -443,5 +522,43 @@ export function initPlan({
|
|
|
443
522
|
append: attributes !== null,
|
|
444
523
|
});
|
|
445
524
|
}
|
|
525
|
+
// A file the repository still runs stays, with the old copy it belongs to (BRK-79): package.json, or settings the
|
|
526
|
+
// rewiring above couldn't move to npx.
|
|
527
|
+
const settingsNow =
|
|
528
|
+
files.find((f) => f.path === '.claude/settings.json')?.content ?? readTarget('.claude/settings.json');
|
|
529
|
+
const uses = [
|
|
530
|
+
['package.json', readTarget('package.json')],
|
|
531
|
+
['.claude/settings.json', settingsNow],
|
|
532
|
+
];
|
|
533
|
+
for (const group of [(p) => p.startsWith('scripts/'), (p) => p.startsWith(HOOKS_DIR)]) {
|
|
534
|
+
const inGroup = removals.filter(group);
|
|
535
|
+
const used = uses.flatMap(([file, text]) =>
|
|
536
|
+
inGroup.filter((p) => String(text ?? '').includes(p)).map((p) => `${file} still runs ${p}`),
|
|
537
|
+
);
|
|
538
|
+
if (!used.length) continue;
|
|
539
|
+
for (const p of inGroup) removals.splice(removals.indexOf(p), 1);
|
|
540
|
+
notes.push(
|
|
541
|
+
`${used.join('; ')}, so its copy stays: switch it to npx breakaway (\`${hookCommand('session')}\` for the hooks), then run --update again to remove the copy.`,
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
if (theirs.length)
|
|
545
|
+
notes.push(
|
|
546
|
+
`${theirs.join(', ')} ${theirs.length > 1 ? 'are' : 'is'} at a path breakaway copies to, but repos init has no record of writing ${theirs.length > 1 ? 'them' : 'it'}, so ${theirs.length > 1 ? 'they are left as they are' : 'it is left as it is'}. If one is breakaway's older copy, delete it and run --update again.`,
|
|
547
|
+
);
|
|
548
|
+
// The record of what is breakaway's here; a run without --update leaves an existing one alone.
|
|
549
|
+
const record = `${JSON.stringify(
|
|
550
|
+
{
|
|
551
|
+
about: `Files repos init copied from ${board} and keeps in step with it (npx breakaway repos init <slug> --update). A file not listed is this repository's own, even at a path breakaway copies to.`,
|
|
552
|
+
from: board,
|
|
553
|
+
files: [...ours].sort(),
|
|
554
|
+
},
|
|
555
|
+
null,
|
|
556
|
+
2,
|
|
557
|
+
)}\n`;
|
|
558
|
+
const recordThere = readTarget(MANIFEST);
|
|
559
|
+
if (recordThere === null) files.push({ path: MANIFEST, content: record });
|
|
560
|
+
else if (recordThere === record) current.push(MANIFEST);
|
|
561
|
+
else if (update) files.push({ path: MANIFEST, content: record, changed: true });
|
|
562
|
+
else skipped.push(MANIFEST);
|
|
446
563
|
return { files, removals, skipped, current, notes, todo };
|
|
447
564
|
}
|
|
@@ -17,7 +17,7 @@ import { tmpdir } from 'node:os';
|
|
|
17
17
|
import { join } from 'node:path';
|
|
18
18
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
19
19
|
import { boardConfig, claimedTask, projectRoot } from './hook-config.js';
|
|
20
|
-
import {
|
|
20
|
+
import { sessionRequest } from './proxy.js';
|
|
21
21
|
import { waitForMessages } from './session-messages.js';
|
|
22
22
|
|
|
23
23
|
async function main() {
|
|
@@ -43,9 +43,9 @@ async function main() {
|
|
|
43
43
|
const { base, headers } = boardConfig(root);
|
|
44
44
|
if (!base) return;
|
|
45
45
|
const url = `${base}/api/tasks/${encodeURIComponent(claim.uuid)}/messages/waiting?agent=${encodeURIComponent(claim.agent)}`;
|
|
46
|
-
|
|
46
|
+
// Through curl in a cloud session, so it works on whichever Node runs hooks there (BRK-86).
|
|
47
47
|
const ask = async () => {
|
|
48
|
-
const res = await
|
|
48
|
+
const res = await sessionRequest(url, { headers, timeoutMs: 10_000 });
|
|
49
49
|
return res.ok ? res.json() : null;
|
|
50
50
|
};
|
|
51
51
|
|
package/scripts/tasks/proxy.js
CHANGED
|
@@ -7,7 +7,11 @@
|
|
|
7
7
|
* routeThroughSessionProxy() sends fetch through the proxy and trusts the system's CA store too, which
|
|
8
8
|
* holds the proxy's certificate. Without a proxy in the environment it does nothing.
|
|
9
9
|
*/
|
|
10
|
+
import { spawn } from 'node:child_process';
|
|
11
|
+
import { readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
10
12
|
import http from 'node:http';
|
|
13
|
+
import { tmpdir } from 'node:os';
|
|
14
|
+
import { join } from 'node:path';
|
|
11
15
|
import tls from 'node:tls';
|
|
12
16
|
|
|
13
17
|
export function sessionProxy(env = process.env) {
|
|
@@ -25,3 +29,89 @@ export function routeThroughSessionProxy(env = process.env) {
|
|
|
25
29
|
}
|
|
26
30
|
return true;
|
|
27
31
|
}
|
|
32
|
+
|
|
33
|
+
/** Runs `command` with `args`, `input` on stdin, and resolves with stdout; rejects with stderr when it fails. */
|
|
34
|
+
function runWithInput(command, args, input) {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const child = spawn(command, args, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
37
|
+
let out = '';
|
|
38
|
+
let err = '';
|
|
39
|
+
child.stdout.on('data', (d) => {
|
|
40
|
+
out += d;
|
|
41
|
+
});
|
|
42
|
+
child.stderr.on('data', (d) => {
|
|
43
|
+
err += d;
|
|
44
|
+
});
|
|
45
|
+
child.on('error', reject);
|
|
46
|
+
child.on('close', (code) =>
|
|
47
|
+
code === 0 ? resolve(out) : reject(Object.assign(new Error(err.trim() || `${command} exited ${code}`), { code })),
|
|
48
|
+
);
|
|
49
|
+
child.stdin.end(input ?? '');
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A hook's request to the board (BRK-86). Claude Code runs hooks with the image's default Node, which in a cloud session
|
|
55
|
+
* can be too old to send fetch through the session proxy (Node 20 has neither http.setGlobalProxyFromEnv nor the CA
|
|
56
|
+
* calls above), and the sandbox refuses a request that goes around it. So with a proxy the request goes through curl,
|
|
57
|
+
* which reads HTTPS_PROXY and the system's certificates on any Node; without curl, or without a proxy, through fetch.
|
|
58
|
+
* Answers like fetch's response: { ok, status, json() }.
|
|
59
|
+
* @param {string} url
|
|
60
|
+
* @param {{ method?: string, headers?: Record<string, string>, body?: string, timeoutMs?: number }} [request]
|
|
61
|
+
*/
|
|
62
|
+
export async function sessionRequest(
|
|
63
|
+
url,
|
|
64
|
+
{ method = 'GET', headers = {}, body, timeoutMs = 4000 } = {},
|
|
65
|
+
{ env = process.env, run = runWithInput, fetch = globalThis.fetch } = {},
|
|
66
|
+
) {
|
|
67
|
+
if (sessionProxy(env)) {
|
|
68
|
+
const args = ['-sS', '--max-time', String(Math.max(1, Math.ceil(timeoutMs / 1000))), '-X', method];
|
|
69
|
+
for (const [name, value] of Object.entries(headers)) args.push('-H', `${name}: ${value}`);
|
|
70
|
+
if (body !== undefined) args.push('--data-binary', '@-');
|
|
71
|
+
args.push('-w', '\n%{http_code}', url);
|
|
72
|
+
try {
|
|
73
|
+
const out = await run('curl', args, body);
|
|
74
|
+
const at = out.lastIndexOf('\n');
|
|
75
|
+
const status = Number(out.slice(at + 1));
|
|
76
|
+
const text = out.slice(0, Math.max(0, at));
|
|
77
|
+
return { ok: status >= 200 && status < 300, status, json: async () => (text.trim() ? JSON.parse(text) : null) };
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (/** @type {any} */ (error)?.code !== 'ENOENT') throw error;
|
|
80
|
+
// No curl here: fetch, through the proxy if this Node can.
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
routeThroughSessionProxy(env);
|
|
84
|
+
return fetch(url, { method, headers, body, signal: AbortSignal.timeout(timeoutMs) });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Where a hook notes why it couldn't post for a task (BRK-86): the system's temp folder, never the repository, where a
|
|
89
|
+
* cloud session's Stop check would take a new file as work to commit. The CLI shows it on its next command here.
|
|
90
|
+
*/
|
|
91
|
+
const failurePath = (uuid, dir) => join(dir, `breakaway-hook-${String(uuid).replace(/[^\w-]/gu, '')}.json`);
|
|
92
|
+
|
|
93
|
+
/** Records that the hook couldn't post for task `uuid`, and why. */
|
|
94
|
+
export function noteHookFailure(uuid, reason, dir = tmpdir(), at = new Date()) {
|
|
95
|
+
try {
|
|
96
|
+
writeFileSync(
|
|
97
|
+
failurePath(uuid, dir),
|
|
98
|
+
`${JSON.stringify({ at: at.toISOString(), reason: String(reason).slice(0, 300) })}\n`,
|
|
99
|
+
);
|
|
100
|
+
} catch {
|
|
101
|
+
/* nowhere to note it */
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Clears that note, after a post that went through. */
|
|
106
|
+
export function clearHookFailure(uuid, dir = tmpdir()) {
|
|
107
|
+
rmSync(failurePath(uuid, dir), { force: true });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The hook's last failure for task `uuid`, or null. */
|
|
111
|
+
export function hookFailure(uuid, dir = tmpdir()) {
|
|
112
|
+
try {
|
|
113
|
+
return JSON.parse(readFileSync(failurePath(uuid, dir), 'utf8'));
|
|
114
|
+
} catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -7,12 +7,13 @@
|
|
|
7
7
|
* additionalContext so Claude gets them on its next turn (docs/specs/IDEA-15-message-a-running-agent.md).
|
|
8
8
|
*
|
|
9
9
|
* Quiet by design: without a claimed task (.task-session, written by `tasks claim`), with
|
|
10
|
-
* BREAKAWAY_SESSION_LOG=off (or SAMEWAVE_TASKS_SESSION_LOG=off), or on any error, it does nothing and exits 0.
|
|
10
|
+
* BREAKAWAY_SESSION_LOG=off (or SAMEWAVE_TASKS_SESSION_LOG=off), or on any error, it does nothing and exits 0. A post
|
|
11
|
+
* that fails leaves its reason in the temp folder, which the CLI's next command here shows (BRK-86).
|
|
11
12
|
*/
|
|
12
|
-
import { boardConfig, claimedTask, projectRoot } from './hook-config.js';
|
|
13
|
-
import {
|
|
13
|
+
import { boardConfig, claimedTask, dropClaim, projectRoot } from './hook-config.js';
|
|
14
|
+
import { clearHookFailure, noteHookFailure, sessionRequest } from './proxy.js';
|
|
14
15
|
import { entryFor } from './session-log.js';
|
|
15
|
-
import { CONTEXT_EVENTS, messageOutput } from './session-messages.js';
|
|
16
|
+
import { CONTEXT_EVENTS, messageOutput, releasedOutput } from './session-messages.js';
|
|
16
17
|
|
|
17
18
|
async function main() {
|
|
18
19
|
const root = projectRoot();
|
|
@@ -26,23 +27,36 @@ async function main() {
|
|
|
26
27
|
if (!entry) return;
|
|
27
28
|
|
|
28
29
|
const { base, headers } = boardConfig(root);
|
|
29
|
-
if (!base) return;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
30
|
+
if (!base) return noteHookFailure(claim.uuid, 'no board address: set BREAKAWAY_URL');
|
|
31
|
+
let res;
|
|
32
|
+
try {
|
|
33
|
+
// Through curl in a cloud session, so it works on whichever Node runs hooks there (BRK-86).
|
|
34
|
+
res = await sessionRequest(`${base}/api/tasks/${encodeURIComponent(claim.uuid)}/session`, {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
headers: { 'Content-Type': 'application/json', ...headers },
|
|
37
|
+
body: JSON.stringify({
|
|
38
|
+
agent: claim.agent,
|
|
39
|
+
session: hook.session_id ?? null,
|
|
40
|
+
remote: process.env.CLAUDE_CODE_REMOTE === 'true',
|
|
41
|
+
entries: [entry],
|
|
42
|
+
// A Stop hook's output can't reach Claude, so it leaves the messages for the next event.
|
|
43
|
+
messages: CONTEXT_EVENTS.has(hook.hook_event_name),
|
|
44
|
+
}),
|
|
45
|
+
timeoutMs: 4000,
|
|
46
|
+
});
|
|
47
|
+
} catch (error) {
|
|
48
|
+
return noteHookFailure(claim.uuid, /** @type {Error} */ (error)?.message ?? String(error));
|
|
49
|
+
}
|
|
50
|
+
if (!res.ok) return noteHookFailure(claim.uuid, `HTTP ${res.status}`);
|
|
51
|
+
clearHookFailure(claim.uuid);
|
|
52
|
+
const answer = await res.json();
|
|
53
|
+
if (answer?.released) {
|
|
54
|
+
dropClaim(claim, root);
|
|
55
|
+
const output = releasedOutput(claim, hook.hook_event_name);
|
|
56
|
+
if (output) process.stdout.write(`${JSON.stringify(output)}\n`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const output = messageOutput(answer, hook.hook_event_name);
|
|
46
60
|
if (output) process.stdout.write(`${JSON.stringify(output)}\n`);
|
|
47
61
|
}
|
|
48
62
|
|
|
@@ -27,6 +27,13 @@ export function messageOutput(answer, event) {
|
|
|
27
27
|
return { hookSpecificOutput: { hookEventName: event, additionalContext: text } };
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/** The one line the hook says when the board no longer has this checkout's task claimed by its agent (BRK-87), or null on an event that can't carry it. */
|
|
31
|
+
export function releasedOutput(claim, event) {
|
|
32
|
+
if (!CONTEXT_EVENTS.has(event)) return null;
|
|
33
|
+
const text = `${claim.wid ?? 'The task'} is no longer claimed by ${claim.agent ?? 'this agent'} (finished or released), so this checkout stops sending its live output. \`tasks claim\` starts it again on another task.`;
|
|
34
|
+
return { hookSpecificOutput: { hookEventName: event, additionalContext: text } };
|
|
35
|
+
}
|
|
36
|
+
|
|
30
37
|
/** The board's answer → the messages as Claude reads them, one paragraph each, or '' when there are none. */
|
|
31
38
|
export function messageText(answer) {
|
|
32
39
|
const messages = Array.isArray(answer?.messages) ? answer.messages : [];
|
package/scripts/tasks.mjs
CHANGED
|
@@ -40,11 +40,18 @@ import {
|
|
|
40
40
|
} from './tasks/structure.js';
|
|
41
41
|
import { looksLikeSecret } from '../src/ping.js';
|
|
42
42
|
import { promptPathOf } from '../src/repos.js';
|
|
43
|
-
import { sessionProxy, routeThroughSessionProxy } from './tasks/proxy.js';
|
|
43
|
+
import { hookFailure, sessionProxy, routeThroughSessionProxy } from './tasks/proxy.js';
|
|
44
44
|
import { githubFromRemote, inRepo, pickRepo } from './tasks/repo.js';
|
|
45
45
|
import { NO_TERMINAL, ask as askIn } from './tasks/ask.js';
|
|
46
46
|
import { CLI_PACKAGE, PROMPT_SECTIONS, initPlan, machineTaskrc, promptSections } from './tasks/init.js';
|
|
47
|
-
import {
|
|
47
|
+
import {
|
|
48
|
+
githubRequest,
|
|
49
|
+
ideaTask,
|
|
50
|
+
pullAgentRequest,
|
|
51
|
+
pullAgentSummary,
|
|
52
|
+
staleCliWarning,
|
|
53
|
+
unknownSubcommand,
|
|
54
|
+
} from './tasks/cli.js';
|
|
48
55
|
import { CLI_VERSION } from '../src/cli-version.js';
|
|
49
56
|
import { LEGACY, parseInstall, secretName } from '../src/install.js';
|
|
50
57
|
import {
|
|
@@ -143,6 +150,8 @@ Reading (list, next, claim, and add work in this checkout's repos
|
|
|
143
150
|
routines saved prompts the owner runs with a button, and their caps
|
|
144
151
|
routines run <slug> run one now: makes a RUN task and starts an agent on it [--note <text>]
|
|
145
152
|
horizon close close now: finished tasks go to the archive, next becomes now, later becomes next [--dry-run]
|
|
153
|
+
github fix <n> start an agent on a pull request's conflicts, failing checks, or review comments (owner) [--problem conflicts|failing|review] [--note <text>] [--repo <slug>]
|
|
154
|
+
github review <n> start an agent that tests a Dependabot pull request, as Safe to merge? does (owner) [--note <text>] [--repo <slug>]
|
|
146
155
|
github the checkout's repository on GitHub: open pull requests, checks, reviews, CI, deploys, alerts [--sync] [--repo <slug>]
|
|
147
156
|
hook session|wait the Claude Code session hooks a repository's .claude/settings.json runs (npx breakaway hook session)
|
|
148
157
|
health the server's state
|
|
@@ -517,6 +526,20 @@ async function startSessionLog(t) {
|
|
|
517
526
|
);
|
|
518
527
|
}
|
|
519
528
|
|
|
529
|
+
/** The session hook couldn't post this checkout's live output: say so where the agent and the owner see it (BRK-86). */
|
|
530
|
+
function warnHookFailure() {
|
|
531
|
+
try {
|
|
532
|
+
const claim = JSON.parse(readFileSync(join(REPO, '.task-session'), 'utf8'));
|
|
533
|
+
const failed = claim?.uuid && hookFailure(claim.uuid);
|
|
534
|
+
if (failed)
|
|
535
|
+
console.error(
|
|
536
|
+
`tasks: the session hook couldn't post ${claim.wid ?? 'this task'}'s live output (${failed.reason}, ${failed.at.slice(0, 16).replace('T', ' ')} UTC), so the board shows none. docs/tasks.md#cloud-agents says what to check.`,
|
|
537
|
+
);
|
|
538
|
+
} catch {
|
|
539
|
+
/* no claim here */
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
520
543
|
function unmarkSession(t) {
|
|
521
544
|
try {
|
|
522
545
|
const path = join(REPO, '.task-session');
|
|
@@ -1188,6 +1211,18 @@ const commands = {
|
|
|
1188
1211
|
);
|
|
1189
1212
|
},
|
|
1190
1213
|
async github() {
|
|
1214
|
+
const action = args[0];
|
|
1215
|
+
if (action === 'fix' || action === 'review') {
|
|
1216
|
+
const built = pullAgentRequest(action, args[1], {
|
|
1217
|
+
repo: (await checkoutRepo()).slug,
|
|
1218
|
+
problem: opts.problem,
|
|
1219
|
+
note: opts.note,
|
|
1220
|
+
});
|
|
1221
|
+
if (built.error || !built.request) fail(built.error ?? 'bad request');
|
|
1222
|
+
const answer = await call(...built.request);
|
|
1223
|
+
print(answer, (a) => pullAgentSummary(action, args[1].replace(/^#/u, ''), a));
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1191
1226
|
const g = await call(...githubRequest((await checkoutRepo()).slug, { sync: Boolean(opts.sync) }));
|
|
1192
1227
|
print(g, (d) => {
|
|
1193
1228
|
if (!d.connected) return "GitHub isn't connected yet: open the GitHub view on the board (docs/tasks.md#github).";
|
|
@@ -1938,5 +1973,6 @@ if (opts.help || command === 'help') {
|
|
|
1938
1973
|
} else if (unknownSubcommand(command, args[0])) {
|
|
1939
1974
|
fail(unknownSubcommand(command, args[0]));
|
|
1940
1975
|
} else {
|
|
1976
|
+
if (command !== 'hook') warnHookFailure();
|
|
1941
1977
|
await commands[command]();
|
|
1942
1978
|
}
|
package/src/cli-version.js
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
* The version of the board's CLI and the files repos init copies with it (CLD-193). The board reports it
|
|
3
3
|
* (every API answer's X-Tasks-Cli header, and health), so a copy in another repository can say it's older
|
|
4
4
|
* and how to update it. scripts/tasks/version.test.js fails when the copied files change and this doesn't:
|
|
5
|
-
* bump CLI_VERSION and set CLI_FINGERPRINT to the value it prints. Constants only: the board's Worker imports it,
|
|
6
5
|
* so it lives in the board's package (CLD-135) and the CLI imports it from here.
|
|
7
6
|
*/
|
|
8
|
-
export const CLI_VERSION =
|
|
9
|
-
export const CLI_FINGERPRINT = '
|
|
7
|
+
export const CLI_VERSION = 44;
|
|
8
|
+
export const CLI_FINGERPRINT = 'd89515e35e35eacd';
|
package/src/install.js
CHANGED
|
@@ -147,7 +147,7 @@ export const secretName = (inst, key) => `${inst.secretsPrefix}${key}`;
|
|
|
147
147
|
export const docsLink = (inst, anchor) => (inst.docs ? `${inst.docs}${anchor ? `#${anchor}` : ''}` : null);
|
|
148
148
|
|
|
149
149
|
/** The routes `run_worker_first` sends to the Worker; everything else is the web app. */
|
|
150
|
-
const WORKER_FIRST = ['/api/*', '/v1/*', '/github/*', '/login', '/logout'];
|
|
150
|
+
export const WORKER_FIRST = ['/api/*', '/v1/*', '/github/*', '/login', '/logout'];
|
|
151
151
|
|
|
152
152
|
/**
|
|
153
153
|
* The Worker's wrangler config for an install. `local` is for `wrangler dev` (interop): no custom
|