cloving 1.0.1 → 1.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/README.md +145 -14
- package/dist/src/authority/commands.js +10 -2
- package/dist/src/authority/reconciliation.d.ts +2 -0
- package/dist/src/authority/reconciliation.js +10 -1
- package/dist/src/authority/transitions.d.ts +1 -1
- package/dist/src/authority/transitions.js +2 -0
- package/dist/src/broker/actor/authority-session.d.ts +2 -0
- package/dist/src/broker/actor/authority-session.js +3 -4
- package/dist/src/chats/analysis-batches.d.ts +24 -0
- package/dist/src/chats/analysis-batches.js +83 -0
- package/dist/src/chats/analysis-contract.d.ts +80 -0
- package/dist/src/chats/analysis-contract.js +16 -0
- package/dist/src/chats/analysis-data.d.ts +3 -0
- package/dist/src/chats/analysis-data.js +85 -0
- package/dist/src/chats/analysis-input.d.ts +12 -0
- package/dist/src/chats/analysis-input.js +83 -0
- package/dist/src/chats/analysis-links.d.ts +2 -0
- package/dist/src/chats/analysis-links.js +25 -0
- package/dist/src/chats/analysis-prompt.d.ts +10 -0
- package/dist/src/chats/analysis-prompt.js +16 -0
- package/dist/src/chats/analysis-proposal.d.ts +2 -0
- package/dist/src/chats/analysis-proposal.js +32 -0
- package/dist/src/chats/analysis-provenance.d.ts +4 -0
- package/dist/src/chats/analysis-provenance.js +99 -0
- package/dist/src/chats/analysis-shape.d.ts +3 -0
- package/dist/src/chats/analysis-shape.js +228 -0
- package/dist/src/chats/analysis.d.ts +3 -0
- package/dist/src/chats/analysis.js +42 -0
- package/dist/src/chats/corpus-input.d.ts +5 -0
- package/dist/src/chats/corpus-input.js +288 -0
- package/dist/src/chats/destination.js +3 -1
- package/dist/src/chats/import.d.ts +8 -6
- package/dist/src/chats/import.js +195 -23
- package/dist/src/chats/input-file.d.ts +3 -0
- package/dist/src/chats/input-file.js +32 -0
- package/dist/src/chats/normalize.d.ts +11 -2
- package/dist/src/chats/normalize.js +18 -2
- package/dist/src/chats/previous-import.d.ts +5 -0
- package/dist/src/chats/previous-import.js +34 -0
- package/dist/src/chats/reader.d.ts +7 -1
- package/dist/src/chats/reader.js +46 -15
- package/dist/src/chats/redact-import.d.ts +14 -0
- package/dist/src/chats/redact-import.js +141 -0
- package/dist/src/chats/redaction.d.ts +41 -0
- package/dist/src/chats/redaction.js +161 -0
- package/dist/src/chats/revision-input.d.ts +3 -0
- package/dist/src/chats/revision-input.js +180 -0
- package/dist/src/chats/revision.d.ts +48 -0
- package/dist/src/chats/revision.js +94 -0
- package/dist/src/chats/selection.d.ts +25 -0
- package/dist/src/chats/selection.js +109 -0
- package/dist/src/chats/source.d.ts +23 -0
- package/dist/src/chats/source.js +135 -0
- package/dist/src/chats/structure.d.ts +57 -0
- package/dist/src/chats/structure.js +216 -0
- package/dist/src/cli/arguments.d.ts +8 -0
- package/dist/src/cli/arguments.js +94 -13
- package/dist/src/cli/client.d.ts +1 -0
- package/dist/src/cli/client.js +38 -1
- package/dist/src/cli/commands.d.ts +3 -0
- package/dist/src/cli/commands.js +29 -11
- package/dist/src/cli/main.d.ts +2 -1
- package/dist/src/cli/main.js +61 -14
- package/dist/src/cli/render.d.ts +2 -0
- package/dist/src/cli/render.js +70 -2
- package/dist/src/cli/usage-render.d.ts +3 -0
- package/dist/src/cli/usage-render.js +28 -0
- package/dist/src/cli/usage.d.ts +3 -2
- package/dist/src/cli/usage.js +38 -6
- package/dist/src/cli/watch-actions.d.ts +10 -0
- package/dist/src/cli/watch-actions.js +58 -0
- package/dist/src/cli/watch-board-view.d.ts +32 -0
- package/dist/src/cli/watch-board-view.js +88 -15
- package/dist/src/cli/watch-board.js +6 -1
- package/dist/src/cli/watch-card-client.d.ts +1 -0
- package/dist/src/cli/watch-card-client.js +44 -0
- package/dist/src/cli/watch-card-forms.d.ts +2 -0
- package/dist/src/cli/watch-card-forms.js +33 -0
- package/dist/src/cli/watch-card-reply.d.ts +5 -0
- package/dist/src/cli/watch-card-reply.js +34 -0
- package/dist/src/cli/watch-card-view.d.ts +40 -0
- package/dist/src/cli/watch-card-view.js +86 -0
- package/dist/src/cli/watch-client.d.ts +1 -0
- package/dist/src/cli/watch-client.js +118 -0
- package/dist/src/cli/watch-model.js +31 -14
- package/dist/src/cli/watch-page.d.ts +1 -1
- package/dist/src/cli/watch-page.js +71 -85
- package/dist/src/cli/watch-pr-board.d.ts +3 -0
- package/dist/src/cli/watch-pr-board.js +26 -0
- package/dist/src/cli/watch-pr-command.d.ts +15 -0
- package/dist/src/cli/watch-pr-command.js +37 -0
- package/dist/src/cli/watch-pr-identity.d.ts +20 -0
- package/dist/src/cli/watch-pr-identity.js +62 -0
- package/dist/src/cli/watch-pr-observer.d.ts +19 -0
- package/dist/src/cli/watch-pr-observer.js +108 -0
- package/dist/src/cli/watch-pr-settings.d.ts +8 -0
- package/dist/src/cli/watch-pr-settings.js +47 -0
- package/dist/src/cli/watch-reads.js +4 -0
- package/dist/src/cli/watch-render.d.ts +16 -2
- package/dist/src/cli/watch-render.js +10 -3
- package/dist/src/cli/watch-run-input.d.ts +14 -0
- package/dist/src/cli/watch-run-input.js +49 -0
- package/dist/src/cli/watch-server.d.ts +3 -0
- package/dist/src/cli/watch-server.js +44 -6
- package/dist/src/cli/watch-start-view.d.ts +18 -0
- package/dist/src/cli/watch-start-view.js +77 -0
- package/dist/src/cli/watch-turn.d.ts +1 -0
- package/dist/src/cli/watch-turn.js +7 -2
- package/dist/src/cli/watch-usage.d.ts +6 -0
- package/dist/src/cli/watch-usage.js +65 -0
- package/dist/src/cli/watch-view.d.ts +15 -0
- package/dist/src/cli/watch-view.js +61 -17
- package/dist/src/cli.js +48 -6
- package/dist/src/daemon/analysis-launch.d.ts +5 -0
- package/dist/src/daemon/analysis-launch.js +81 -0
- package/dist/src/daemon/analysis-token.d.ts +6 -0
- package/dist/src/daemon/analysis-token.js +88 -0
- package/dist/src/daemon/analysis-worker.d.ts +22 -0
- package/dist/src/daemon/analysis-worker.js +219 -0
- package/dist/src/daemon/chats-files.d.ts +15 -0
- package/dist/src/daemon/chats-files.js +113 -0
- package/dist/src/daemon/chats-worker.d.ts +12 -0
- package/dist/src/daemon/chats-worker.js +200 -0
- package/dist/src/daemon/claude-turn.d.ts +10 -0
- package/dist/src/daemon/claude-turn.js +96 -15
- package/dist/src/daemon/claude-worker.d.ts +4 -1
- package/dist/src/daemon/claude-worker.js +99 -47
- package/dist/src/daemon/codex-stream.js +16 -0
- package/dist/src/daemon/daemon.d.ts +3 -0
- package/dist/src/daemon/daemon.js +16 -7
- package/dist/src/daemon/github-methods.js +4 -0
- package/dist/src/daemon/github-ticket-status.d.ts +3 -0
- package/dist/src/daemon/github-ticket-status.js +268 -0
- package/dist/src/daemon/issue-queue.d.ts +1 -0
- package/dist/src/daemon/issue-queue.js +115 -24
- package/dist/src/daemon/operation-journal.d.ts +1 -0
- package/dist/src/daemon/operation-journal.js +4 -2
- package/dist/src/daemon/retrying-turn.d.ts +9 -0
- package/dist/src/daemon/retrying-turn.js +185 -0
- package/dist/src/host/world-reconciliation.d.ts +2 -0
- package/dist/src/host/world-reconciliation.js +6 -0
- package/dist/src/host/world-runs.js +9 -1
- package/dist/src/service/run-handlers.js +15 -23
- package/dist/src/service/worker-totals.d.ts +3 -0
- package/dist/src/service/worker-totals.js +28 -0
- package/dist/src/ticket-status.d.ts +8 -0
- package/dist/src/ticket-status.js +45 -0
- package/dist/src/token-prices.d.ts +19 -0
- package/dist/src/token-prices.js +76 -0
- package/dist/src/worker-usage.d.ts +22 -0
- package/dist/src/worker-usage.js +44 -0
- package/dist/src/workflow-candidate.d.ts +29 -0
- package/dist/src/workflow-candidate.js +59 -0
- package/dist/src/workflow-contracts.d.ts +2 -0
- package/dist/src/workflow-contracts.js +27 -5
- package/dist/src/workflow-declaration.d.ts +1 -1
- package/dist/src/workflow-declaration.js +3 -1
- package/dist/src/workflows.d.ts +1 -0
- package/dist/src/workflows.js +1 -0
- package/dist/tools/self-host-processes.js +7 -2
- package/dist/tools/self-host-workflows.d.ts +1 -1
- package/dist/tools/self-host-workflows.js +2 -2
- package/dist/tools/self-host.js +6 -6
- package/dist/tools/self-update-host.d.ts +7 -0
- package/dist/tools/self-update-host.js +48 -1
- package/dist/tools/self-update.d.ts +1 -0
- package/dist/tools/self-update.js +85 -13
- package/dist/workflows/analyze-chats.d.ts +10 -0
- package/dist/workflows/analyze-chats.declaration.json +9 -0
- package/dist/workflows/analyze-chats.js +73 -0
- package/dist/workflows/develop-ticket.d.ts +1 -7
- package/dist/workflows/develop-ticket.js +42 -141
- package/dist/workflows/issue-labels.d.ts +3 -1
- package/dist/workflows/issue-labels.js +34 -0
- package/dist/workflows/ticket-prompts.d.ts +35 -0
- package/dist/workflows/ticket-prompts.js +133 -0
- package/package.json +5 -3
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { issuePrivacy } from "./github-issue-privacy.js";
|
|
2
|
+
import { field, text } from "./github-operation.js";
|
|
3
|
+
import { ticketLimits } from "./ticket-source.js";
|
|
4
|
+
const privateRemaining = "Remaining work is in the private run report.";
|
|
5
|
+
const issueBodyBytes = ticketLimits.textBytes - 2_048;
|
|
6
|
+
const workflowLabels = [
|
|
7
|
+
"cloving: ready",
|
|
8
|
+
"cloving: blocked",
|
|
9
|
+
"cloving: in progress",
|
|
10
|
+
];
|
|
11
|
+
function positiveInteger(operation, name) {
|
|
12
|
+
const value = operation.payload[name];
|
|
13
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1)
|
|
14
|
+
throw new Error("invalid-payload");
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
function issuePath(operation) {
|
|
18
|
+
const repository = `${text(operation, "owner")}/${text(operation, "repository")}`;
|
|
19
|
+
if (!/^[\w.-]+\/[\w.-]+$/.test(repository))
|
|
20
|
+
throw new Error("invalid-payload");
|
|
21
|
+
return `repos/${repository}/issues/${String(positiveInteger(operation, "number"))}`;
|
|
22
|
+
}
|
|
23
|
+
function labelsOf(issue) {
|
|
24
|
+
const labels = field(issue, "labels");
|
|
25
|
+
if (!Array.isArray(labels))
|
|
26
|
+
throw new Error("github-call-failed");
|
|
27
|
+
return labels.map((item) => {
|
|
28
|
+
const name = typeof item === "string" ? item : field(item, "name");
|
|
29
|
+
if (typeof name !== "string")
|
|
30
|
+
throw new Error("github-call-failed");
|
|
31
|
+
return name;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
async function readIssue(operation, path) {
|
|
35
|
+
const issue = JSON.parse(await operation.calls.gh(["api", path]));
|
|
36
|
+
const body = field(issue, "body");
|
|
37
|
+
if ((body !== null && typeof body !== "string") ||
|
|
38
|
+
field(issue, "number") !== positiveInteger(operation, "number") ||
|
|
39
|
+
!["open", "closed"].includes(String(field(issue, "state"))) ||
|
|
40
|
+
field(issue, "pull_request") !== undefined)
|
|
41
|
+
throw new Error("github-call-failed");
|
|
42
|
+
return {
|
|
43
|
+
state: field(issue, "state"),
|
|
44
|
+
reason: field(issue, "state_reason"),
|
|
45
|
+
body: typeof body === "string" ? body : "",
|
|
46
|
+
labels: labelsOf(issue),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function isPositiveInteger(value) {
|
|
50
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
51
|
+
}
|
|
52
|
+
function isRemaining(value) {
|
|
53
|
+
return (Array.isArray(value) &&
|
|
54
|
+
value.length > 0 &&
|
|
55
|
+
value.every((item) => typeof item === "string" && item.trim() !== "" && !/[\r\n]/.test(item)));
|
|
56
|
+
}
|
|
57
|
+
function isPartialRun(value) {
|
|
58
|
+
return (isPositiveInteger(field(value, "pullRequest")) &&
|
|
59
|
+
isRemaining(field(value, "remaining")));
|
|
60
|
+
}
|
|
61
|
+
function historyOf(json) {
|
|
62
|
+
try {
|
|
63
|
+
const value = JSON.parse(json);
|
|
64
|
+
const partialRuns = field(value, "partialRuns");
|
|
65
|
+
const completedPullRequest = field(value, "completedPullRequest");
|
|
66
|
+
if (field(value, "version") !== 1 ||
|
|
67
|
+
!Array.isArray(partialRuns) ||
|
|
68
|
+
!partialRuns.every(isPartialRun) ||
|
|
69
|
+
new Set(partialRuns.map((run) => run.pullRequest)).size !==
|
|
70
|
+
partialRuns.length ||
|
|
71
|
+
(completedPullRequest !== undefined &&
|
|
72
|
+
!isPositiveInteger(completedPullRequest)))
|
|
73
|
+
return undefined;
|
|
74
|
+
return {
|
|
75
|
+
version: 1,
|
|
76
|
+
partialRuns,
|
|
77
|
+
...(completedPullRequest === undefined ? {} : { completedPullRequest }),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function statusText(history) {
|
|
85
|
+
const lines = history.partialRuns.map((run) => `- Pull request #${String(run.pullRequest)}: partial. Remaining: ${run.remaining.join("; ")}`);
|
|
86
|
+
if (history.completedPullRequest !== undefined)
|
|
87
|
+
lines.push(`Completed in pull request #${String(history.completedPullRequest)}.`);
|
|
88
|
+
return `## Cloving status\n${lines.join("\n")}\n\n`;
|
|
89
|
+
}
|
|
90
|
+
function statusSection(body) {
|
|
91
|
+
const empty = { version: 1, partialRuns: [] };
|
|
92
|
+
const marker = /^<!-- cloving-status(?=\s|-->|$)/gm;
|
|
93
|
+
const start = [...body.matchAll(marker)].at(-1)?.index;
|
|
94
|
+
if (start === undefined)
|
|
95
|
+
return { outside: body, history: empty };
|
|
96
|
+
const tail = body.slice(start);
|
|
97
|
+
const end = tail.indexOf("-->");
|
|
98
|
+
if (end !== -1 && tail.slice(end + 3).trim() !== "")
|
|
99
|
+
return { outside: body, history: empty };
|
|
100
|
+
const outside = body.slice(0, start);
|
|
101
|
+
if (end === -1)
|
|
102
|
+
return { outside, history: empty };
|
|
103
|
+
const history = historyOf(tail.slice("<!-- cloving-status".length, end).trim());
|
|
104
|
+
if (history === undefined)
|
|
105
|
+
return { outside, history: empty };
|
|
106
|
+
const section = statusText(history);
|
|
107
|
+
return {
|
|
108
|
+
outside: outside.endsWith(section)
|
|
109
|
+
? outside.slice(0, -section.length)
|
|
110
|
+
: outside,
|
|
111
|
+
history,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function withStatus(outside, history) {
|
|
115
|
+
const separator = "\n".repeat(Math.max(0, 2 - (outside.match(/\n*$/)?.[0].length ?? 0)));
|
|
116
|
+
const json = JSON.stringify(history)
|
|
117
|
+
.replace(/--/g, "\\u002d\\u002d")
|
|
118
|
+
.replace(/>/g, "\\u003e");
|
|
119
|
+
return `${outside}${outside === "" ? "" : separator}${statusText(history)}<!-- cloving-status\n${json}\n-->\n`;
|
|
120
|
+
}
|
|
121
|
+
async function writeIssue(operation, update) {
|
|
122
|
+
const { path, body, labels } = update;
|
|
123
|
+
const latest = await readIssue(operation, path);
|
|
124
|
+
if (latest.body !== update.originalBody)
|
|
125
|
+
throw new Error("github-issue-conflict");
|
|
126
|
+
const fields = update.fields ?? [];
|
|
127
|
+
if (body !== latest.body || fields.length > 0)
|
|
128
|
+
await operation.calls.gh([
|
|
129
|
+
"api",
|
|
130
|
+
"-X",
|
|
131
|
+
"PATCH",
|
|
132
|
+
path,
|
|
133
|
+
"-f",
|
|
134
|
+
`body=${body}`,
|
|
135
|
+
...fields,
|
|
136
|
+
]);
|
|
137
|
+
const add = workflowLabels.filter((name) => labels.includes(name) && !latest.labels.includes(name));
|
|
138
|
+
const remove = workflowLabels.filter((name) => !labels.includes(name) && latest.labels.includes(name));
|
|
139
|
+
if (add.length === 0 && remove.length === 0)
|
|
140
|
+
return;
|
|
141
|
+
await operation.calls.gh([
|
|
142
|
+
"issue",
|
|
143
|
+
"edit",
|
|
144
|
+
String(positiveInteger(operation, "number")),
|
|
145
|
+
"--repo",
|
|
146
|
+
`${text(operation, "owner")}/${text(operation, "repository")}`,
|
|
147
|
+
...add.flatMap((name) => ["--add-label", name]),
|
|
148
|
+
...remove.flatMap((name) => ["--remove-label", name]),
|
|
149
|
+
]);
|
|
150
|
+
}
|
|
151
|
+
function sameLabels(first, second) {
|
|
152
|
+
return JSON.stringify(first.toSorted()) === JSON.stringify(second.toSorted());
|
|
153
|
+
}
|
|
154
|
+
export async function closeIssue(operation) {
|
|
155
|
+
const path = issuePath(operation);
|
|
156
|
+
const pull = positiveInteger(operation, "pullRequest");
|
|
157
|
+
const issue = await readIssue(operation, path);
|
|
158
|
+
const section = statusSection(issue.body);
|
|
159
|
+
const body = withStatus(section.outside, {
|
|
160
|
+
...section.history,
|
|
161
|
+
completedPullRequest: pull,
|
|
162
|
+
});
|
|
163
|
+
const labels = issue.labels.filter((name) => !["cloving: ready", "cloving: blocked", "cloving: in progress"].includes(name));
|
|
164
|
+
if (body === issue.body &&
|
|
165
|
+
sameLabels(issue.labels, labels) &&
|
|
166
|
+
issue.state === "closed" &&
|
|
167
|
+
issue.reason === "completed")
|
|
168
|
+
return {
|
|
169
|
+
number: positiveInteger(operation, "number"),
|
|
170
|
+
ticketStatus: "complete",
|
|
171
|
+
};
|
|
172
|
+
await writeIssue(operation, {
|
|
173
|
+
path,
|
|
174
|
+
originalBody: issue.body,
|
|
175
|
+
body,
|
|
176
|
+
labels,
|
|
177
|
+
fields: issue.state === "closed" && issue.reason === "completed"
|
|
178
|
+
? []
|
|
179
|
+
: ["-f", "state=closed", "-f", "state_reason=completed"],
|
|
180
|
+
});
|
|
181
|
+
return {
|
|
182
|
+
number: positiveInteger(operation, "number"),
|
|
183
|
+
ticketStatus: "complete",
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function remainingItems(operation) {
|
|
187
|
+
const items = operation.payload["remaining"];
|
|
188
|
+
if (!Array.isArray(items) ||
|
|
189
|
+
items.length === 0 ||
|
|
190
|
+
items.some((item) => typeof item !== "string" || item.trim() === "" || /[\r\n]/.test(item)))
|
|
191
|
+
throw new Error("invalid-payload");
|
|
192
|
+
return items.map(String);
|
|
193
|
+
}
|
|
194
|
+
function publicRemaining(operation) {
|
|
195
|
+
const remaining = remainingItems(operation);
|
|
196
|
+
try {
|
|
197
|
+
issuePrivacy(remaining.join("; "), {
|
|
198
|
+
settings: operation.settings,
|
|
199
|
+
repository: `${text(operation, "owner")}/${text(operation, "repository")}`,
|
|
200
|
+
});
|
|
201
|
+
return remaining;
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return [privateRemaining];
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function partialHistory(section, status) {
|
|
208
|
+
const recorded = section.history.partialRuns.some((run) => run.pullRequest === status.pull);
|
|
209
|
+
for (const remaining of [status.remaining, [privateRemaining]]) {
|
|
210
|
+
const history = {
|
|
211
|
+
...section.history,
|
|
212
|
+
partialRuns: recorded
|
|
213
|
+
? section.history.partialRuns
|
|
214
|
+
: [
|
|
215
|
+
...section.history.partialRuns,
|
|
216
|
+
{ pullRequest: status.pull, remaining },
|
|
217
|
+
],
|
|
218
|
+
};
|
|
219
|
+
if (Buffer.byteLength(withStatus(section.outside, history)) <= issueBodyBytes)
|
|
220
|
+
return history;
|
|
221
|
+
}
|
|
222
|
+
throw new Error("github-issue-too-large");
|
|
223
|
+
}
|
|
224
|
+
export async function partialIssue(operation) {
|
|
225
|
+
const path = issuePath(operation);
|
|
226
|
+
const pull = positiveInteger(operation, "pullRequest");
|
|
227
|
+
const remaining = publicRemaining(operation);
|
|
228
|
+
const issue = await readIssue(operation, path);
|
|
229
|
+
const section = statusSection(issue.body);
|
|
230
|
+
const history = partialHistory(section, { pull, remaining });
|
|
231
|
+
const count = history.partialRuns.length;
|
|
232
|
+
const label = count >= 3 ? "cloving: blocked" : "cloving: ready";
|
|
233
|
+
const body = withStatus(section.outside, history);
|
|
234
|
+
const labels = [
|
|
235
|
+
...issue.labels.filter((name) => ![
|
|
236
|
+
"cloving: ready",
|
|
237
|
+
"cloving: blocked",
|
|
238
|
+
"cloving: in progress",
|
|
239
|
+
].includes(name)),
|
|
240
|
+
label,
|
|
241
|
+
];
|
|
242
|
+
if (body === issue.body && sameLabels(issue.labels, labels))
|
|
243
|
+
return {
|
|
244
|
+
number: positiveInteger(operation, "number"),
|
|
245
|
+
ticketStatus: "partial",
|
|
246
|
+
};
|
|
247
|
+
const repository = `${text(operation, "owner")}/${text(operation, "repository")}`;
|
|
248
|
+
await operation.calls.gh([
|
|
249
|
+
"label",
|
|
250
|
+
"create",
|
|
251
|
+
label,
|
|
252
|
+
"--repo",
|
|
253
|
+
repository,
|
|
254
|
+
"--color",
|
|
255
|
+
"ededed",
|
|
256
|
+
"--force",
|
|
257
|
+
]);
|
|
258
|
+
await writeIssue(operation, {
|
|
259
|
+
path,
|
|
260
|
+
originalBody: issue.body,
|
|
261
|
+
body,
|
|
262
|
+
labels,
|
|
263
|
+
});
|
|
264
|
+
return {
|
|
265
|
+
number: positiveInteger(operation, "number"),
|
|
266
|
+
ticketStatus: "partial",
|
|
267
|
+
};
|
|
268
|
+
}
|
|
@@ -2,6 +2,7 @@ interface QueueSettings {
|
|
|
2
2
|
readonly directory: string;
|
|
3
3
|
readonly runtimeDirectory: string;
|
|
4
4
|
readonly intervalMilliseconds: number;
|
|
5
|
+
readonly resetQueue?: boolean;
|
|
5
6
|
}
|
|
6
7
|
export declare function issueQueue(settings: QueueSettings): {
|
|
7
8
|
readonly stop: () => Promise<void>;
|
|
@@ -1,10 +1,27 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { readFile, rename, writeFile } from "node:fs/promises";
|
|
2
|
+
import { readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { setTimeout as delay } from "node:timers/promises";
|
|
5
5
|
import { send } from "../cli/client.js";
|
|
6
6
|
import { record } from "../protocol.js";
|
|
7
7
|
import { runIdentity } from "../service/submission-handler.js";
|
|
8
|
+
function savedHold(value) {
|
|
9
|
+
if (value === undefined)
|
|
10
|
+
return undefined;
|
|
11
|
+
const held = record(value);
|
|
12
|
+
if (typeof held?.["mark"] !== "string" ||
|
|
13
|
+
typeof held["startedAt"] !== "number" ||
|
|
14
|
+
!Number.isFinite(held["startedAt"]) ||
|
|
15
|
+
held["startedAt"] <= 0)
|
|
16
|
+
throw new Error("invalid-queue-commands");
|
|
17
|
+
return { mark: held["mark"], startedAt: held["startedAt"] };
|
|
18
|
+
}
|
|
19
|
+
function identifiers(value) {
|
|
20
|
+
if (!Array.isArray(value) ||
|
|
21
|
+
!value.every((id) => typeof id === "string" && id.length > 0))
|
|
22
|
+
throw new Error("invalid-queue-commands");
|
|
23
|
+
return value.map(String);
|
|
24
|
+
}
|
|
8
25
|
async function journal(directory) {
|
|
9
26
|
const file = join(directory, "queue-commands.json");
|
|
10
27
|
const text = await readFile(file, "utf8").catch((error) => {
|
|
@@ -13,21 +30,26 @@ async function journal(directory) {
|
|
|
13
30
|
throw error;
|
|
14
31
|
});
|
|
15
32
|
const value = JSON.parse(text);
|
|
16
|
-
|
|
17
|
-
|
|
33
|
+
const saved = record(value);
|
|
34
|
+
const commands = new Set(identifiers(Array.isArray(value) ? value : saved?.["commands"]));
|
|
35
|
+
const failures = Array.isArray(value) ? [] : identifiers(saved?.["failures"]);
|
|
36
|
+
if (failures.length > 3)
|
|
18
37
|
throw new Error("invalid-queue-commands");
|
|
19
|
-
const
|
|
20
|
-
return {
|
|
38
|
+
const pending = {
|
|
21
39
|
commands,
|
|
40
|
+
failures,
|
|
22
41
|
retryCommandId: undefined,
|
|
42
|
+
selfUpdateHold: savedHold(saved?.["selfUpdateHold"]),
|
|
23
43
|
save: async () => {
|
|
24
|
-
await writeFile(`${file}.tmp`, JSON.stringify(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
44
|
+
await writeFile(`${file}.tmp`, JSON.stringify({
|
|
45
|
+
commands: [...commands],
|
|
46
|
+
failures,
|
|
47
|
+
selfUpdateHold: pending.selfUpdateHold,
|
|
48
|
+
}) + "\n", { mode: 0o600, flush: true });
|
|
28
49
|
await rename(`${file}.tmp`, file);
|
|
29
50
|
},
|
|
30
51
|
};
|
|
52
|
+
return pending;
|
|
31
53
|
}
|
|
32
54
|
function terminal(run) {
|
|
33
55
|
return ["completed", "failed", "cancelled"].includes(String(run["state"]));
|
|
@@ -54,8 +76,20 @@ async function recover(settings, pending) {
|
|
|
54
76
|
await pending.save();
|
|
55
77
|
}
|
|
56
78
|
}
|
|
79
|
+
function handedOff(run) {
|
|
80
|
+
const outcome = record(run["outcome"]);
|
|
81
|
+
return (run["state"] === "completed" &&
|
|
82
|
+
outcome?.["kind"] === "handoff_requested" &&
|
|
83
|
+
outcome["issueUpdate"] !== "failed");
|
|
84
|
+
}
|
|
85
|
+
class QueueFailureLimit extends Error {
|
|
86
|
+
}
|
|
87
|
+
function checkFailureLimit(pending) {
|
|
88
|
+
if (pending.failures.length >= 3)
|
|
89
|
+
throw new QueueFailureLimit(`3 runs in a row ended without handoff: ${pending.failures.join(", ")}`);
|
|
90
|
+
}
|
|
57
91
|
async function completed(settings, pending) {
|
|
58
|
-
|
|
92
|
+
checkFailureLimit(pending);
|
|
59
93
|
let pendingRun = false;
|
|
60
94
|
const tracked = [...pending.commands].filter((commandId) => commandId !== pending.retryCommandId);
|
|
61
95
|
for (const commandId of tracked) {
|
|
@@ -66,19 +100,18 @@ async function completed(settings, pending) {
|
|
|
66
100
|
pendingRun = true;
|
|
67
101
|
continue;
|
|
68
102
|
}
|
|
69
|
-
const outcome = record(run["outcome"]);
|
|
70
|
-
if (run["state"] !== "completed" ||
|
|
71
|
-
outcome?.["kind"] !== "handoff_requested")
|
|
72
|
-
failure ??= JSON.stringify({
|
|
73
|
-
runId: run["runId"],
|
|
74
|
-
state: run["state"],
|
|
75
|
-
outcome: run["outcome"] ?? null,
|
|
76
|
-
});
|
|
77
103
|
pending.commands.delete(commandId);
|
|
104
|
+
if (handedOff(run))
|
|
105
|
+
pending.failures.length = 0;
|
|
106
|
+
else
|
|
107
|
+
pending.failures.push(runIdentity(commandId));
|
|
78
108
|
await pending.save();
|
|
109
|
+
checkFailureLimit(pending);
|
|
110
|
+
if (!handedOff(run)) {
|
|
111
|
+
const outcome = record(run["outcome"]);
|
|
112
|
+
process.stderr.write(`Queue run ended ${String(outcome?.["kind"] ?? run["state"])}${outcome?.["issueUpdate"] === "failed" ? "; issueUpdate: failed" : ""}: ${runIdentity(commandId)}; continuing\n`);
|
|
113
|
+
}
|
|
79
114
|
}
|
|
80
|
-
if (failure !== undefined)
|
|
81
|
-
throw new Error(failure);
|
|
82
115
|
return pendingRun;
|
|
83
116
|
}
|
|
84
117
|
async function active(runtimeDirectory) {
|
|
@@ -121,9 +154,13 @@ async function submit(settings, pending) {
|
|
|
121
154
|
pending.retryCommandId = undefined;
|
|
122
155
|
if (reply.kind === "result")
|
|
123
156
|
return;
|
|
124
|
-
if (
|
|
157
|
+
if ([
|
|
158
|
+
"resolver-timeout",
|
|
159
|
+
"next-ticket-not-available",
|
|
160
|
+
"ticket-not-available",
|
|
161
|
+
].includes(reply.error.code)) {
|
|
125
162
|
pending.retryCommandId = commandId;
|
|
126
|
-
process.stderr.write(`Queue
|
|
163
|
+
process.stderr.write(`Queue ${reply.error.code}: command ${commandId}; will retry\n`);
|
|
127
164
|
return;
|
|
128
165
|
}
|
|
129
166
|
if (!["no-ready-ticket", "workflow-active"].includes(reply.error.code))
|
|
@@ -131,8 +168,59 @@ async function submit(settings, pending) {
|
|
|
131
168
|
pending.commands.delete(commandId);
|
|
132
169
|
await pending.save();
|
|
133
170
|
}
|
|
171
|
+
function updateHold(directory, pending) {
|
|
172
|
+
let notice;
|
|
173
|
+
return async () => {
|
|
174
|
+
const mark = await readFile(join(directory, "pending-self-update"), "utf8").catch((error) => {
|
|
175
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
176
|
+
return undefined;
|
|
177
|
+
throw error;
|
|
178
|
+
});
|
|
179
|
+
if (mark === undefined && pending.selfUpdateHold !== undefined) {
|
|
180
|
+
pending.selfUpdateHold = undefined;
|
|
181
|
+
await pending.save();
|
|
182
|
+
}
|
|
183
|
+
if (mark === undefined) {
|
|
184
|
+
notice = undefined;
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
const timestamp = Number(mark);
|
|
188
|
+
if (!Number.isFinite(timestamp) || timestamp <= 0)
|
|
189
|
+
throw new Error("invalid-pending-self-update");
|
|
190
|
+
if (pending.selfUpdateHold?.mark !== mark) {
|
|
191
|
+
pending.selfUpdateHold = { mark, startedAt: Date.now() };
|
|
192
|
+
await pending.save();
|
|
193
|
+
}
|
|
194
|
+
const holding = Date.now() - pending.selfUpdateHold.startedAt < 30 * 60 * 1000;
|
|
195
|
+
const line = holding
|
|
196
|
+
? "Queue holding for pending self-update\n"
|
|
197
|
+
: "Queue continuing after pending self-update time limit\n";
|
|
198
|
+
const key = mark + line;
|
|
199
|
+
if (notice !== key)
|
|
200
|
+
process.stderr.write(line);
|
|
201
|
+
notice = key;
|
|
202
|
+
return holding;
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
async function admission(settings, attempt) {
|
|
206
|
+
const file = join(settings.directory, "queue-submission");
|
|
207
|
+
await writeFile(file, "", { mode: 0o600 });
|
|
208
|
+
try {
|
|
209
|
+
if (!(await attempt.holding()) && !attempt.signal.aborted)
|
|
210
|
+
await submit(settings, attempt.pending);
|
|
211
|
+
}
|
|
212
|
+
finally {
|
|
213
|
+
await rm(file, { force: true });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
134
216
|
async function cycle(settings, signal) {
|
|
135
217
|
const pending = await journal(settings.directory);
|
|
218
|
+
if (settings.resetQueue === true) {
|
|
219
|
+
pending.failures.length = 0;
|
|
220
|
+
await pending.save();
|
|
221
|
+
}
|
|
222
|
+
const holding = updateHold(settings.directory, pending);
|
|
223
|
+
await rm(join(settings.directory, "queue-submission"), { force: true });
|
|
136
224
|
await recover(settings, pending);
|
|
137
225
|
const stopped = () => signal.aborted;
|
|
138
226
|
while (!stopped()) {
|
|
@@ -143,16 +231,19 @@ async function cycle(settings, signal) {
|
|
|
143
231
|
continue;
|
|
144
232
|
if (await active(settings.runtimeDirectory))
|
|
145
233
|
continue;
|
|
234
|
+
if (await holding())
|
|
235
|
+
continue;
|
|
146
236
|
if (stopped())
|
|
147
237
|
return;
|
|
148
|
-
await
|
|
238
|
+
await admission(settings, { pending, holding, signal });
|
|
149
239
|
}
|
|
150
240
|
}
|
|
151
241
|
export function issueQueue(settings) {
|
|
152
242
|
const stopping = new AbortController();
|
|
153
243
|
const running = cycle(settings, stopping.signal).catch((error) => {
|
|
154
244
|
const reason = error instanceof Error ? error.message : String(error);
|
|
155
|
-
|
|
245
|
+
const diagnostic = error instanceof QueueFailureLimit ? reason : JSON.stringify(reason);
|
|
246
|
+
process.stderr.write(`Queue stopped: ${diagnostic}\n`);
|
|
156
247
|
});
|
|
157
248
|
return {
|
|
158
249
|
stop: async () => {
|
|
@@ -32,6 +32,7 @@ export declare function readRecord(folder: string, name: string): Promise<unknow
|
|
|
32
32
|
export declare function writeRecord(folder: string, record: {
|
|
33
33
|
readonly name: string;
|
|
34
34
|
readonly value: unknown;
|
|
35
|
+
readonly permissions?: "private";
|
|
35
36
|
}): Promise<void>;
|
|
36
37
|
export declare function replied(identity: Identity, recorded: unknown): AdapterOutcome;
|
|
37
38
|
export declare function interrupted(reason: string): Recorded;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
|
+
import { chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
export function operationKey(text) {
|
|
5
5
|
return createHash("sha256").update(text).digest("hex").slice(0, 32);
|
|
@@ -16,7 +16,9 @@ export async function readRecord(folder, name) {
|
|
|
16
16
|
export async function writeRecord(folder, record) {
|
|
17
17
|
const path = join(folder, record.name);
|
|
18
18
|
await mkdir(folder, { recursive: true });
|
|
19
|
-
|
|
19
|
+
if (record.permissions === "private")
|
|
20
|
+
await chmod(folder, 0o700);
|
|
21
|
+
await writeFile(`${path}.staged`, JSON.stringify(record.value, null, 2), record.permissions === "private" ? { mode: 0o600 } : {});
|
|
20
22
|
await rename(`${path}.staged`, path);
|
|
21
23
|
}
|
|
22
24
|
export function replied(identity, recorded) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type RunningTurn, type TurnStart } from "./claude-turn.ts";
|
|
2
|
+
export interface RetrySettings {
|
|
3
|
+
readonly retryDelay?: (milliseconds: number, signal: AbortSignal) => Promise<void>;
|
|
4
|
+
}
|
|
5
|
+
interface RetryOptions extends RetrySettings {
|
|
6
|
+
readonly metadata: Readonly<Record<string, unknown>>;
|
|
7
|
+
}
|
|
8
|
+
export declare function startRetriedTurn(start: TurnStart, options: RetryOptions): Promise<RunningTurn>;
|
|
9
|
+
export {};
|