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.
Files changed (177) hide show
  1. package/README.md +145 -14
  2. package/dist/src/authority/commands.js +10 -2
  3. package/dist/src/authority/reconciliation.d.ts +2 -0
  4. package/dist/src/authority/reconciliation.js +10 -1
  5. package/dist/src/authority/transitions.d.ts +1 -1
  6. package/dist/src/authority/transitions.js +2 -0
  7. package/dist/src/broker/actor/authority-session.d.ts +2 -0
  8. package/dist/src/broker/actor/authority-session.js +3 -4
  9. package/dist/src/chats/analysis-batches.d.ts +24 -0
  10. package/dist/src/chats/analysis-batches.js +83 -0
  11. package/dist/src/chats/analysis-contract.d.ts +80 -0
  12. package/dist/src/chats/analysis-contract.js +16 -0
  13. package/dist/src/chats/analysis-data.d.ts +3 -0
  14. package/dist/src/chats/analysis-data.js +85 -0
  15. package/dist/src/chats/analysis-input.d.ts +12 -0
  16. package/dist/src/chats/analysis-input.js +83 -0
  17. package/dist/src/chats/analysis-links.d.ts +2 -0
  18. package/dist/src/chats/analysis-links.js +25 -0
  19. package/dist/src/chats/analysis-prompt.d.ts +10 -0
  20. package/dist/src/chats/analysis-prompt.js +16 -0
  21. package/dist/src/chats/analysis-proposal.d.ts +2 -0
  22. package/dist/src/chats/analysis-proposal.js +32 -0
  23. package/dist/src/chats/analysis-provenance.d.ts +4 -0
  24. package/dist/src/chats/analysis-provenance.js +99 -0
  25. package/dist/src/chats/analysis-shape.d.ts +3 -0
  26. package/dist/src/chats/analysis-shape.js +228 -0
  27. package/dist/src/chats/analysis.d.ts +3 -0
  28. package/dist/src/chats/analysis.js +42 -0
  29. package/dist/src/chats/corpus-input.d.ts +5 -0
  30. package/dist/src/chats/corpus-input.js +288 -0
  31. package/dist/src/chats/destination.js +3 -1
  32. package/dist/src/chats/import.d.ts +8 -6
  33. package/dist/src/chats/import.js +195 -23
  34. package/dist/src/chats/input-file.d.ts +3 -0
  35. package/dist/src/chats/input-file.js +32 -0
  36. package/dist/src/chats/normalize.d.ts +11 -2
  37. package/dist/src/chats/normalize.js +18 -2
  38. package/dist/src/chats/previous-import.d.ts +5 -0
  39. package/dist/src/chats/previous-import.js +34 -0
  40. package/dist/src/chats/reader.d.ts +7 -1
  41. package/dist/src/chats/reader.js +46 -15
  42. package/dist/src/chats/redact-import.d.ts +14 -0
  43. package/dist/src/chats/redact-import.js +141 -0
  44. package/dist/src/chats/redaction.d.ts +41 -0
  45. package/dist/src/chats/redaction.js +161 -0
  46. package/dist/src/chats/revision-input.d.ts +3 -0
  47. package/dist/src/chats/revision-input.js +180 -0
  48. package/dist/src/chats/revision.d.ts +48 -0
  49. package/dist/src/chats/revision.js +94 -0
  50. package/dist/src/chats/selection.d.ts +25 -0
  51. package/dist/src/chats/selection.js +109 -0
  52. package/dist/src/chats/source.d.ts +23 -0
  53. package/dist/src/chats/source.js +135 -0
  54. package/dist/src/chats/structure.d.ts +57 -0
  55. package/dist/src/chats/structure.js +216 -0
  56. package/dist/src/cli/arguments.d.ts +8 -0
  57. package/dist/src/cli/arguments.js +94 -13
  58. package/dist/src/cli/client.d.ts +1 -0
  59. package/dist/src/cli/client.js +38 -1
  60. package/dist/src/cli/commands.d.ts +3 -0
  61. package/dist/src/cli/commands.js +29 -11
  62. package/dist/src/cli/main.d.ts +2 -1
  63. package/dist/src/cli/main.js +61 -14
  64. package/dist/src/cli/render.d.ts +2 -0
  65. package/dist/src/cli/render.js +70 -2
  66. package/dist/src/cli/usage-render.d.ts +3 -0
  67. package/dist/src/cli/usage-render.js +28 -0
  68. package/dist/src/cli/usage.d.ts +3 -2
  69. package/dist/src/cli/usage.js +38 -6
  70. package/dist/src/cli/watch-actions.d.ts +10 -0
  71. package/dist/src/cli/watch-actions.js +58 -0
  72. package/dist/src/cli/watch-board-view.d.ts +32 -0
  73. package/dist/src/cli/watch-board-view.js +88 -15
  74. package/dist/src/cli/watch-board.js +6 -1
  75. package/dist/src/cli/watch-card-client.d.ts +1 -0
  76. package/dist/src/cli/watch-card-client.js +44 -0
  77. package/dist/src/cli/watch-card-forms.d.ts +2 -0
  78. package/dist/src/cli/watch-card-forms.js +33 -0
  79. package/dist/src/cli/watch-card-reply.d.ts +5 -0
  80. package/dist/src/cli/watch-card-reply.js +34 -0
  81. package/dist/src/cli/watch-card-view.d.ts +40 -0
  82. package/dist/src/cli/watch-card-view.js +86 -0
  83. package/dist/src/cli/watch-client.d.ts +1 -0
  84. package/dist/src/cli/watch-client.js +118 -0
  85. package/dist/src/cli/watch-model.js +31 -14
  86. package/dist/src/cli/watch-page.d.ts +1 -1
  87. package/dist/src/cli/watch-page.js +71 -85
  88. package/dist/src/cli/watch-pr-board.d.ts +3 -0
  89. package/dist/src/cli/watch-pr-board.js +26 -0
  90. package/dist/src/cli/watch-pr-command.d.ts +15 -0
  91. package/dist/src/cli/watch-pr-command.js +37 -0
  92. package/dist/src/cli/watch-pr-identity.d.ts +20 -0
  93. package/dist/src/cli/watch-pr-identity.js +62 -0
  94. package/dist/src/cli/watch-pr-observer.d.ts +19 -0
  95. package/dist/src/cli/watch-pr-observer.js +108 -0
  96. package/dist/src/cli/watch-pr-settings.d.ts +8 -0
  97. package/dist/src/cli/watch-pr-settings.js +47 -0
  98. package/dist/src/cli/watch-reads.js +4 -0
  99. package/dist/src/cli/watch-render.d.ts +16 -2
  100. package/dist/src/cli/watch-render.js +10 -3
  101. package/dist/src/cli/watch-run-input.d.ts +14 -0
  102. package/dist/src/cli/watch-run-input.js +49 -0
  103. package/dist/src/cli/watch-server.d.ts +3 -0
  104. package/dist/src/cli/watch-server.js +44 -6
  105. package/dist/src/cli/watch-start-view.d.ts +18 -0
  106. package/dist/src/cli/watch-start-view.js +77 -0
  107. package/dist/src/cli/watch-turn.d.ts +1 -0
  108. package/dist/src/cli/watch-turn.js +7 -2
  109. package/dist/src/cli/watch-usage.d.ts +6 -0
  110. package/dist/src/cli/watch-usage.js +65 -0
  111. package/dist/src/cli/watch-view.d.ts +15 -0
  112. package/dist/src/cli/watch-view.js +61 -17
  113. package/dist/src/cli.js +48 -6
  114. package/dist/src/daemon/analysis-launch.d.ts +5 -0
  115. package/dist/src/daemon/analysis-launch.js +81 -0
  116. package/dist/src/daemon/analysis-token.d.ts +6 -0
  117. package/dist/src/daemon/analysis-token.js +88 -0
  118. package/dist/src/daemon/analysis-worker.d.ts +22 -0
  119. package/dist/src/daemon/analysis-worker.js +219 -0
  120. package/dist/src/daemon/chats-files.d.ts +15 -0
  121. package/dist/src/daemon/chats-files.js +113 -0
  122. package/dist/src/daemon/chats-worker.d.ts +12 -0
  123. package/dist/src/daemon/chats-worker.js +200 -0
  124. package/dist/src/daemon/claude-turn.d.ts +10 -0
  125. package/dist/src/daemon/claude-turn.js +96 -15
  126. package/dist/src/daemon/claude-worker.d.ts +4 -1
  127. package/dist/src/daemon/claude-worker.js +99 -47
  128. package/dist/src/daemon/codex-stream.js +16 -0
  129. package/dist/src/daemon/daemon.d.ts +3 -0
  130. package/dist/src/daemon/daemon.js +16 -7
  131. package/dist/src/daemon/github-methods.js +4 -0
  132. package/dist/src/daemon/github-ticket-status.d.ts +3 -0
  133. package/dist/src/daemon/github-ticket-status.js +268 -0
  134. package/dist/src/daemon/issue-queue.d.ts +1 -0
  135. package/dist/src/daemon/issue-queue.js +115 -24
  136. package/dist/src/daemon/operation-journal.d.ts +1 -0
  137. package/dist/src/daemon/operation-journal.js +4 -2
  138. package/dist/src/daemon/retrying-turn.d.ts +9 -0
  139. package/dist/src/daemon/retrying-turn.js +185 -0
  140. package/dist/src/host/world-reconciliation.d.ts +2 -0
  141. package/dist/src/host/world-reconciliation.js +6 -0
  142. package/dist/src/host/world-runs.js +9 -1
  143. package/dist/src/service/run-handlers.js +15 -23
  144. package/dist/src/service/worker-totals.d.ts +3 -0
  145. package/dist/src/service/worker-totals.js +28 -0
  146. package/dist/src/ticket-status.d.ts +8 -0
  147. package/dist/src/ticket-status.js +45 -0
  148. package/dist/src/token-prices.d.ts +19 -0
  149. package/dist/src/token-prices.js +76 -0
  150. package/dist/src/worker-usage.d.ts +22 -0
  151. package/dist/src/worker-usage.js +44 -0
  152. package/dist/src/workflow-candidate.d.ts +29 -0
  153. package/dist/src/workflow-candidate.js +59 -0
  154. package/dist/src/workflow-contracts.d.ts +2 -0
  155. package/dist/src/workflow-contracts.js +27 -5
  156. package/dist/src/workflow-declaration.d.ts +1 -1
  157. package/dist/src/workflow-declaration.js +3 -1
  158. package/dist/src/workflows.d.ts +1 -0
  159. package/dist/src/workflows.js +1 -0
  160. package/dist/tools/self-host-processes.js +7 -2
  161. package/dist/tools/self-host-workflows.d.ts +1 -1
  162. package/dist/tools/self-host-workflows.js +2 -2
  163. package/dist/tools/self-host.js +6 -6
  164. package/dist/tools/self-update-host.d.ts +7 -0
  165. package/dist/tools/self-update-host.js +48 -1
  166. package/dist/tools/self-update.d.ts +1 -0
  167. package/dist/tools/self-update.js +85 -13
  168. package/dist/workflows/analyze-chats.d.ts +10 -0
  169. package/dist/workflows/analyze-chats.declaration.json +9 -0
  170. package/dist/workflows/analyze-chats.js +73 -0
  171. package/dist/workflows/develop-ticket.d.ts +1 -7
  172. package/dist/workflows/develop-ticket.js +42 -141
  173. package/dist/workflows/issue-labels.d.ts +3 -1
  174. package/dist/workflows/issue-labels.js +34 -0
  175. package/dist/workflows/ticket-prompts.d.ts +35 -0
  176. package/dist/workflows/ticket-prompts.js +133 -0
  177. 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
