codeden 0.4.3 → 0.4.4

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 (27) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +1 -1
  3. package/dist/{main-CtuRKdHE.js → main-CKLHi4sz.js} +292 -47
  4. package/dist/main-CKLHi4sz.js.map +1 -0
  5. package/dist/{main-BsCZetsu.js → main-D335_24e.js} +2 -2
  6. package/dist/{main-BsCZetsu.js.map → main-D335_24e.js.map} +1 -1
  7. package/dist/{runner-BPkTmgxq.js → runner-DjavcMnG.js} +83 -8
  8. package/dist/runner-DjavcMnG.js.map +1 -0
  9. package/dist/runner.js +1 -1
  10. package/dist/web/assets/{core-BSglb5T2.js → core-BFbrQR07.js} +1 -1
  11. package/dist/web/assets/engine-oniguruma-Dod-24Yp.js +1 -0
  12. package/dist/web/assets/{highlighted-body-JLMTFACH-C4JIAaW0.js → highlighted-body-JLMTFACH-C9Ii0196.js} +1 -1
  13. package/dist/web/assets/{highlights-BiFbzXfR.js → highlights-C4aNCgJw.js} +1 -1
  14. package/dist/web/assets/{index-B175wXOK.css → index-BIBpPpjX.css} +1 -1
  15. package/dist/web/assets/{index-Dwp28BCB.js → index-WLihS360.js} +202 -202
  16. package/dist/web/assets/langs-X885YKmd.js +1 -0
  17. package/dist/web/assets/mermaid-WFAXDIZX-Dh9Bmb5R.js +1 -0
  18. package/dist/web/assets/{shiki-BJFGjRKC.js → shiki-MNiRnTih.js} +2 -2
  19. package/dist/web/assets/{stream-tokenizer-FM7Mm_zq.js → stream-tokenizer-BOiamYv-.js} +1 -1
  20. package/dist/web/assets/{themes-EFt_x-Bi.js → themes-DPD-gG-5.js} +2 -2
  21. package/dist/web/index.html +2 -2
  22. package/package.json +1 -1
  23. package/dist/main-CtuRKdHE.js.map +0 -1
  24. package/dist/runner-BPkTmgxq.js.map +0 -1
  25. package/dist/web/assets/engine-oniguruma-DShJzqyt.js +0 -1
  26. package/dist/web/assets/langs-Fwnh7v07.js +0 -1
  27. package/dist/web/assets/mermaid-WFAXDIZX-dh5FbPgs.js +0 -1
package/README.md CHANGED
@@ -125,7 +125,7 @@ One `generate_image` tool, available to every agent whatever its provider. It ro
125
125
 
126
126
  ### Voice
127
127
 
128
- Dictate into the composer (browser speech, or ElevenLabs / OpenAI), with push-to-talk. Have an agent's final message read aloud when its turn completes, with code stripped and long messages summarised first. [Voice and images](https://github.com/leonvanzyl/codeden/blob/main/docs/voice-and-images.md).
128
+ Dictate into the composer (browser speech, or ElevenLabs / OpenAI), with push-to-talk. Have an agent's final message read aloud when its turn completes, with code stripped, as a short TL;DR or the full message. [Voice and images](https://github.com/leonvanzyl/codeden/blob/main/docs/voice-and-images.md).
129
129
 
130
130
  ### Usage and cost reports
131
131
 
package/dist/cli.js CHANGED
@@ -242,7 +242,7 @@ async function run() {
242
242
  console.log(selfVersion);
243
243
  process.exit(0);
244
244
  }
245
- await import("./main-CtuRKdHE.js");
245
+ await import("./main-CKLHi4sz.js");
246
246
  }
247
247
  /** Settles once this copy's CLI has loaded (an older launcher that switched here awaits it). */
248
248
  const started = run();
@@ -1,5 +1,5 @@
1
1
  import { n as __require, r as __toESM, t as __commonJSMin } from "./rolldown-runtime-DegdyBIC.js";
