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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isTicketStatus } from "./ticket-status.js";
|
|
1
2
|
const identifier = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
2
3
|
const reason = /^\S[\s\S]{0,255}$/;
|
|
3
4
|
export function isBlockedReason(value) {
|
|
@@ -39,6 +40,7 @@ const outcomes = {
|
|
|
39
40
|
},
|
|
40
41
|
handoff_requested: {
|
|
41
42
|
keys: ["cycleId", "kind", "promotionId"],
|
|
43
|
+
optional: ["ticketStatus", "issueUpdate"],
|
|
42
44
|
parse: (fields) => {
|
|
43
45
|
const cycleId = textField(fields, {
|
|
44
46
|
key: "cycleId",
|
|
@@ -50,12 +52,24 @@ const outcomes = {
|
|
|
50
52
|
});
|
|
51
53
|
if (cycleId === null || promotionId === null)
|
|
52
54
|
return null;
|
|
53
|
-
|
|
55
|
+
const ticketStatus = fields["ticketStatus"];
|
|
56
|
+
const issueUpdate = fields["issueUpdate"];
|
|
57
|
+
if (issueUpdate !== undefined && issueUpdate !== "failed")
|
|
58
|
+
return null;
|
|
59
|
+
if (ticketStatus !== undefined && !isTicketStatus(ticketStatus))
|
|
60
|
+
return null;
|
|
61
|
+
return {
|
|
62
|
+
kind: "handoff_requested",
|
|
63
|
+
cycleId,
|
|
64
|
+
promotionId,
|
|
65
|
+
...(issueUpdate === "failed" ? { issueUpdate } : {}),
|
|
66
|
+
...(ticketStatus === undefined ? {} : { ticketStatus }),
|
|
67
|
+
};
|
|
54
68
|
},
|
|
55
69
|
},
|
|
56
70
|
};
|
|
57
71
|
export function validateDevelopmentInput(value) {
|
|
58
|
-
const fields = exactFields(value, ["cycleId"]);
|
|
72
|
+
const fields = exactFields(value, { required: ["cycleId"] });
|
|
59
73
|
const cycleId = fields && textField(fields, { key: "cycleId", pattern: identifier });
|
|
60
74
|
if (!cycleId)
|
|
61
75
|
return { kind: "invalid", reason: "invalid-development-input" };
|
|
@@ -66,18 +80,26 @@ export function validateCycleOutcome(value, input) {
|
|
|
66
80
|
const contract = typeof kind === "string" && Object.hasOwn(outcomes, kind)
|
|
67
81
|
? outcomes[kind]
|
|
68
82
|
: undefined;
|
|
69
|
-
const fields = contract &&
|
|
83
|
+
const fields = contract &&
|
|
84
|
+
exactFields(value, {
|
|
85
|
+
required: contract.keys,
|
|
86
|
+
optional: contract.optional ?? [],
|
|
87
|
+
});
|
|
70
88
|
const outcome = fields && contract.parse(fields);
|
|
71
89
|
if (!outcome || outcome.cycleId !== input.cycleId)
|
|
72
90
|
return { kind: "invalid", reason: "invalid-cycle-outcome" };
|
|
73
91
|
return { kind: "valid", value: outcome };
|
|
74
92
|
}
|
|
75
|
-
function exactFields(value,
|
|
93
|
+
function exactFields(value, shape) {
|
|
76
94
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
77
95
|
return null;
|
|
78
96
|
const fields = Object.fromEntries(Object.entries(value));
|
|
97
|
+
const keys = shape?.required;
|
|
98
|
+
const optional = shape?.optional ?? [];
|
|
79
99
|
if (keys &&
|
|
80
|
-
JSON.stringify(Object.keys(fields)
|
|
100
|
+
JSON.stringify(Object.keys(fields)
|
|
101
|
+
.filter((key) => !optional.includes(key))
|
|
102
|
+
.sort()) !== JSON.stringify(keys))
|
|
81
103
|
return null;
|
|
82
104
|
return fields;
|
|
83
105
|
}
|
|
@@ -2,7 +2,7 @@ import type { SubmissionContract } from "./submission.ts";
|
|
|
2
2
|
export interface WorkflowDeclaration {
|
|
3
3
|
readonly workflowId: string;
|
|
4
4
|
readonly effectLimit: number;
|
|
5
|
-
readonly capabilities: readonly ("claude" | "github")[];
|
|
5
|
+
readonly capabilities: readonly ("claude" | "github" | "chats")[];
|
|
6
6
|
readonly input: SubmissionContract;
|
|
7
7
|
}
|
|
8
8
|
export interface DeclaredWorkflow extends WorkflowDeclaration {
|
|
@@ -52,6 +52,8 @@ export function isWorkflowDeclaration(value) {
|
|
|
52
52
|
integer(effectLimit, { minimum: 1, maximum: 100 }) &&
|
|
53
53
|
Array.isArray(capabilities) &&
|
|
54
54
|
capabilities.length > 0 &&
|
|
55
|
-
capabilities.every((capability) => capability === "claude" ||
|
|
55
|
+
capabilities.every((capability) => capability === "claude" ||
|
|
56
|
+
capability === "github" ||
|
|
57
|
+
capability === "chats") &&
|
|
56
58
|
isInput(input));
|
|
57
59
|
}
|
package/dist/src/workflows.d.ts
CHANGED
|
@@ -3,3 +3,4 @@ export { protectedEffect } from "./steps/protected-effect.ts";
|
|
|
3
3
|
export { decisionGate } from "./steps/decision-gate.ts";
|
|
4
4
|
export type { GateRequest, GateResult, ProtectedRequest, ProtectedResult, ProtectedScope, } from "./step-bridge.ts";
|
|
5
5
|
export type { CycleOutcome, DevelopmentInput } from "./workflow-contracts.ts";
|
|
6
|
+
export { ticketStatus, type TicketStatus } from "./ticket-status.ts";
|
package/dist/src/workflows.js
CHANGED
|
@@ -4,7 +4,7 @@ import { extname, join } from "node:path";
|
|
|
4
4
|
import { setTimeout as delay } from "node:timers/promises";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
6
|
import { selfUpdater } from "./self-update.js";
|
|
7
|
-
import {
|
|
7
|
+
import { selfUpdateActive } from "./self-update-host.js";
|
|
8
8
|
function launched(argumentsList, settings) {
|
|
9
9
|
const child = spawn(process.execPath, argumentsList, {
|
|
10
10
|
cwd: settings.directory,
|
|
@@ -58,7 +58,11 @@ async function polled(settings, lifecycle) {
|
|
|
58
58
|
directory: settings.directory,
|
|
59
59
|
git: updatingGit(settings, lifecycle.stopping),
|
|
60
60
|
active: async () => lifecycle.stopping.aborted ||
|
|
61
|
-
(await
|
|
61
|
+
(await selfUpdateActive({
|
|
62
|
+
directory: settings.directory,
|
|
63
|
+
runtimeDirectory: settings.runtime,
|
|
64
|
+
signal: lifecycle.stopping,
|
|
65
|
+
})),
|
|
62
66
|
stop: async () => {
|
|
63
67
|
if (lifecycle.stopping.aborted)
|
|
64
68
|
throw new Error("self-update-stopped");
|
|
@@ -102,6 +106,7 @@ function hostedChildren(settings) {
|
|
|
102
106
|
environment: {
|
|
103
107
|
...process.env,
|
|
104
108
|
CLOVING_RUNTIME_DIR: settings.runtime,
|
|
109
|
+
CLOVING_WATCH_REPOSITORY: settings.repository,
|
|
105
110
|
},
|
|
106
111
|
}),
|
|
107
112
|
];
|
|
@@ -11,5 +11,5 @@ interface HostManifest {
|
|
|
11
11
|
}
|
|
12
12
|
export declare function workflowFiles(directory: string): Promise<string[]>;
|
|
13
13
|
export declare function workflowDeclarations(directory: string, manifests: Readonly<Record<string, HostManifest>>): Promise<readonly DeclaredWorkflow[]>;
|
|
14
|
-
export declare function workflowManifests(compiled: WorkflowManifest): Readonly<Record<string, HostManifest>>;
|
|
14
|
+
export declare function workflowManifests(compiled: WorkflowManifest, declarations?: readonly DeclaredWorkflow[]): Readonly<Record<string, HostManifest>>;
|
|
15
15
|
export {};
|
|
@@ -45,7 +45,7 @@ export async function workflowDeclarations(directory, manifests) {
|
|
|
45
45
|
}
|
|
46
46
|
return declarations;
|
|
47
47
|
}
|
|
48
|
-
export function workflowManifests(compiled) {
|
|
48
|
+
export function workflowManifests(compiled, declarations = []) {
|
|
49
49
|
const names = new Set();
|
|
50
50
|
const steps = Object.values(compiled.steps ?? {})
|
|
51
51
|
.flatMap((entries) => Object.values(entries))
|
|
@@ -68,7 +68,7 @@ export function workflowManifests(compiled) {
|
|
|
68
68
|
name: basename(file, extname(file)),
|
|
69
69
|
workflowId,
|
|
70
70
|
steps,
|
|
71
|
-
capabilities: ["claude", "github"],
|
|
71
|
+
capabilities: declarations.find((declaration) => declaration.functionName === functionName)?.capabilities ?? ["claude", "github"],
|
|
72
72
|
resources: [],
|
|
73
73
|
artifacts: "artifacts",
|
|
74
74
|
},
|
package/dist/tools/self-host.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isProfilesFile } from "../src/turn-profile.js";
|
|
2
|
+
import { parseTokenPrices } from "../src/token-prices.js";
|
|
2
3
|
import { execFileSync, spawn } from "node:child_process";
|
|
3
4
|
import { copyFile, mkdir, readFile, rm, symlink, writeFile, } from "node:fs/promises";
|
|
4
5
|
import { dirname, extname, join, resolve } from "node:path";
|
|
@@ -156,13 +157,9 @@ async function registered() {
|
|
|
156
157
|
const manifests = workflowManifests(compiled);
|
|
157
158
|
const declarations = await workflowDeclarations(join(product, "workflows"), manifests);
|
|
158
159
|
const paths = new Map();
|
|
159
|
-
for (const [functionName, manifest] of Object.entries(
|
|
160
|
+
for (const [functionName, manifest] of Object.entries(workflowManifests(compiled, declarations))) {
|
|
160
161
|
const path = join(project, `${manifest.name}.workflow.json`);
|
|
161
|
-
|
|
162
|
-
await writeFile(path, JSON.stringify({
|
|
163
|
-
...manifest,
|
|
164
|
-
capabilities: declared?.capabilities ?? manifest.capabilities,
|
|
165
|
-
}));
|
|
162
|
+
await writeFile(path, JSON.stringify(manifest));
|
|
166
163
|
paths.set(functionName, path);
|
|
167
164
|
}
|
|
168
165
|
return { manifests: Object.fromEntries(paths), declarations };
|
|
@@ -229,6 +226,7 @@ async function prepared() {
|
|
|
229
226
|
throw new Error("The workflows did not compile.");
|
|
230
227
|
return registered();
|
|
231
228
|
}
|
|
229
|
+
const tokenPrices = parseTokenPrices(process.env["CLOVING_TOKEN_PRICES"]);
|
|
232
230
|
const profiles = await profilesFile();
|
|
233
231
|
const worker = selectedWorker();
|
|
234
232
|
const command = worker === "codex"
|
|
@@ -239,12 +237,14 @@ const ticketCommand = listed("CLOVING_TICKET_COMMAND");
|
|
|
239
237
|
const queueMilliseconds = queueInterval({ nextCommand, ticketCommand });
|
|
240
238
|
const manifests = await prepared();
|
|
241
239
|
await launchers({
|
|
240
|
+
tokenPrices,
|
|
242
241
|
worker,
|
|
243
242
|
profiles,
|
|
244
243
|
command,
|
|
245
244
|
ticketCommand,
|
|
246
245
|
nextCommand,
|
|
247
246
|
queueMilliseconds,
|
|
247
|
+
resetQueue: flags.includes("--reset-queue"),
|
|
248
248
|
notifyCommand: listed("CLOVING_NOTIFY_COMMAND"),
|
|
249
249
|
githubCommand: listed("CLOVING_GH_COMMAND"),
|
|
250
250
|
checkCommand: listed("CLOVING_CHECK_COMMAND"),
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
+
interface ActivitySettings {
|
|
2
|
+
readonly directory: string;
|
|
3
|
+
readonly runtimeDirectory: string;
|
|
4
|
+
readonly signal?: AbortSignal;
|
|
5
|
+
}
|
|
6
|
+
export declare function selfUpdateActive(settings: ActivitySettings): Promise<boolean>;
|
|
1
7
|
export declare function selfUpdateMilliseconds(settings: {
|
|
2
8
|
readonly seconds: string | undefined;
|
|
3
9
|
readonly repository: string;
|
|
4
10
|
readonly product: string;
|
|
5
11
|
}): Promise<number | undefined>;
|
|
6
12
|
export declare function workerTurnActive(directory: string): Promise<boolean>;
|
|
13
|
+
export {};
|
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
import { access, readdir, realpath } from "node:fs/promises";
|
|
2
|
-
import {
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { extname, join } from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { record } from "../src/protocol.js";
|
|
6
|
+
async function runPage(settings, after) {
|
|
7
|
+
const output = await promisify(execFile)(process.execPath, [
|
|
8
|
+
join(import.meta.dirname, `../src/cli${extname(import.meta.filename)}`),
|
|
9
|
+
"status",
|
|
10
|
+
"--json",
|
|
11
|
+
"--limit",
|
|
12
|
+
"500",
|
|
13
|
+
...(after === "" ? [] : ["--after", after]),
|
|
14
|
+
], {
|
|
15
|
+
env: { ...process.env, CLOVING_RUNTIME_DIR: settings.runtimeDirectory },
|
|
16
|
+
encoding: "utf8",
|
|
17
|
+
timeout: 30000,
|
|
18
|
+
signal: settings.signal,
|
|
19
|
+
});
|
|
20
|
+
const page = record(JSON.parse(output.stdout));
|
|
21
|
+
if (!page)
|
|
22
|
+
throw new Error("invalid-run-list");
|
|
23
|
+
return page;
|
|
24
|
+
}
|
|
25
|
+
export async function selfUpdateActive(settings) {
|
|
26
|
+
if (await present(join(settings.directory, "queue-submission")))
|
|
27
|
+
return true;
|
|
28
|
+
if (await workerTurnActive(settings.directory))
|
|
29
|
+
return true;
|
|
30
|
+
let after = "";
|
|
31
|
+
do {
|
|
32
|
+
const page = await runPage(settings, after);
|
|
33
|
+
const runs = page["runs"];
|
|
34
|
+
if (!Array.isArray(runs))
|
|
35
|
+
throw new Error("invalid-run-list");
|
|
36
|
+
if (runs.some((value) => {
|
|
37
|
+
const run = record(value);
|
|
38
|
+
if (!run)
|
|
39
|
+
throw new Error("invalid-run-status");
|
|
40
|
+
return !["completed", "failed", "cancelled"].includes(String(run["state"]));
|
|
41
|
+
}))
|
|
42
|
+
return true;
|
|
43
|
+
const next = page["next"];
|
|
44
|
+
if (next !== null && (typeof next !== "string" || next <= after))
|
|
45
|
+
throw new Error("invalid-run-page");
|
|
46
|
+
after = next;
|
|
47
|
+
} while (after !== null);
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
3
50
|
export async function selfUpdateMilliseconds(settings) {
|
|
4
51
|
const { seconds } = settings;
|
|
5
52
|
if (seconds === undefined)
|
|
@@ -5,6 +5,7 @@ export interface SelfUpdateSettings {
|
|
|
5
5
|
readonly stop: () => Promise<void>;
|
|
6
6
|
readonly error: (line: string) => void;
|
|
7
7
|
}
|
|
8
|
+
export declare function lockRequiresInstall(before: string, after: string): boolean;
|
|
8
9
|
export declare function selfUpdater(settings: SelfUpdateSettings): {
|
|
9
10
|
readonly check: () => Promise<number | undefined>;
|
|
10
11
|
};
|
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
export function lockRequiresInstall(before, after) {
|
|
2
|
+
try {
|
|
3
|
+
return !isDeepStrictEqual(lockPackages(before), lockPackages(after));
|
|
4
|
+
}
|
|
5
|
+
catch {
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
function lockPackages(text) {
|
|
10
|
+
const lock = JSON.parse(text);
|
|
11
|
+
if (typeof lock !== "object" || lock === null || !("packages" in lock))
|
|
12
|
+
throw new Error("invalid package lock");
|
|
13
|
+
const packages = lock.packages;
|
|
14
|
+
if (typeof packages !== "object" ||
|
|
15
|
+
packages === null ||
|
|
16
|
+
Array.isArray(packages))
|
|
17
|
+
throw new Error("invalid package lock packages");
|
|
18
|
+
return Object.fromEntries(Object.entries(packages).filter(([path]) => path !== ""));
|
|
19
|
+
}
|
|
1
20
|
async function clean(git) {
|
|
2
21
|
const status = await git([
|
|
3
22
|
"status",
|
|
@@ -9,6 +28,24 @@ async function clean(git) {
|
|
|
9
28
|
return false;
|
|
10
29
|
return !/^[a-zS] /m.test(await git(["ls-files", "-v"]));
|
|
11
30
|
}
|
|
31
|
+
async function installRequired(git, update) {
|
|
32
|
+
try {
|
|
33
|
+
await git([
|
|
34
|
+
"diff",
|
|
35
|
+
"--exit-code",
|
|
36
|
+
update.head,
|
|
37
|
+
update.target,
|
|
38
|
+
"--",
|
|
39
|
+
"package-lock.json",
|
|
40
|
+
]);
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
const before = await git(["show", `${update.head}:package-lock.json`]);
|
|
45
|
+
const after = await git(["show", `${update.target}:package-lock.json`]);
|
|
46
|
+
return lockRequiresInstall(before, after);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
12
49
|
async function candidate(settings) {
|
|
13
50
|
const git = settings.git;
|
|
14
51
|
await git(["fetch", "origin"]);
|
|
@@ -26,10 +63,40 @@ async function candidate(settings) {
|
|
|
26
63
|
await git(["merge-base", "--is-ancestor", head, target]);
|
|
27
64
|
if (!(await clean(git)))
|
|
28
65
|
return;
|
|
66
|
+
await markPending(settings.directory);
|
|
29
67
|
if (await settings.active())
|
|
30
|
-
return;
|
|
68
|
+
return "waiting";
|
|
31
69
|
return { branch, head, target };
|
|
32
70
|
}
|
|
71
|
+
async function markPending(directory, publication = "initial") {
|
|
72
|
+
if (publication === "renew") {
|
|
73
|
+
const timestamp = Number(await readFile(join(directory, "pending-self-update"), "utf8"));
|
|
74
|
+
if (timestamp > 0 && Date.now() - timestamp < 30 * 60 * 1000)
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
await mkdir(directory, { recursive: true });
|
|
78
|
+
const temporary = join(directory, `pending-self-update-${randomUUID()}.tmp`);
|
|
79
|
+
try {
|
|
80
|
+
await writeFile(temporary, `${String(Date.now())}\n`, {
|
|
81
|
+
flag: "wx",
|
|
82
|
+
mode: 0o600,
|
|
83
|
+
flush: true,
|
|
84
|
+
});
|
|
85
|
+
if (publication === "renew") {
|
|
86
|
+
await rename(temporary, join(directory, "pending-self-update"));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
await link(temporary, join(directory, "pending-self-update")).catch((error) => {
|
|
90
|
+
if (!(error instanceof Error) ||
|
|
91
|
+
!("code" in error) ||
|
|
92
|
+
error.code !== "EEXIST")
|
|
93
|
+
throw error;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
await rm(temporary, { force: true });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
33
100
|
async function failedTargets(directory) {
|
|
34
101
|
try {
|
|
35
102
|
return (await readFile(join(directory, "failed-self-updates"), "utf8")).split("\n");
|
|
@@ -70,21 +137,16 @@ export function selfUpdater(settings) {
|
|
|
70
137
|
if (disabled || checking)
|
|
71
138
|
return;
|
|
72
139
|
checking = true;
|
|
140
|
+
let waiting = false;
|
|
73
141
|
try {
|
|
74
142
|
const update = await candidate(settings).catch(() => undefined);
|
|
75
|
-
if (
|
|
143
|
+
if (update === "waiting") {
|
|
144
|
+
waiting = true;
|
|
76
145
|
return;
|
|
77
|
-
try {
|
|
78
|
-
await settings.git([
|
|
79
|
-
"diff",
|
|
80
|
-
"--exit-code",
|
|
81
|
-
update.head,
|
|
82
|
-
update.target,
|
|
83
|
-
"--",
|
|
84
|
-
"package-lock.json",
|
|
85
|
-
]);
|
|
86
146
|
}
|
|
87
|
-
|
|
147
|
+
if (!update)
|
|
148
|
+
return;
|
|
149
|
+
if (await installRequired(settings.git, update).catch(() => true)) {
|
|
88
150
|
disabled = true;
|
|
89
151
|
settings.error("Self-update disabled: package-lock.json changed; update and install by hand.\n");
|
|
90
152
|
return;
|
|
@@ -92,14 +154,24 @@ export function selfUpdater(settings) {
|
|
|
92
154
|
const failed = await failedTargets(settings.directory).catch(() => undefined);
|
|
93
155
|
if (!failed || failed.includes(update.target))
|
|
94
156
|
return;
|
|
157
|
+
await markPending(settings.directory, "renew");
|
|
158
|
+
waiting = await settings.active().catch(() => true);
|
|
159
|
+
if (waiting)
|
|
160
|
+
return;
|
|
95
161
|
disabled = true;
|
|
96
162
|
return await updated(settings, update);
|
|
97
163
|
}
|
|
98
164
|
finally {
|
|
99
165
|
checking = false;
|
|
166
|
+
if (!waiting)
|
|
167
|
+
await rm(join(settings.directory, "pending-self-update"), {
|
|
168
|
+
force: true,
|
|
169
|
+
});
|
|
100
170
|
}
|
|
101
171
|
},
|
|
102
172
|
};
|
|
103
173
|
}
|
|
104
|
-
import {
|
|
174
|
+
import { randomUUID } from "node:crypto";
|
|
175
|
+
import { appendFile, link, mkdir, readFile, rename, rm, writeFile, } from "node:fs/promises";
|
|
105
176
|
import { join } from "node:path";
|
|
177
|
+
import { isDeepStrictEqual } from "node:util";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CycleOutcome } from "cloving/workflows";
|
|
2
|
+
interface AnalysisInput {
|
|
3
|
+
readonly cycleId: string;
|
|
4
|
+
readonly repository: string;
|
|
5
|
+
readonly input: {
|
|
6
|
+
readonly importId: string;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export declare function analyzeChats(input: AnalysisInput): Promise<CycleOutcome>;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { protectedEffect } from "cloving/workflows/steps/protected-effect";
|
|
2
|
+
function request(importId, step) {
|
|
3
|
+
const { operation, payload } = step;
|
|
4
|
+
return {
|
|
5
|
+
scope: { phase: "analysis", operation, round: 1 },
|
|
6
|
+
capability: operation === "turn-analysis" ? "claude" : "chats",
|
|
7
|
+
method: operation === "turn-analysis" ? "run-turn:analyze-chats" : operation,
|
|
8
|
+
target: importId,
|
|
9
|
+
payload: JSON.stringify(payload),
|
|
10
|
+
units: 1,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function blocked(cycleId, failure) {
|
|
14
|
+
const { operation, result } = failure;
|
|
15
|
+
return {
|
|
16
|
+
kind: "blocked",
|
|
17
|
+
cycleId,
|
|
18
|
+
evidenceId: operation,
|
|
19
|
+
reason: "reason" in result && result.reason !== null
|
|
20
|
+
? result.reason
|
|
21
|
+
: result.kind,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function record(value) {
|
|
25
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
26
|
+
}
|
|
27
|
+
function fields(result) {
|
|
28
|
+
try {
|
|
29
|
+
const value = JSON.parse(result ?? "null");
|
|
30
|
+
return record(value) ? value : {};
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export async function analyzeChats(input) {
|
|
37
|
+
"use workflow";
|
|
38
|
+
const { cycleId } = input;
|
|
39
|
+
const { importId } = input.input;
|
|
40
|
+
const read = await protectedEffect(request(importId, { operation: "read-analysis", payload: { importId } }));
|
|
41
|
+
if (read.kind !== "completed")
|
|
42
|
+
return blocked(cycleId, { operation: "read-analysis", result: read });
|
|
43
|
+
const snapshot = fields(read.result)["snapshot"];
|
|
44
|
+
if (!record(snapshot) || snapshot["importId"] !== importId)
|
|
45
|
+
return {
|
|
46
|
+
kind: "blocked",
|
|
47
|
+
cycleId,
|
|
48
|
+
reason: "analysis-input-refused",
|
|
49
|
+
evidenceId: "read-analysis",
|
|
50
|
+
};
|
|
51
|
+
const turn = await protectedEffect(request(importId, { operation: "turn-analysis", payload: { snapshot } }));
|
|
52
|
+
if (turn.kind !== "completed")
|
|
53
|
+
return blocked(cycleId, { operation: "turn-analysis", result: turn });
|
|
54
|
+
const reply = fields(turn.result)["reply"];
|
|
55
|
+
if (typeof reply !== "string")
|
|
56
|
+
return {
|
|
57
|
+
kind: "blocked",
|
|
58
|
+
cycleId,
|
|
59
|
+
reason: "analysis-turn-failed",
|
|
60
|
+
evidenceId: "turn-analysis",
|
|
61
|
+
};
|
|
62
|
+
const save = await protectedEffect(request(importId, {
|
|
63
|
+
operation: "save-analysis",
|
|
64
|
+
payload: { importId, cycleId, snapshot, reply },
|
|
65
|
+
}));
|
|
66
|
+
if (save.kind !== "completed")
|
|
67
|
+
return blocked(cycleId, { operation: "save-analysis", result: save });
|
|
68
|
+
return {
|
|
69
|
+
kind: "handoff_requested",
|
|
70
|
+
cycleId,
|
|
71
|
+
promotionId: `analysis:${importId}:${cycleId}`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import type { CycleOutcome } from "cloving/workflows";
|
|
2
|
-
|
|
3
|
-
readonly cycleId: string;
|
|
4
|
-
readonly ticket: string;
|
|
5
|
-
readonly ticketId?: string;
|
|
6
|
-
readonly repository: string;
|
|
7
|
-
readonly delivery?: string;
|
|
8
|
-
}
|
|
2
|
+
import { type Ticket } from "./ticket-prompts.ts";
|
|
9
3
|
interface Decision {
|
|
10
4
|
readonly gateId: string;
|
|
11
5
|
readonly decision: string;
|