- return { kind: "handoff_requested", cycleId, promotionId };
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 && exactFields(value, contract.keys);
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, keys) {
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).sort()) !== JSON.stringify(keys))
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" || capability === "github") &&
55
+ capabilities.every((capability) => capability === "claude" ||
56
+ capability === "github" ||
57
+ capability === "chats") &&
56
58
  isInput(input));
57
59
  }
@@ -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";
@@ -1,3 +1,4 @@
1
1
  export { loadCycle } from "./steps/load-cycle.js";
2
2
  export { protectedEffect } from "./steps/protected-effect.js";
3
3
  export { decisionGate } from "./steps/decision-gate.js";
4
+ export { ticketStatus } from "./ticket-status.js";
@@ -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 { workerTurnActive } from "./self-update-host.js";
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 workerTurnActive(settings.directory)),
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
  },
@@ -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(manifests)) {
160
+ for (const [functionName, manifest] of Object.entries(workflowManifests(compiled, declarations))) {
160
161
  const path = join(project, `${manifest.name}.workflow.json`);
161
- const declared = declarations.find((entry) => entry.functionName === functionName);
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 { join } from "node:path";
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 (!update)
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
- catch {
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 { appendFile, mkdir, readFile } from "node:fs/promises";
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,9 @@
1
+ {
2
+ "workflowId": "analyze-chats",
3
+ "effectLimit": 3,
4
+ "capabilities": ["chats", "claude"],
5
+ "input": {
6
+ "maximumBytes": 4096,
7
+ "fields": { "importId": { "type": "string" } }
8
+ }
9
+ }
@@ -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
- interface Ticket {
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;