2
- import { $ as BrowserNavigateRequest, $n as AgentConfigListQuery, $t as NOTICE_BATCH_CODE, A as SessionFileQuery, An as EditQueuedMessageRequest, Ar as prettifyError, At as RespondRequest, B as NetworkStatus, Bn as ProjectConfigFile, Bt as HealthResponse, C as isElectronRuntime, Cn as AutomationGuards, Cr as record, Ct as CreateProjectRequest, D as CODEDEN_VERSION, Dn as Session, Dr as ZodError, Dt as ListSessionsQuery, E as wrapper_default, En as TriagePolicy, Er as unknown, Et as ListInboxQuery, F as ListConflictsQuery, Fn as SpeechSummaryRequest, Ft as StartSessionRequest, G as SetNetworkPasswordResponse, Gn as sortProjects, Gt as SttQuery, H as RemoteAccessFile, Hn as StartedBy, Ht as Settings, I as ListNoticesQuery, In as TerminalClientMessage, It as SteerRequest, J as ImageTestRequest, Jn as InboxItem, Jt as UpdateInstallRequest, K as SignInRequest, Kn as DEFAULT_VISION_PATH, Kt as TtsRequest, L as ResolveConflictRequest, Ln as TerminalQuery, Lt as ThreadItemsQuery, M as RunnerToServerMessage, Mn as GitPushRequest, Mt as SendMessageRequest, N as ServerLock, Nn as RestoreFilesRequest, Nt as SessionStartMeta, O as UsageQuery, On as SessionStatus, Or as toJSONSchema, Ot as ReorderProjectsRequest, P as ArchiveSessionRequest, Pn as SessionResult, Pt as SetSecretRequest, Q as BrowserHistoryRequest, Qn as AgentConfigDiffRequest, Qt as ConflictRecord, R as RoleMessageRequest, Rn as RunnerCommandResults, Rt as UiClientMessage, S as acquireInstallLock, Sn as AutomationAction, Sr as preprocess, St as UpdateAutomationRequest, T as import_websocket_server, Tn as AutomationTrigger, Tr as union$1, Tt as FileWriteRequest, U as SetNetworkAccessRequest, Un as WorkKey, Ut as LOCAL_STT_SAMPLE_RATE, V as PASSWORD_RULE_TEXT, Vn as ConflictDecision, Vt as agentEventToNewEvent, W as SetNetworkPasswordRequest, Wn as Project, Wt as LocalSttModelParams, X as BrowserClientMessage, Xn as messageFrom, Xt as compareVersions, Y as summarizeActivity, Yn as InboxItemKind, Yt as UpdateStatus, Z as BrowserDevServerRequest, Zn as AgentConfigDeleteQuery, Zt as isNewerVersion, _ as importClaudeSdkFrom, _n as ReviewVerdict, _r as literal, _t as StarterSetRequest, a as Hono, an as GithubPr, ar as AutonomyLevel, at as AutomationImportSource, b as runNpm, bn as Artifact, br as object, bt as UpcomingRunsQuery, c as createAdaptorServer, cn as githubPrId, cr as datetime, ct as ImportAutomationsRequest, d as createImageEngineRegistry, dn as FactoryCandidate, dr as array, dt as AutomationProjectRequest, en as CreateGithubIssueRequest, er as AgentConfigLocator, et as BrowserNewTabRequest, f as IMAGE_ASPECT_RATIOS, fn as FactoryDecisionRequest, fr as boolean, ft as CreateAutomationRequest, g as CLAUDE_SDK_VERSION, gn as PutVisionRequest, gr as intersection, gt as SetAutomationEnabledRequest, h as claudeSdkSource, hn as KillSwitchRequest, hr as int, ht as RunAutomationRequest, i as killTree$1, in as GithubListQuery, ir as McpServerUpsertRequest, it as AUTOMATION_IMPORT_LIMITS, j as RunnerLock, jn as GitCommitRequest, jt as SecretKey, k as UsageSettings, kn as DiffScope, kr as safeParse$1, kt as ResolveInboxRequest, l as upgradeWebSocket, ln as parseGithubPrId, lr as _enum, lt as AutomationFileFrontmatter, m as ImageGenerationError, mn as FactoryTuning, mr as discriminatedUnion, mt as ListAutomationsQuery, n as readLock, nn as GithubIssue, nr as McpServerDiffRequest, nt as BrowserViewportRequest, o as tryDecodeURIComponent, on as MergePrRequest, or as ProviderId, ot as DuplicateAutomationRequest, p as resolveImageOutput, pn as FactoryTriageRequest, pr as custom, pt as ListAutomationRunsQuery, q as passwordProblem, qn as FactoryLimits, qt as AppPointer, r as isAlive, rn as GithubLabelRequest, rr as McpServerTarget, rt as BrowserWsQuery, s as bufferToFormData, sn as TriageIssueRequest, sr as ReasoningEffort, st as ExportAutomationsQuery, tn as GithubCommentRequest, tr as AgentConfigWriteRequest, tt as BrowserOpenRequest, u as ImageEngineRegistry, un as DiscoveryArea, ur as _null, ut as AutomationGuardsPatch, v as installClaudeSdk, vn as SetAutonomyModeRequest, vr as looseObject, vt as TriageDecisionRequest, w as ownNodeEnv, wn as AutomationRun, wr as string, wt as FileReadQuery, x as resolveNpm, xn as Automation, xr as optional, xt as UpdateAutomationProjectSettingsRequest, y as isClaudeSdkInstalled, yn as isFactoryMode, yr as number, yt as TriageReport, z as UpdateSessionRequest, zn as SessionOptions, zt as UpdateProjectRequest } from "./runner-BPkTmgxq.js";
2
+ import { $ as BrowserNavigateRequest, $n as InboxItemKind, $t as isNewerVersion, A as SessionFileQuery, An as TriagePolicy, Ar as unknown, At as RespondRequest, B as NetworkStatus, Bn as TerminalClientMessage, Bt as HealthResponse, C as isElectronRuntime, Cn as isFactoryMode, Cr as number, Ct as CreateProjectRequest, D as CODEDEN_VERSION, Dn as AutomationGuards, Dr as record, Dt as ListSessionsQuery, E as wrapper_default, En as AutomationAction, Er as preprocess, Et as ListInboxQuery, F as ListConflictsQuery, Fn as GitCommitRequest, Ft as StartSessionRequest, G as SetNetworkPasswordResponse, Gn as ConflictDecision, Gt as LOCAL_STT_SAMPLE_RATE, H as RemoteAccessFile, Hn as RunnerCommandResults, Ht as READ_ALOUD_FULL_MAX_CHARS, I as ListNoticesQuery, In as GitPushRequest, It as SteerRequest, J as ImageTestRequest, Jn as Project, Jt as TtsRequest, K as SignInRequest, Kn as StartedBy, Kt as LocalSttModelParams, L as ResolveConflictRequest, Ln as RestoreFilesRequest, Lt as ThreadItemsQuery, M as RunnerToServerMessage, Mn as SessionStatus, Mr as toJSONSchema, Mt as SendMessageRequest, N as ServerLock, Nn as DiffScope, Nr as safeParse$1, Nt as SessionStartMeta, O as UsageQuery, On as AutomationRun, Or as string, Ot as ReorderProjectsRequest, P as ArchiveSessionRequest, Pn as EditQueuedMessageRequest, Pr as prettifyError, Pt as SetSecretRequest, Q as BrowserHistoryRequest, Qn as InboxItem, Qt as compareVersions, R as RoleMessageRequest, Rn as SessionResult, Rt as UiClientMessage, S as acquireInstallLock, Sn as SetAutonomyModeRequest, Sr as looseObject, St as UpdateAutomationRequest, T as import_websocket_server, Tn as Automation, Tr as optional, Tt as FileWriteRequest, U as SetNetworkAccessRequest, Un as SessionOptions, Ut as Settings, V as PASSWORD_RULE_TEXT, Vn as TerminalQuery, Vt as agentEventToNewEvent, W as SetNetworkPasswordRequest, Wn as ProjectConfigFile, Wt as speechExcerpt, X as BrowserClientMessage, Xn as DEFAULT_VISION_PATH, Xt as UpdateInstallRequest, Y as summarizeActivity, Yn as sortProjects, Yt as AppPointer, Z as BrowserDevServerRequest, Zn as FactoryLimits, Zt as UpdateStatus, _ as importClaudeSdkFrom, _n as FactoryTuning, _r as custom, _t as StarterSetRequest, a as Hono, an as GithubLabelRequest, ar as AgentConfigWriteRequest, at as AutomationImportSource, b as runNpm, bn as PutVisionRequest, br as intersection, bt as UpcomingRunsQuery, c as createAdaptorServer, cn as MergePrRequest, cr as McpServerUpsertRequest, ct as ImportAutomationsRequest, d as createImageEngineRegistry, dn as parseGithubPrId, dr as ReasoningEffort, dt as AutomationProjectRequest, en as ConflictRecord, er as messageFrom, et as BrowserNewTabRequest, f as IMAGE_ASPECT_RATIOS, fn as DiscoveryArea, fr as datetime, ft as CreateAutomationRequest, g as CLAUDE_SDK_VERSION, gn as FactoryTriageRequest, gr as boolean, gt as SetAutomationEnabledRequest, h as claudeSdkSource, hn as FactoryDecisionRequest, hr as array, ht as RunAutomationRequest, i as killTree$1, in as GithubIssue, ir as AgentConfigLocator, it as AUTOMATION_IMPORT_LIMITS, j as RunnerLock, jn as Session, jr as ZodError, jt as SecretKey, k as UsageSettings, kn as AutomationTrigger, kr as union$1, kt as ResolveInboxRequest, l as upgradeWebSocket, ln as TriageIssueRequest, lr as AutonomyLevel, lt as AutomationFileFrontmatter, m as ImageGenerationError, mn as FactoryCandidate, mr as _null, mt as ListAutomationsQuery, n as readLock, nn as CreateGithubIssueRequest, nr as AgentConfigDiffRequest, nt as BrowserViewportRequest, o as tryDecodeURIComponent, on as GithubListQuery, or as McpServerDiffRequest, ot as DuplicateAutomationRequest, p as resolveImageOutput, pn as FactoryAllowLargeChangeRequest, pr as _enum, pt as ListAutomationRunsQuery, q as passwordProblem, qn as WorkKey, qt as SttQuery, r as isAlive, rn as GithubCommentRequest, rr as AgentConfigListQuery, rt as BrowserWsQuery, s as bufferToFormData, sn as GithubPr, sr as McpServerTarget, st as ExportAutomationsQuery, tn as NOTICE_BATCH_CODE, tr as AgentConfigDeleteQuery, tt as BrowserOpenRequest, u as ImageEngineRegistry, un as githubPrId, ur as ProviderId, ut as AutomationGuardsPatch, v as installClaudeSdk, vn as KillSwitchRequest, vr as discriminatedUnion, vt as TriageDecisionRequest, w as ownNodeEnv, wn as Artifact, wr as object, wt as FileReadQuery, x as resolveNpm, xn as ReviewVerdict, xr as literal, xt as UpdateAutomationProjectSettingsRequest, y as isClaudeSdkInstalled, yn as LEGACY_SIZE_LIMITS, yr as int, yt as TriageReport, z as UpdateSessionRequest, zn as SpeechSummaryRequest, zt as UpdateProjectRequest } from "./runner-DjavcMnG.js";
3
3
  import { createRequire } from "node:module";
4
4
  import fs, { accessSync, appendFileSync, closeSync, constants, copyFileSync, cpSync, createReadStream, existsSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, realpathSync, renameSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
5
5
  import { homedir, networkInterfaces, tmpdir } from "node:os";
@@ -31573,24 +31573,24 @@ function validateAutomation(a) {
31573
31573
  if (a.action.type === "triage" && a.trigger.type === "cron") throw badRequest("Triage runs on issue events, not on a schedule", "invalid_trigger");
31574
31574
  }
31575
31575
  // ../server/src/services/automations/import.ts
31576
- const tooLarge = (message) => new ServiceError(413, "import_too_large", message);
31576
+ const tooLarge$1 = (message) => new ServiceError(413, "import_too_large", message);
31577
31577
  /** The last path segment of a zip entry or uploaded file name. */
31578
31578
  const baseName = (name) => name.split(/[\\/]/).pop()?.trim() || "automation.md";
31579
31579
  /** Read the `.md` entries of a zip, refusing oversized or overfull archives before inflating. */
31580
31580
  function unzipMarkdown(zipBase64) {
31581
31581
  const data = Buffer.from(zipBase64, "base64");
31582
31582
  if (data.length === 0) throw badRequest("The zip is empty or not base64", "invalid_zip");
31583
- if (data.length > AUTOMATION_IMPORT_LIMITS.maxZipBytes) throw tooLarge(`The zip is larger than ${AUTOMATION_IMPORT_LIMITS.maxZipBytes / 1024} KB`);
31583
+ if (data.length > AUTOMATION_IMPORT_LIMITS.maxZipBytes) throw tooLarge$1(`The zip is larger than ${AUTOMATION_IMPORT_LIMITS.maxZipBytes / 1024} KB`);
31584
31584
  let entries = 0;
31585
31585
  let total = 0;
31586
31586
  const wanted = (file) => {
31587
31587
  entries++;
31588
- if (entries > AUTOMATION_IMPORT_LIMITS.maxZipEntries) throw tooLarge(`The zip lists more than ${AUTOMATION_IMPORT_LIMITS.maxZipEntries} entries`);
31588
+ if (entries > AUTOMATION_IMPORT_LIMITS.maxZipEntries) throw tooLarge$1(`The zip lists more than ${AUTOMATION_IMPORT_LIMITS.maxZipEntries} entries`);
31589
31589
  const name = file.name.replace(/\\/g, "/");
31590
31590
  if (name.endsWith("/") || name.startsWith("__MACOSX/") || !/\.md$/i.test(name)) return false;
31591
- if (file.originalSize > AUTOMATION_IMPORT_LIMITS.maxFileChars) throw tooLarge(`${baseName(name)} is larger than ${AUTOMATION_IMPORT_LIMITS.maxFileChars / 1024} KB`);
31591
+ if (file.originalSize > AUTOMATION_IMPORT_LIMITS.maxFileChars) throw tooLarge$1(`${baseName(name)} is larger than ${AUTOMATION_IMPORT_LIMITS.maxFileChars / 1024} KB`);
31592
31592
  total += file.originalSize;
31593
- if (total > AUTOMATION_IMPORT_LIMITS.maxTotalChars) throw tooLarge(`The zip holds more than ${AUTOMATION_IMPORT_LIMITS.maxTotalChars / 1024} KB of automations`);
31593
+ if (total > AUTOMATION_IMPORT_LIMITS.maxTotalChars) throw tooLarge$1(`The zip holds more than ${AUTOMATION_IMPORT_LIMITS.maxTotalChars / 1024} KB of automations`);
31594
31594
  return true;
31595
31595
  };
31596
31596
  let files;
@@ -31601,7 +31601,7 @@ function unzipMarkdown(zipBase64) {
31601
31601
  throw badRequest(`Not a readable zip: ${errorMessage$1(error)}`, "invalid_zip");
31602
31602
  }
31603
31603
  return Object.entries(files).map(([name, bytes]) => {
31604
- if (bytes.length > AUTOMATION_IMPORT_LIMITS.maxFileChars) throw tooLarge(`${baseName(name)} is too large`);
31604
+ if (bytes.length > AUTOMATION_IMPORT_LIMITS.maxFileChars) throw tooLarge$1(`${baseName(name)} is too large`);
31605
31605
  return {
31606
31606
  name: baseName(name),
31607
31607
  content: strFromU8(bytes)
@@ -31630,8 +31630,8 @@ function collectImportFiles(source) {
31630
31630
  ...source.text?.trim() ? [pastedFile(source.text)] : []
31631
31631
  ];
31632
31632
  if (files.length === 0) throw badRequest("No .md automation files found", "import_empty");
31633
- if (files.length > AUTOMATION_IMPORT_LIMITS.maxFiles) throw tooLarge(`At most ${AUTOMATION_IMPORT_LIMITS.maxFiles} automations can be imported at once`);
31634
- if (files.reduce((n, f) => n + f.content.length, 0) > AUTOMATION_IMPORT_LIMITS.maxTotalChars) throw tooLarge(`The import holds more than ${AUTOMATION_IMPORT_LIMITS.maxTotalChars / 1024} KB`);
31633
+ if (files.length > AUTOMATION_IMPORT_LIMITS.maxFiles) throw tooLarge$1(`At most ${AUTOMATION_IMPORT_LIMITS.maxFiles} automations can be imported at once`);
31634
+ if (files.reduce((n, f) => n + f.content.length, 0) > AUTOMATION_IMPORT_LIMITS.maxTotalChars) throw tooLarge$1(`The import holds more than ${AUTOMATION_IMPORT_LIMITS.maxTotalChars / 1024} KB`);
31635
31635
  const seen = /* @__PURE__ */ new Map();
31636
31636
  return files.map((f) => {
31637
31637
  const key = f.name.toLowerCase();
@@ -33984,6 +33984,18 @@ function fixPrompt(item, failed) {
33984
33984
  "Fix these in the same branch, run the tests, commit and push (call `create_pull_request` again to update the PR), then call `finish`."
33985
33985
  ].filter((l) => l !== "").join("\n");
33986
33986
  }
33987
+ /** Message to the PM when a pull request is over the size limit. */
33988
+ function tooLargePrompt(item, detail, failed) {
33989
+ const others = failed.filter((c) => c.name !== "size");
33990
+ const pr = item.pr ? `pull request #${item.pr.number}` : "its pull request";
33991
+ return [
33992
+ `Item ${item.id} ("${item.title}", ${pr}) is too large to merge automatically: ${detail}.`,
33993
+ others.length > 0 ? `It also failed: ${others.map((c) => `${c.name} (${c.detail ?? "failed"})`).join("; ")}.` : "Every other check that ran passed.",
33994
+ "Decide now; don't ask the user:",
33995
+ "- If the work can be split, open the parts as smaller issues with `create_issue` (each under the limit, with acceptance criteria). They become candidates; triage them as usual. Then close the oversized pull request on GitHub.",
33996
+ `- If it can't be split (an initial scaffold, a stack migration), call \`factory_allow_large_change\` with item ${item.id} and the reason. The checks run again${others.length > 0 ? ", the builder gets a fix round for the other failures," : ""} and it merges once the rest pass.`
33997
+ ].join("\n");
33998
+ }
33987
33999
  /** Ask a reviewer to look again after fixes. */
33988
34000
  function reReviewPrompt(item) {
33989
34001
  return `The author pushed fixes to pull request #${item.pr?.number ?? "?"}. Review the updated change again against the same criteria and call \`factory_submit_review\` once with a fresh verdict.`;
@@ -33998,12 +34010,13 @@ function revertPrompt(opts) {
33998
34010
  * The PM's structured triage instructions for new candidates. `unattended` (dark factory): the
33999
34011
  * PM makes every judgement call itself and records it, since nobody is watching to answer.
34000
34012
  */
34001
- function triageInstructions(items, unattended = false) {
34013
+ function triageInstructions(items, unattended = false, sizeLimit = "no limit") {
34002
34014
  return [
34003
34015
  `The factory has ${items.length} new candidate${items.length === 1 ? "" : "s"} to triage. Score each against the project vision (read it with read_project_notes or the vision file) and decide:`,
34004
34016
  ...items.map((i) => `- item ${i.id} [${i.source}${i.area ? `/${i.area}` : ""}] "${i.title}": ${i.summary.slice(0, 300)}${i.acceptanceCriteria.length ? ` | criteria: ${i.acceptanceCriteria.join("; ").slice(0, 400)}` : ""}`),
34005
34017
  "",
34006
34018
  "For each, call `factory_triage` with decision `accept` (a score from 0 to 1 and, if needed, sharper acceptance criteria) or `reject` (with the reason). Accepted items become GitHub issues and are built and merged automatically within the limits; reject anything outside the vision, too large, or touching protected areas.",
34019
+ sizeLimit === "no limit" ? "" : `Size: pull requests over ${sizeLimit} (lockfiles and generated or vendored files don't count) aren't merged automatically. Split bigger work into separate items that each fit (create the extra ones with \`create_issue\`). If the work can't be split (an initial scaffold, a stack migration), accept it with \`largeChange\` and the reason instead.`,
34007
34020
  "The vision wins over the current code. When the code doesn't match it (a different stack or architecture, say), the work that brings it in line comes first: score it highest, or open it with `create_issue` if no candidate covers it (new issues become candidates). Reject work that change would throw away, saying it waits for it.",
34008
34021
  unattended ? "This project runs as a dark factory: nobody is watching, so never ask the user. Make every judgement call yourself from the vision and record the ones that matter with `factory_record_decision` (the user reviews them in the inbox and the daily digest). Flag `needsHuman` only for a protected area, a missing or self-contradicting vision, or something irreversible outside the code, and triage everything else meanwhile." : ""
34009
34022
  ].filter((l) => l !== "").join("\n");
@@ -34235,16 +34248,19 @@ async function answerProviderQuestion(ctx, respond, e) {
34235
34248
  return false;
34236
34249
  }
34237
34250
  }
34238
- /** `factory_record_decision`: put a factory agent's own decision in front of the user. */
34239
- function recordDecision(ctx, session, req) {
34240
- if (!session.projectId) throw badRequest("Only a project's sessions record factory decisions", "no_project");
34251
+ /**
34252
+ * `factory_record_decision`: put a factory decision in front of the user (the inbox and the
34253
+ * daily digest). Also used for the PM's size allowances.
34254
+ */
34255
+ function recordDecision(ctx, by, req) {
34256
+ if (!by.projectId) throw badRequest("Only a project's sessions record factory decisions", "no_project");
34241
34257
  const id = `${DECISION_INBOX_PREFIX}${randomUUID()}`;
34242
34258
  raiseInbox(ctx, {
34243
34259
  id,
34244
34260
  kind: "message",
34245
34261
  priority: req.needsHuman ? "high" : "low",
34246
- projectId: session.projectId,
34247
- sessionId: session.id,
34262
+ projectId: by.projectId,
34263
+ sessionId: by.sessionId,
34248
34264
  title: `${req.needsHuman ? "Factory needs you" : "Factory decided"}: ${oneLine(req.question, 120)}`,
34249
34265
  body: [
34250
34266
  `**Question:** ${req.question}`,
@@ -34697,7 +34713,7 @@ const factoryTools = [
34697
34713
  }),
34698
34714
  defineTool({
34699
34715
  name: "factory_triage",
34700
- description: "Decide on a factory candidate after scoring it against the vision. `accept` with a score (0..1) and, if needed, sharper acceptance criteria: CodeDen creates the GitHub issue and builds it within the limits. `reject` with the reason (outside the vision, too big, touches protected areas, duplicate).",
34716
+ description: "Decide on a factory candidate after scoring it against the vision. `accept` with a score (0..1) and, if needed, sharper acceptance criteria: CodeDen creates the GitHub issue and builds it within the limits. `reject` with the reason (outside the vision, too big, touches protected areas, duplicate). Work over the size limit that can't be split (an initial scaffold, a stack migration): accept with `largeChange` and the reason.",
34701
34717
  roles: ["pm"],
34702
34718
  scope: "project",
34703
34719
  alwaysLoad: true,
@@ -34713,6 +34729,23 @@ const factoryTools = [
34713
34729
  };
34714
34730
  })
34715
34731
  }),
34732
+ defineTool({
34733
+ name: "factory_allow_large_change",
34734
+ description: "Let a factory item whose pull request failed the size check go ahead after all, because the work can't be split (an initial scaffold, a stack migration). Give the item id and the reason. The merge checks run again; CI, the review, tests and protected areas still apply. Prefer splitting the work into smaller issues when it can be split.",
34735
+ roles: ["pm"],
34736
+ scope: "project",
34737
+ alwaysLoad: true,
34738
+ isAvailable: inFactoryProject,
34739
+ input: FactoryAllowLargeChangeRequest,
34740
+ handler: async (input, { services, projectId }) => asTool(() => {
34741
+ const item = services.factory.allowLargeChange(projectId, input);
34742
+ return {
34743
+ itemId: item.id,
34744
+ stage: item.stage,
34745
+ pr: item.pr
34746
+ };
34747
+ })
34748
+ }),
34716
34749
  defineTool({
34717
34750
  name: "factory_record_decision",
34718
34751
  description: "Record a decision you made on your own instead of asking the user (this project runs as a dark factory, so nobody waits to answer). Give the question you would have asked, what you decided and why, grounded in the vision. It goes to the user's inbox and the daily digest so they can review or override it; nothing waits on it. Set `needsHuman` only for a protected area, a missing or self-contradicting vision, or something irreversible outside the code: that flags it for the user, and you carry on with the work that doesn't depend on it.",
@@ -46527,6 +46560,42 @@ function killTree(pid) {
46527
46560
  process.kill(pid, "SIGKILL");
46528
46561
  } catch {}
46529
46562
  }
46563
+ // ../server/src/services/factory/large-change.ts
46564
+ /**
46565
+ * Work that can't be split under the size limit (an initial scaffold, a stack migration). The
46566
+ * PM allows it at triage or, after the size check failed, with `factory_allow_large_change`.
46567
+ * Either way it is recorded as a factory decision, so the user sees it in the inbox and the
46568
+ * digest. Every other merge check still applies.
46569
+ */
46570
+ /** Record the PM's allowance for `item` as a factory decision. */
46571
+ function recordLargeChange(ctx, item, reason) {
46572
+ recordDecision(ctx, {
46573
+ projectId: item.projectId,
46574
+ sessionId: null
46575
+ }, {
46576
+ question: `"${item.title}" is bigger than the factory's size limit. Split it, or build and merge it as one change?`,
46577
+ decision: "Build and merge it as one change",
46578
+ reasoning: reason
46579
+ });
46580
+ }
46581
+ /** `factory_allow_large_change`: let an item that failed on size go ahead after all. */
46582
+ function allowLargeChange(ctx, projectId, req) {
46583
+ const item = ctx.store.item(req.itemId);
46584
+ if (!item || item.projectId !== projectId) throw notFound(`No factory item ${req.itemId}`, "item_not_found");
46585
+ if (item.stage !== "failed" && item.stage !== "verifying") throw conflict(`Item ${item.id} is ${item.stage}; only an item whose pull request is being verified, or failed its checks, can be allowed`, "not_verifiable");
46586
+ if (item.pr?.state !== "open") throw conflict(`Item ${item.id} has no open pull request`, "no_open_pr");
46587
+ const sizeFailed = item.checks.some((c) => c.name === "size" && c.state === "fail");
46588
+ if (item.stage === "failed" && !sizeFailed) throw conflict(`Item ${item.id} didn't fail on size: ${item.reason ?? ""}`, "not_size");
46589
+ recordLargeChange(ctx, item, req.reason);
46590
+ return ctx.store.updateItem(item.id, (i) => ({
46591
+ ...i,
46592
+ largeChange: req.reason,
46593
+ stage: "verifying",
46594
+ reason: null,
46595
+ finishedAt: null,
46596
+ note: "Allowed over the size limit by the PM; checking again"
46597
+ }));
46598
+ }
46530
46599
  // ../server/src/services/factory/globs.ts
46531
46600
  /**
46532
46601
  * Small glob matcher for protected paths (§10.1): `**` matches any number of path segments,
@@ -46675,7 +46744,7 @@ function slugify$1(title) {
46675
46744
  // ../server/src/services/factory/outcomes.ts
46676
46745
  const AUDIT_TIMEOUT_MS = 3e5;
46677
46746
  /** Mark an item failed and count it; pauses the loop after too many failures in a row. */
46678
- function failItem(ctx, loop, item, reason) {
46747
+ function failItem(ctx, loop, item, reason, opts = {}) {
46679
46748
  const at = ctx.now().toISOString();
46680
46749
  ctx.store.updateItem(item.id, (i) => ({
46681
46750
  ...i,
@@ -46684,7 +46753,7 @@ function failItem(ctx, loop, item, reason) {
46684
46753
  note: null,
46685
46754
  finishedAt: at
46686
46755
  }));
46687
- factoryNotice(ctx, {
46756
+ if (opts.notify !== false) factoryNotice(ctx, {
46688
46757
  id: `factory.failed:${item.id}`,
46689
46758
  projectId: item.projectId,
46690
46759
  target: "pm",
@@ -46694,6 +46763,7 @@ function failItem(ctx, loop, item, reason) {
46694
46763
  itemIds: [item.id],
46695
46764
  wake: false
46696
46765
  });
46766
+ if (opts.countsAsFailure === false) return loop;
46697
46767
  const next = {
46698
46768
  ...loop,
46699
46769
  consecutiveFailures: loop.consecutiveFailures + 1
@@ -46947,6 +47017,88 @@ async function regression(ctx, project, loop, item) {
46947
47017
  next = pauseLoop(ctx, next, `CI on ${branch} failed after merging #${prNumber}; ${revertNote}`, { inbox: false });
46948
47018
  return next;
46949
47019
  }
47020
+ // ../server/src/services/factory/size.ts
47021
+ /**
47022
+ * The size merge check (§10.1 step 4) counts code someone has to review. Lockfiles and
47023
+ * generated or vendored files are left out: a new dependency can add thousands of lockfile
47024
+ * lines, and a scaffold copies in UI components nobody reviews line by line.
47025
+ */
47026
+ const LOCKFILES = /* @__PURE__ */ new Set([
47027
+ "package-lock.json",
47028
+ "npm-shrinkwrap.json",
47029
+ "pnpm-lock.yaml",
47030
+ "yarn.lock",
47031
+ "bun.lock",
47032
+ "bun.lockb",
47033
+ "deno.lock",
47034
+ "cargo.lock",
47035
+ "poetry.lock",
47036
+ "uv.lock",
47037
+ "pipfile.lock",
47038
+ "pdm.lock",
47039
+ "gemfile.lock",
47040
+ "composer.lock",
47041
+ "go.sum",
47042
+ "flake.lock",
47043
+ "packages.lock.json",
47044
+ "pubspec.lock",
47045
+ "mix.lock",
47046
+ "podfile.lock",
47047
+ "package.resolved"
47048
+ ]);
47049
+ /** Folders whose contents are built, installed or vendored rather than written. */
47050
+ const GENERATED_DIRS = [
47051
+ "node_modules",
47052
+ "vendor",
47053
+ "dist",
47054
+ "build",
47055
+ ".next",
47056
+ ".nuxt",
47057
+ ".svelte-kit",
47058
+ "coverage",
47059
+ "__snapshots__",
47060
+ "__generated__"
47061
+ ];
47062
+ /** File names that mark generated output. */
47063
+ const GENERATED_NAME = /\.(min\.(js|css)|map|snap|lock)$|\.(gen|generated)\.[a-z]+$/i;
47064
+ /** True for files the size check doesn't count (lockfiles, generated or vendored files). */
47065
+ function isUncountedFile(path) {
47066
+ const parts = path.replace(/\\/g, "/").split("/");
47067
+ const name = (parts.at(-1) ?? "").toLowerCase();
47068
+ if (LOCKFILES.has(name) || GENERATED_NAME.test(name)) return true;
47069
+ const dirs = parts.slice(0, -1);
47070
+ if (dirs.some((d) => GENERATED_DIRS.includes(d))) return true;
47071
+ return dirs.some((d, i) => d === "components" && dirs[i + 1] === "ui");
47072
+ }
47073
+ /** Lines and files the size check counts for a PR. */
47074
+ function changeSize(pr) {
47075
+ if (!pr.fileLines) return {
47076
+ lines: pr.additions + pr.deletions,
47077
+ files: pr.files.length,
47078
+ uncounted: 0
47079
+ };
47080
+ let lines = 0;
47081
+ let files = 0;
47082
+ for (const f of pr.fileLines) {
47083
+ if (isUncountedFile(f.path)) continue;
47084
+ lines += f.lines;
47085
+ files += 1;
47086
+ }
47087
+ return {
47088
+ lines,
47089
+ files,
47090
+ uncounted: pr.fileLines.length - files
47091
+ };
47092
+ }
47093
+ /** The size limits as a phrase ("5,000 lines", "no limit"). */
47094
+ function sizeLimitText(tuning) {
47095
+ const parts = [tuning.maxChangedLines !== null ? `${tuning.maxChangedLines.toLocaleString("en")} lines` : "", tuning.maxChangedFiles !== null ? `${tuning.maxChangedFiles.toLocaleString("en")} files` : ""].filter(Boolean);
47096
+ return parts.length > 0 ? parts.join(", ") : "no limit";
47097
+ }
47098
+ /** True when the change is over a limit that is set. */
47099
+ function overSizeLimit(size, tuning) {
47100
+ return tuning.maxChangedLines !== null && size.lines > tuning.maxChangedLines || tuning.maxChangedFiles !== null && size.files > tuning.maxChangedFiles;
47101
+ }
46950
47102
  // ../server/src/services/factory/store.ts
46951
47103
  /** Stages of items that are still moving through the loop. */
46952
47104
  const ACTIVE_STAGES = [
@@ -46976,7 +47128,8 @@ var FactoryStore = class {
46976
47128
  this.events = events;
46977
47129
  }
46978
47130
  loop(projectId) {
46979
- return this.db.select().from(factoryLoops).where(eq(factoryLoops.projectId, projectId)).get()?.loop;
47131
+ const loop = this.db.select().from(factoryLoops).where(eq(factoryLoops.projectId, projectId)).get()?.loop;
47132
+ return loop ? upgradeSizeLimits(loop) : void 0;
46980
47133
  }
46981
47134
  /** The project's loop, or a fresh idle one (not saved). */
46982
47135
  loopOrNew(projectId, now = /* @__PURE__ */ new Date()) {
@@ -47044,6 +47197,7 @@ var FactoryStore = class {
47044
47197
  revertOf: null,
47045
47198
  triageRequestedAt: null,
47046
47199
  nudges: {},
47200
+ largeChange: null,
47047
47201
  mergedAt: null,
47048
47202
  finishedAt: null,
47049
47203
  ...draft,
@@ -47086,6 +47240,24 @@ var FactoryStore = class {
47086
47240
  }]);
47087
47241
  }
47088
47242
  };
47243
+ /**
47244
+ * Loops saved before 0.4.4 carry the old default size limits (800 lines, 40 files, counting
47245
+ * lockfiles and generated files), which blocked ordinary scaffolds. Untouched old defaults
47246
+ * become the current defaults; limits someone chose are kept.
47247
+ */
47248
+ function upgradeSizeLimits(loop) {
47249
+ const t = loop.tuning;
47250
+ if (t.maxChangedLines !== LEGACY_SIZE_LIMITS.maxChangedLines || t.maxChangedFiles !== LEGACY_SIZE_LIMITS.maxChangedFiles) return loop;
47251
+ const defaults = FactoryTuning.parse({});
47252
+ return {
47253
+ ...loop,
47254
+ tuning: {
47255
+ ...t,
47256
+ maxChangedLines: defaults.maxChangedLines,
47257
+ maxChangedFiles: defaults.maxChangedFiles
47258
+ }
47259
+ };
47260
+ }
47089
47261
  // ../server/src/services/factory/checks.ts
47090
47262
  const ORDER$1 = [
47091
47263
  "protected_paths",
@@ -47125,9 +47297,11 @@ function evaluateChecks(i) {
47125
47297
  });
47126
47298
  if (hits.paths.length > 0 || hits.topics.length > 0) set("protected_paths", "fail", `Touches protected areas (${[hits.paths.length ? `paths: ${list(hits.paths.map((h) => h.path))}` : "", hits.topics.length ? `topics: ${hits.topics.join(", ")}` : ""].filter(Boolean).join("; ")})`, true);
47127
47299
  else set("protected_paths", "pass", null);
47128
- const lines = i.pr.additions + i.pr.deletions;
47129
- if (lines > i.tuning.maxChangedLines || files.length > i.tuning.maxChangedFiles) set("size", "fail", `${lines} lines in ${files.length} files (limit ${i.tuning.maxChangedLines} lines, ${i.tuning.maxChangedFiles} files)`);
47130
- else set("size", "pass", `${lines} lines in ${files.length} files`);
47300
+ const size = changeSize(i.pr);
47301
+ const measured = `${size.lines.toLocaleString("en")} lines in ${size.files} files${size.uncounted ? ` (${size.uncounted} lockfiles or generated files not counted)` : ""}`;
47302
+ if (!overSizeLimit(size, i.tuning)) set("size", "pass", measured);
47303
+ else if (i.largeChange) set("size", "pass", `${measured}; allowed by the PM: ${i.largeChange}`);
47304
+ else set("size", "fail", `${measured}, over the limit of ${sizeLimitText(i.tuning)}`);
47131
47305
  switch (i.pr.checks) {
47132
47306
  case "success":
47133
47307
  set("ci", "pass", null);
@@ -47238,6 +47412,7 @@ async function advanceVerifying(ctx, project, loop, item) {
47238
47412
  ciGraceOver: now.getTime() - since > ctx.ciGraceMs,
47239
47413
  limits: project.factoryLimits,
47240
47414
  tuning: loop.tuning,
47415
+ largeChange: item.largeChange ?? null,
47241
47416
  areas: vision.parsed.protectedAreas,
47242
47417
  now: now.toISOString()
47243
47418
  });
@@ -47414,6 +47589,7 @@ function failReview(ctx, project, loop, item, reason) {
47414
47589
  /** Failed checks: one more fix round if allowed, otherwise the item fails (and may pause). */
47415
47590
  async function handleFailure(ctx, project, loop, item) {
47416
47591
  const failed = failedChecks$1(item.checks);
47592
+ if (failed.some((c) => c.name === "size")) return tooLarge(ctx, project, loop, item);
47417
47593
  const builder = item.builderSessionId ? ctx.repo.find(item.builderSessionId)?.session : void 0;
47418
47594
  if (item.attempts < loop.tuning.maxFixAttempts && builder !== void 0 && builder.status !== "failed" && builder.status !== "stopped" && item.review?.verdict !== "reject" && builder) {
47419
47595
  const at = ctx.now().toISOString();
@@ -47436,6 +47612,30 @@ async function handleFailure(ctx, project, loop, item) {
47436
47612
  if (item.pr) await ctx.github.comment(project.id, item.pr.number, `The CodeDen factory's merge checks failed, so this PR is left for a human:\n\n${failed.map((c) => `- **${c.name}**: ${c.detail ?? "failed"}`).join("\n")}`).catch((error) => ctx.log?.(`factory: comment: ${errorMessage$1(error)}`));
47437
47613
  return next;
47438
47614
  }
47615
+ /**
47616
+ * The size check failed. That isn't a quality failure, so it doesn't count towards pausing
47617
+ * the loop, and no fix round runs. The PM is woken to split the work into smaller issues or,
47618
+ * when it can't be split, to allow it with `factory_allow_large_change`. The PR stays open.
47619
+ */
47620
+ async function tooLarge(ctx, project, loop, item) {
47621
+ const failed = failedChecks$1(item.checks);
47622
+ const size = failed.find((c) => c.name === "size");
47623
+ const next = failItem(ctx, loop, item, failed.map((c) => `${c.name}: ${c.detail ?? "failed"}`).join("; "), {
47624
+ countsAsFailure: false,
47625
+ notify: false
47626
+ });
47627
+ factoryNotice(ctx, {
47628
+ id: `factory.too-large:${item.id}:${item.pr?.number ?? ""}`,
47629
+ projectId: item.projectId,
47630
+ target: "pm",
47631
+ event: "failed",
47632
+ title: `Too large to merge automatically: ${item.title}`,
47633
+ text: tooLargePrompt(item, size?.detail ?? "over the size limit", failed),
47634
+ itemIds: [item.id]
47635
+ });
47636
+ if (item.pr) await ctx.github.comment(project.id, item.pr.number, `This PR is larger than the CodeDen factory's size limit (${size?.detail ?? "over the limit"}), so it isn't merged automatically yet. The project manager will split the work into smaller issues or allow it as one change.`).catch((error) => ctx.log?.(`factory: comment: ${errorMessage$1(error)}`));
47637
+ return next;
47638
+ }
47439
47639
  // ../server/src/services/factory/loop.ts
47440
47640
  /** Label on issues the factory created or accepted. */
47441
47641
  const FACTORY_LABEL = "codeden-factory";
@@ -47646,7 +47846,7 @@ function requestTriage(ctx, project) {
47646
47846
  target: "pm",
47647
47847
  event: "triage_needed",
47648
47848
  title: `Factory: ${fresh.length} candidate${fresh.length === 1 ? "" : "s"} to triage`,
47649
- text: triageInstructions(fresh, project.autonomyMode === "dark_factory"),
47849
+ text: triageInstructions(fresh, project.autonomyMode === "dark_factory", sizeLimitText(ctx.store.loopOrNew(project.id).tuning)),
47650
47850
  itemIds: fresh.map((i) => i.id)
47651
47851
  });
47652
47852
  for (const item of fresh) ctx.store.updateItem(item.id, (i) => ({
@@ -47780,6 +47980,7 @@ async function triage(ctx, projectId, req) {
47780
47980
  labels: [FACTORY_LABEL]
47781
47981
  });
47782
47982
  const accepted = issue;
47983
+ if (req.largeChange) recordLargeChange(ctx, item, req.largeChange);
47783
47984
  return ctx.store.updateItem(item.id, (i) => ({
47784
47985
  ...i,
47785
47986
  stage: "backlog",
@@ -47787,6 +47988,7 @@ async function triage(ctx, projectId, req) {
47787
47988
  acceptanceCriteria: criteria,
47788
47989
  issue: accepted,
47789
47990
  workKey: `issue:${accepted.number}`,
47991
+ largeChange: req.largeChange ?? i.largeChange ?? null,
47790
47992
  note: null
47791
47993
  }));
47792
47994
  }
@@ -48213,9 +48415,18 @@ var FactoryService = class {
48213
48415
  if (session.projectId) this.soon(session.projectId);
48214
48416
  return res;
48215
48417
  }
48418
+ /** `factory_allow_large_change`: the PM lets an item over the size limit merge. */
48419
+ allowLargeChange(projectId, req) {
48420
+ const item = allowLargeChange(this.ctx, projectId, req);
48421
+ this.soon(projectId);
48422
+ return item;
48423
+ }
48216
48424
  /** `factory_record_decision`: a decision an unattended agent made instead of asking. */
48217
48425
  recordDecision(session, req) {
48218
- return recordDecision(this.ctx, session, req);
48426
+ return recordDecision(this.ctx, {
48427
+ projectId: session.projectId,
48428
+ sessionId: session.id
48429
+ }, req);
48219
48430
  }
48220
48431
  /** True when nobody answers this session's questions (dark factory PM or factory agent). */
48221
48432
  isUnattended(sessionId) {
@@ -48371,7 +48582,11 @@ var GithubFactoryPort = class {
48371
48582
  ].includes(c.conclusion)).map((c) => c.name),
48372
48583
  additions: pr.additions,
48373
48584
  deletions: pr.deletions,
48374
- files: pr.files.map((f) => f.path)
48585
+ files: pr.files.map((f) => f.path),
48586
+ fileLines: pr.files.map((f) => ({
48587
+ path: f.path,
48588
+ lines: f.additions + f.deletions
48589
+ }))
48375
48590
  };
48376
48591
  }
48377
48592
  async hasScreenshotEvidence(projectId, prNumber, issueNumber) {
@@ -59283,13 +59498,13 @@ function round(n) {
59283
59498
  return Math.round(n * 1e6) / 1e6;
59284
59499
  }
59285
59500
  // ../server/src/services/utility.ts
59501
+ /** The summary-mode fallback: the first sentence or two, within the TL;DR cap. */
59502
+ function shortSpeech(plain) {
59503
+ return speechExcerpt(plain, 220, 2);
59504
+ }
59286
59505
  /** Strip code blocks and markdown so text reads naturally aloud (§11.3). */
59287
- function plainSpeech(text, maxChars = 600) {
59288
- const plain = text.replace(/```[\s\S]*?```/g, " ").replace(/`([^`]*)`/g, "$1").replace(/!\[[^\]]*\]\([^)]*\)/g, " ").replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/^#{1,6}\s+/gm, "").replace(/^\s*[-*+]\s+/gm, "").replace(/[*_~>|]/g, "").replace(/\s+/g, " ").trim();
59289
- if (plain.length <= maxChars) return plain;
59290
- const cut = plain.slice(0, maxChars);
59291
- const end = cut.lastIndexOf(". ");
59292
- return end > maxChars / 2 ? cut.slice(0, end + 1) : `${cut.trimEnd()}...`;
59506
+ function plainSpeech(text) {
59507
+ return text.replace(/```[\s\S]*?```/g, " ").replace(/`([^`]*)`/g, "$1").replace(/!\[[^\]]*\]\([^)]*\)/g, " ").replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/^#{1,6}\s+/gm, "").replace(/^\s*[-*+]\s+/gm, "").replace(/[*_~>|]/g, "").replace(/\s+/g, " ").trim();
59293
59508
  }
59294
59509
  function runnerOneShot(runner) {
59295
59510
  return {
@@ -59303,6 +59518,18 @@ function runnerOneShot(runner) {
59303
59518
  }
59304
59519
  const PREFERENCE = ["claude", "codex"];
59305
59520
  const TIMEOUT_MS = 6e4;
59521
+ /** Read-aloud waits on the summary, so it gets less time than the other jobs. */
59522
+ const SPEECH_TIMEOUT_MS = 3e4;
59523
+ const TLDR_SYSTEM = [
59524
+ "You write the spoken TL;DR of a coding agent's final message. It is read aloud by a text-to-speech voice, so the listener cannot skim.",
59525
+ "Reply with at most two short sentences, under 200 characters in total.",
59526
+ "Say what was done and whether anything needs the user (a decision, a review, a failure). If nothing needs them, don't mention it.",
59527
+ "Plain spoken English only: no markdown, code, file paths, lists, numbers of lines or preamble such as 'Summary:'."
59528
+ ].join(" ");
59529
+ /** Server-side warnings for utility jobs, shown whatever the debug setting (stderr). */
59530
+ function defaultWarn(message) {
59531
+ console.warn(`[codeden] ${(/* @__PURE__ */ new Date()).toISOString()} ${message}`);
59532
+ }
59306
59533
  /**
59307
59534
  * The real `UtilityService`: a one-shot, tool-less call on a small model (§7.4). The provider
59308
59535
  * comes from settings (`auto` = Claude, then Codex); when the chosen provider fails, the
@@ -59313,51 +59540,70 @@ var ProviderUtilityService = class {
59313
59540
  constructor(deps) {
59314
59541
  this.deps = deps;
59315
59542
  }
59543
+ /** Set while no provider can run one-shots, so that is reported once, not on every job. */
59544
+ reportedNoProvider = false;
59545
+ warn(message) {
59546
+ (this.deps.warn ?? defaultWarn)(message);
59547
+ }
59316
59548
  async sessionTitle(ctx) {
59317
59549
  if (!this.deps.settings().utilityModel.jobs.sessionTitles) return null;
59318
59550
  const source = ctx.firstMessage ?? ctx.finalMessage;
59319
59551
  if (!source) return null;
59320
- const title = (await this.run("Write a title of 3 to 6 words for a coding session that starts with the request below. Reply with the title only: no quotes, no trailing punctuation.", `Request:\n${clip(source, 4e3)}`))?.split("\n")[0]?.replace(/^["'#\s]+|["'.\s]+$/g, "").slice(0, 80);
59552
+ const title = (await this.run("session title", "Write a title of 3 to 6 words for a coding session that starts with the request below. Reply with the title only: no quotes, no trailing punctuation.", `Request:\n${clip(source, 4e3)}`))?.split("\n")[0]?.replace(/^["'#\s]+|["'.\s]+$/g, "").slice(0, 80);
59321
59553
  return title ? title : null;
59322
59554
  }
59323
59555
  async suggestFollowups(ctx) {
59324
59556
  if (!this.deps.settings().utilityModel.jobs.followups || !ctx.finalMessage) return null;
59325
59557
  const files = ctx.changedFiles?.length ? `\nFiles changed: ${ctx.changedFiles.join(", ")}` : "";
59326
- const lines = (await this.run("You suggest what a developer might ask a coding agent next. Reply with 2 to 4 short prompts, one per line, written as the developer would type them. No numbering, no quotes, nothing else.", `The agent's final message:\n${clip(ctx.finalMessage, 6e3)}${files}`) ?? "").split("\n").map((l) => l.replace(/^\s*(?:[-*\d.)]+\s*)?/, "").replace(/^["']|["']$/g, "").trim()).filter((l) => l.length > 0 && l.length <= 300).slice(0, 4);
59558
+ const lines = (await this.run("follow-ups", "You suggest what a developer might ask a coding agent next. Reply with 2 to 4 short prompts, one per line, written as the developer would type them. No numbering, no quotes, nothing else.", `The agent's final message:\n${clip(ctx.finalMessage, 6e3)}${files}`) ?? "").split("\n").map((l) => l.replace(/^\s*(?:[-*\d.)]+\s*)?/, "").replace(/^["']|["']$/g, "").trim()).filter((l) => l.length > 0 && l.length <= 300).slice(0, 4);
59327
59559
  return lines.length >= 2 ? lines.map((prompt) => ({ prompt })) : null;
59328
59560
  }
59329
59561
  async speechSummary(text) {
59330
- const plain = plainSpeech(text, 4e3);
59331
- if (!this.deps.settings().utilityModel.jobs.readAloudSummary || plain.length <= 240) return {
59332
- text: plainSpeech(text),
59562
+ const plain = plainSpeech(text);
59563
+ if (this.deps.settings().voice.readAloud.content === "full") return {
59564
+ text: speechExcerpt(plain, READ_ALOUD_FULL_MAX_CHARS),
59333
59565
  source: "plain"
59334
59566
  };
59335
- const out = await this.run("Compress the message below into one or two plain sentences that will be read aloud. No markdown, no code, no lists.", plain);
59336
- return out ? {
59337
- text: out.trim(),
59567
+ if (plain.length <= 220) return {
59568
+ text: plain,
59569
+ source: "plain"
59570
+ };
59571
+ const out = await this.run("read-aloud TL;DR", TLDR_SYSTEM, clip(plain, 12e3), { timeoutMs: SPEECH_TIMEOUT_MS });
59572
+ const tldr = out ? plainSpeech(out) : "";
59573
+ if (tldr) return {
59574
+ text: shortSpeech(tldr),
59338
59575
  source: "utility"
59339
- } : {
59340
- text: plainSpeech(text),
59576
+ };
59577
+ this.warn("utility: read-aloud TL;DR unavailable, speaking the message's first sentences");
59578
+ return {
59579
+ text: shortSpeech(plain),
59341
59580
  source: "plain"
59342
59581
  };
59343
59582
  }
59344
59583
  /** One-shot on the configured provider, falling back to the next; null on failure. */
59345
- async run(system, prompt) {
59584
+ async run(job, system, prompt, opts = {}) {
59346
59585
  const { utilityModel } = this.deps.settings();
59347
59586
  const available = this.deps.oneShot.available();
59348
59587
  const chosen = utilityModel.provider === "auto" ? null : utilityModel.provider;
59349
59588
  const order = [chosen, ...PREFERENCE].filter((id, i, all) => !!id && available.includes(id) && all.indexOf(id) === i);
59589
+ if (order.length === 0) {
59590
+ if (!this.reportedNoProvider) this.warn(`utility: no provider can run the utility model (runner lists: ${available.join(", ") || "none"})`);
59591
+ this.reportedNoProvider = true;
59592
+ return null;
59593
+ }
59594
+ this.reportedNoProvider = false;
59350
59595
  for (const providerId of order) try {
59351
59596
  const model = providerId === chosen ? utilityModel.model ?? void 0 : void 0;
59352
59597
  const text = await this.deps.oneShot.oneShot(providerId, {
59353
59598
  prompt,
59354
59599
  system,
59355
59600
  model,
59356
- timeoutMs: TIMEOUT_MS
59601
+ timeoutMs: opts.timeoutMs ?? TIMEOUT_MS
59357
59602
  });
59358
59603
  if (text.trim()) return text.trim();
59604
+ this.warn(`utility: ${job} on ${providerId} returned nothing`);
59359
59605
  } catch (error) {
59360
- this.deps.log?.(`utility: ${providerId} failed: ${error instanceof Error ? error.message : String(error)}`);
59606
+ this.warn(`utility: ${job} on ${providerId} failed: ${error instanceof Error ? error.message : String(error)}`);
59361
59607
  }
59362
59608
  return null;
59363
59609
  }
@@ -59897,8 +60143,7 @@ function createServices(opts) {
59897
60143
  const userHome = opts.userHome ?? homedir();
59898
60144
  const utility = opts.utility ?? new ProviderUtilityService({
59899
60145
  settings: () => opts.settings.get(),
59900
- oneShot: runnerOneShot(opts.runner),
59901
- log: opts.log
60146
+ oneShot: runnerOneShot(opts.runner)
59902
60147
  });
59903
60148
  const projects = new ProjectService({
59904
60149
  db: opts.db,
@@ -61367,4 +61612,4 @@ main().then((code) => process.exit(code), (error) => {
61367
61612
  });
61368
61613
  export {};
61369
61614
 
61370
- //# sourceMappingURL=main-CtuRKdHE.js.map
61615
+ //# sourceMappingURL=main-CKLHi4sz.js.map