codeden 0.4.0 → 0.4.2
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/dist/cli.js +1 -1
- package/dist/{main-B7VuaduN.js → main-B7rU7myk.js} +2 -2
- package/dist/{main-B7VuaduN.js.map → main-B7rU7myk.js.map} +1 -1
- package/dist/{main-sEg3GS7X.js → main-sdPbRDgf.js} +602 -461
- package/dist/main-sdPbRDgf.js.map +1 -0
- package/dist/{runner-QPAnitMx.js → runner-B20DGEAR.js} +27 -3
- package/dist/runner-B20DGEAR.js.map +1 -0
- package/dist/runner.js +1 -1
- package/dist/web/assets/{core-1ZGcgEnc.js → core-CZsR2WCQ.js} +1 -1
- package/dist/web/assets/engine-oniguruma-B6ZU4lV4.js +1 -0
- package/dist/web/assets/{highlighted-body-JLMTFACH-Gb9WrOV2.js → highlighted-body-JLMTFACH-Tgy2loNJ.js} +1 -1
- package/dist/web/assets/{highlights-BbG82Nvt.js → highlights-C-rSaP9T.js} +1 -1
- package/dist/web/assets/{index-Capj1YIA.js → index-5SrUkic_.js} +176 -176
- package/dist/web/assets/index-DcErU_f1.css +1 -0
- package/dist/web/assets/langs-DPvNNit8.js +1 -0
- package/dist/web/assets/mermaid-WFAXDIZX-df73_wYp.js +1 -0
- package/dist/web/assets/{shiki-DvdkI_wn.js → shiki-CBcuG-Nl.js} +2 -2
- package/dist/web/assets/{stream-tokenizer-hzLEdzf1.js → stream-tokenizer-CzcO6yIN.js} +1 -1
- package/dist/web/assets/{themes-o5fWh38V.js → themes-BL20ecgi.js} +2 -2
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/main-sEg3GS7X.js.map +0 -1
- package/dist/runner-QPAnitMx.js.map +0 -1
- package/dist/web/assets/engine-oniguruma-S10PniCp.js +0 -1
- package/dist/web/assets/index-CW3M7yYL.css +0 -1
- package/dist/web/assets/langs-92p-7kXy.js +0 -1
- package/dist/web/assets/mermaid-WFAXDIZX-D5w0oM_l.js +0 -1
|
@@ -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
|
|
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-B20DGEAR.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";
|
|
@@ -33706,6 +33706,565 @@ var McpTokenStore = class {
|
|
|
33706
33706
|
this.db.update(mcpTokens).set({ revokedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and(eq(mcpTokens.sessionId, sessionId), isNull(mcpTokens.revokedAt))).run();
|
|
33707
33707
|
}
|
|
33708
33708
|
};
|
|
33709
|
+
// ../server/src/services/factory/vision.ts
|
|
33710
|
+
/**
|
|
33711
|
+
* The project vision (§10.1): `.codeden/VISION.md`, required for autopilot and dark factory.
|
|
33712
|
+
* Sections are `##` headings; bullets under them are the entries. Protected areas become path
|
|
33713
|
+
* globs (anything in backticks, or that looks like a path) and topics (plain words).
|
|
33714
|
+
*/
|
|
33715
|
+
const VISION_TEMPLATE = `# Vision
|
|
33716
|
+
|
|
33717
|
+
Describe what this project is for. The factory scores every piece of work against this file.
|
|
33718
|
+
|
|
33719
|
+
## Goals
|
|
33720
|
+
- What should this project achieve in the next months?
|
|
33721
|
+
|
|
33722
|
+
## Target users
|
|
33723
|
+
- Who uses it, and what do they need?
|
|
33724
|
+
|
|
33725
|
+
## Principles
|
|
33726
|
+
- Small, well-tested changes
|
|
33727
|
+
- Keep the public API stable
|
|
33728
|
+
|
|
33729
|
+
## Out of scope
|
|
33730
|
+
- Things the factory must never propose
|
|
33731
|
+
|
|
33732
|
+
## Quality bar
|
|
33733
|
+
- Every behaviour change comes with tests
|
|
33734
|
+
- CI is green before merge
|
|
33735
|
+
|
|
33736
|
+
## Protected areas
|
|
33737
|
+
Paths (globs in backticks) and topics that always need a human. The factory never merges
|
|
33738
|
+
changes that touch them; they go to your inbox.
|
|
33739
|
+
- \`**/auth/**\`
|
|
33740
|
+
- \`**/migrations/**\`
|
|
33741
|
+
- \`.github/workflows/**\`
|
|
33742
|
+
- \`LICENSE\`
|
|
33743
|
+
- payments
|
|
33744
|
+
- licensing
|
|
33745
|
+
`;
|
|
33746
|
+
/** Paths that deploy or release, protected unless `allowDeploys` is on. */
|
|
33747
|
+
const DEPLOY_GLOBS = [
|
|
33748
|
+
".github/workflows/*deploy*",
|
|
33749
|
+
".github/workflows/*release*",
|
|
33750
|
+
".github/workflows/*publish*",
|
|
33751
|
+
"**/Dockerfile",
|
|
33752
|
+
"**/fly.toml",
|
|
33753
|
+
"**/vercel.json",
|
|
33754
|
+
"**/netlify.toml"
|
|
33755
|
+
];
|
|
33756
|
+
/** True when `topic` appears as a word or phrase in `text`. */
|
|
33757
|
+
function mentionsTopic(text, topic) {
|
|
33758
|
+
const escaped = topic.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\s+/g, "\\s+");
|
|
33759
|
+
return new RegExp(`\\b${escaped}\\b`, "i").test(text);
|
|
33760
|
+
}
|
|
33761
|
+
function visionFile(project) {
|
|
33762
|
+
const file = resolve(project.path, project.visionPath);
|
|
33763
|
+
const rel = relative(project.path, file);
|
|
33764
|
+
if (rel.startsWith("..") || isAbsolute(rel)) throw badRequest("The vision path must stay inside the project", "invalid_vision_path");
|
|
33765
|
+
return file;
|
|
33766
|
+
}
|
|
33767
|
+
/** Read the vision; the template (and `exists: false`) when the file is missing. */
|
|
33768
|
+
function readVision(project) {
|
|
33769
|
+
const file = visionFile(project);
|
|
33770
|
+
const exists = existsSync(file);
|
|
33771
|
+
const contents = exists ? readFileSync(file, "utf8") : VISION_TEMPLATE;
|
|
33772
|
+
return {
|
|
33773
|
+
path: project.visionPath,
|
|
33774
|
+
exists,
|
|
33775
|
+
contents,
|
|
33776
|
+
parsed: parseVision(contents)
|
|
33777
|
+
};
|
|
33778
|
+
}
|
|
33779
|
+
/** Write the vision atomically (in the main checkout; commit it to share it with agents). */
|
|
33780
|
+
function writeVision(project, contents) {
|
|
33781
|
+
const file = visionFile(project);
|
|
33782
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
33783
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
33784
|
+
writeFileSync(tmp, contents.endsWith("\n") ? contents : `${contents}\n`);
|
|
33785
|
+
renameSync(tmp, file);
|
|
33786
|
+
return readVision(project);
|
|
33787
|
+
}
|
|
33788
|
+
// ../server/src/services/factory/context.ts
|
|
33789
|
+
/** The vision text and its parsed form ("" and empty when the file is missing). */
|
|
33790
|
+
function visionOf(project) {
|
|
33791
|
+
try {
|
|
33792
|
+
const v = readVision(project);
|
|
33793
|
+
return {
|
|
33794
|
+
text: v.exists ? v.contents : "",
|
|
33795
|
+
parsed: v.parsed,
|
|
33796
|
+
exists: v.exists
|
|
33797
|
+
};
|
|
33798
|
+
} catch {
|
|
33799
|
+
return {
|
|
33800
|
+
text: "",
|
|
33801
|
+
exists: false,
|
|
33802
|
+
parsed: {
|
|
33803
|
+
goals: [],
|
|
33804
|
+
users: [],
|
|
33805
|
+
principles: [],
|
|
33806
|
+
outOfScope: [],
|
|
33807
|
+
qualityBar: [],
|
|
33808
|
+
protectedAreas: {
|
|
33809
|
+
globs: [],
|
|
33810
|
+
topics: []
|
|
33811
|
+
}
|
|
33812
|
+
}
|
|
33813
|
+
};
|
|
33814
|
+
}
|
|
33815
|
+
}
|
|
33816
|
+
/** Raise (or refresh) a factory inbox item. */
|
|
33817
|
+
function raiseInbox(ctx, item) {
|
|
33818
|
+
const existing = ctx.inbox.find(item.id);
|
|
33819
|
+
if (existing?.status === "open" && existing.title === item.title && existing.body === item.body) return;
|
|
33820
|
+
const now = ctx.now().toISOString();
|
|
33821
|
+
ctx.inbox.put({
|
|
33822
|
+
id: item.id,
|
|
33823
|
+
kind: item.kind,
|
|
33824
|
+
status: "open",
|
|
33825
|
+
priority: item.priority ?? "normal",
|
|
33826
|
+
title: item.title,
|
|
33827
|
+
body: item.body,
|
|
33828
|
+
projectId: item.projectId,
|
|
33829
|
+
sessionId: item.sessionId ?? null,
|
|
33830
|
+
threadId: null,
|
|
33831
|
+
itemId: null,
|
|
33832
|
+
requestId: null,
|
|
33833
|
+
issue: item.issue ?? null,
|
|
33834
|
+
pr: item.pr ?? null,
|
|
33835
|
+
automationRunId: null,
|
|
33836
|
+
createdAt: existing?.createdAt ?? now,
|
|
33837
|
+
updatedAt: now,
|
|
33838
|
+
resolvedAt: null,
|
|
33839
|
+
resolution: null
|
|
33840
|
+
});
|
|
33841
|
+
}
|
|
33842
|
+
/** Notice for the PM or the orchestrator about the loop. */
|
|
33843
|
+
function factoryNotice(ctx, opts) {
|
|
33844
|
+
ctx.notify({
|
|
33845
|
+
id: opts.id,
|
|
33846
|
+
type: "factory.update",
|
|
33847
|
+
projectId: opts.projectId,
|
|
33848
|
+
target: opts.target,
|
|
33849
|
+
sessionId: null,
|
|
33850
|
+
title: opts.title,
|
|
33851
|
+
data: {
|
|
33852
|
+
event: opts.event,
|
|
33853
|
+
text: opts.text,
|
|
33854
|
+
itemIds: opts.itemIds ?? []
|
|
33855
|
+
}
|
|
33856
|
+
}, opts.wake ?? true);
|
|
33857
|
+
}
|
|
33858
|
+
/** Pause the loop (failures, regression, budget) with an inbox item for everything but budget. */
|
|
33859
|
+
function pauseLoop(ctx, loop, reason, opts = { inbox: true }) {
|
|
33860
|
+
if (loop.state === "paused" && loop.pausedReason === reason) return loop;
|
|
33861
|
+
ctx.log?.(`factory: pausing ${loop.projectId}: ${reason}`);
|
|
33862
|
+
const next = {
|
|
33863
|
+
...loop,
|
|
33864
|
+
state: "paused",
|
|
33865
|
+
pausedReason: reason
|
|
33866
|
+
};
|
|
33867
|
+
if (opts.inbox) {
|
|
33868
|
+
raiseInbox(ctx, {
|
|
33869
|
+
id: `inbox:factory-paused:${loop.projectId}`,
|
|
33870
|
+
kind: "run_failed",
|
|
33871
|
+
priority: "high",
|
|
33872
|
+
projectId: loop.projectId,
|
|
33873
|
+
title: "Factory loop paused",
|
|
33874
|
+
body: `${reason}. Resume it from the factory panel once you have looked.`
|
|
33875
|
+
});
|
|
33876
|
+
factoryNotice(ctx, {
|
|
33877
|
+
id: `factory.paused:${loop.projectId}:${ctx.now().toISOString()}`,
|
|
33878
|
+
projectId: loop.projectId,
|
|
33879
|
+
target: "pm",
|
|
33880
|
+
event: "paused",
|
|
33881
|
+
title: "Factory loop paused",
|
|
33882
|
+
text: `${reason}. No new work starts until the user resumes the loop.`,
|
|
33883
|
+
itemIds: opts.items?.map((i) => i.id),
|
|
33884
|
+
wake: false
|
|
33885
|
+
});
|
|
33886
|
+
}
|
|
33887
|
+
return next;
|
|
33888
|
+
}
|
|
33889
|
+
/**
|
|
33890
|
+
* Spend since `day` that counts against the project's daily factory budget: the factory's own
|
|
33891
|
+
* sessions (`budgetScope: "factory"`, the default) or the whole project (`"project"`).
|
|
33892
|
+
*/
|
|
33893
|
+
function budgetSpend(ctx, projectId, day, limits) {
|
|
33894
|
+
if (limits.budgetScope === "project") return ctx.usage.spendSince(day, projectId);
|
|
33895
|
+
const ids = new Set(ctx.sessions.list(projectId).map((s) => s.id));
|
|
33896
|
+
return ctx.usage.spendSince(day, projectId, ids);
|
|
33897
|
+
}
|
|
33898
|
+
// ../server/src/services/factory/prompts.ts
|
|
33899
|
+
/**
|
|
33900
|
+
* Prompts of the factory loop (§10.1): improvement finders, builders, independent reviewers,
|
|
33901
|
+
* the PM's triage instructions and the follow-ups the loop sends. The server enforces limits;
|
|
33902
|
+
* these prompts only carry the judgement work.
|
|
33903
|
+
*/
|
|
33904
|
+
const AREA_BRIEF = {
|
|
33905
|
+
features: "feature ideas that move the project towards its goals and serve its target users (small, shippable increments)",
|
|
33906
|
+
ux: "user-experience problems: confusing flows, missing feedback, accessibility gaps, rough edges",
|
|
33907
|
+
performance: "performance problems: slow paths, wasteful work, bundle size, blocking I/O",
|
|
33908
|
+
tech_debt: "technical debt that slows work down: duplication, dead code, unclear modules, outdated patterns",
|
|
33909
|
+
test_gaps: "behaviour that is not covered by tests, or tests that are flaky or misleading",
|
|
33910
|
+
security: "security weaknesses: unsafe input handling, secrets, injection, outdated vulnerable dependencies"
|
|
33911
|
+
};
|
|
33912
|
+
function visionBlock(vision) {
|
|
33913
|
+
return [
|
|
33914
|
+
"<vision>",
|
|
33915
|
+
vision.trim(),
|
|
33916
|
+
"</vision>"
|
|
33917
|
+
].join("\n");
|
|
33918
|
+
}
|
|
33919
|
+
function criteriaList(item) {
|
|
33920
|
+
return item.acceptanceCriteria.map((c, i) => `${i + 1}. ${c}`).join("\n");
|
|
33921
|
+
}
|
|
33922
|
+
/** An improvement finder for one area. */
|
|
33923
|
+
function discoveryPrompt(opts) {
|
|
33924
|
+
return [
|
|
33925
|
+
`You are an improvement finder for this repository's factory loop. Area: **${opts.area}** — ${AREA_BRIEF[opts.area]}.`,
|
|
33926
|
+
"",
|
|
33927
|
+
"Read the code (do not modify any files, do not commit, do not open pull requests). Find at most 3 concrete, independently shippable improvements in this area that are grounded in the vision below. Each must be small enough for one pull request, must respect the out-of-scope list and must not touch protected areas.",
|
|
33928
|
+
"",
|
|
33929
|
+
visionBlock(opts.vision),
|
|
33930
|
+
"",
|
|
33931
|
+
opts.known.length > 0 ? `Already known (do not repeat): ${opts.known.map((t) => `"${t}"`).join(", ")}.` : "",
|
|
33932
|
+
"Don't ask the user anything: when you're unsure about a candidate, state your assumption in its summary or leave it out. If the code doesn't match the vision (a different stack or architecture, say), the work that brings it in line is a candidate in its own right.",
|
|
33933
|
+
"When done, call `factory_report_candidates` once with your candidates (title, summary of why it matters, testable acceptance criteria, paths it would touch). If you found nothing worth doing, call it with an empty list. Then call `finish` with a one-line summary."
|
|
33934
|
+
].filter((l) => l !== "").join("\n");
|
|
33935
|
+
}
|
|
33936
|
+
/** The builder of one accepted item. */
|
|
33937
|
+
function builderPrompt(item, vision) {
|
|
33938
|
+
return [
|
|
33939
|
+
`Implement ${item.issue ? `GitHub issue #${item.issue.number} (${item.issue.url})` : "the item"}: **${item.title}**`,
|
|
33940
|
+
"",
|
|
33941
|
+
item.summary,
|
|
33942
|
+
"",
|
|
33943
|
+
"Acceptance criteria:",
|
|
33944
|
+
criteriaList(item),
|
|
33945
|
+
"",
|
|
33946
|
+
visionBlock(vision),
|
|
33947
|
+
"",
|
|
33948
|
+
"How to work:",
|
|
33949
|
+
"- Work only in this worktree. Keep the change small and focused on the criteria; stay out of protected areas and never deploy or release anything.",
|
|
33950
|
+
"- Add or update tests that prove every criterion, and run the project's tests and linters until they pass.",
|
|
33951
|
+
"- Don't stop to ask the user: make the reasonable call within the criteria and the vision, and say what you assumed in the pull request description. If you truly cannot go on, call `finish` with outcome `blocked`.",
|
|
33952
|
+
"- Commit, then open the pull request with `create_pull_request` (it links the issue with `Fixes #N`).",
|
|
33953
|
+
"- If you changed anything visible in a UI, start the dev server, take browser screenshots of the change and attach them with `attach_evidence`.",
|
|
33954
|
+
"- Call `finish` with the outcome, a summary, the tests you ran and the changed files.",
|
|
33955
|
+
"An independent reviewer on another provider checks your pull request against the criteria and the vision before CodeDen merges it automatically."
|
|
33956
|
+
].join("\n");
|
|
33957
|
+
}
|
|
33958
|
+
/** The independent reviewer of one pull request. */
|
|
33959
|
+
function reviewerPrompt(item, vision, note, localBase) {
|
|
33960
|
+
const pr = item.pr ? `#${item.pr.number} (${item.pr.url})` : "the pull request";
|
|
33961
|
+
const read = localBase ? `You are in the author's worktree, on the PR branch. Read the change locally and offline: \`git diff ${localBase}...HEAD\`, \`git log --oneline ${localBase}..HEAD\`, and the code around it; run the tests if useful. Do not use the network (no \`gh\`, no fetch). Do not modify files, commit or push.` : `Read the change (for example \`gh pr diff ${item.pr?.number ?? ""}\` and \`gh pr view ${item.pr?.number ?? ""}\`), and the code around it. Do not modify files, commit, push or comment on GitHub.`;
|
|
33962
|
+
return [
|
|
33963
|
+
`You are the independent reviewer of pull request ${pr}: **${item.title}**. Another agent wrote it; CodeDen merges it automatically only if you approve and every other merge check passes.`,
|
|
33964
|
+
note ? `Note: ${note}` : "",
|
|
33965
|
+
"",
|
|
33966
|
+
"Acceptance criteria:",
|
|
33967
|
+
criteriaList(item),
|
|
33968
|
+
"",
|
|
33969
|
+
visionBlock(vision),
|
|
33970
|
+
"",
|
|
33971
|
+
read,
|
|
33972
|
+
"Judge: does it meet every criterion? Does it fit the vision and stay out of its out-of-scope list and protected areas? Are there tests that prove the behaviour (or are none needed)? Any security problems (injection, secrets, unsafe input, risky dependencies)?",
|
|
33973
|
+
"",
|
|
33974
|
+
"Then call `factory_submit_review` exactly once with your verdict: `approve` only when everything holds, `request_changes` for fixable problems, `reject` when the change should not exist. Be specific in the summary."
|
|
33975
|
+
].filter((l) => l !== "").join("\n");
|
|
33976
|
+
}
|
|
33977
|
+
/** Message to the builder after failed merge checks. */
|
|
33978
|
+
function fixPrompt(item, failed) {
|
|
33979
|
+
return [
|
|
33980
|
+
`The merge checks for your pull request${item.pr ? ` #${item.pr.number}` : ""} failed:`,
|
|
33981
|
+
...failed.map((c) => `- ${c.name}: ${c.detail ?? "failed"}`),
|
|
33982
|
+
item.review && item.review.verdict !== "approve" ? `Reviewer: ${item.review.summary}` : "",
|
|
33983
|
+
"",
|
|
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
|
+
].filter((l) => l !== "").join("\n");
|
|
33986
|
+
}
|
|
33987
|
+
/** Ask a reviewer to look again after fixes. */
|
|
33988
|
+
function reReviewPrompt(item) {
|
|
33989
|
+
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.`;
|
|
33990
|
+
}
|
|
33991
|
+
const BUILDER_NUDGE = "You ended your turn without an open pull request. If the work is done, commit it and call `create_pull_request`; if you are blocked, call `finish` with outcome `blocked` and explain why.";
|
|
33992
|
+
const REVIEWER_NUDGE = "You ended your turn without a verdict. Call `factory_submit_review` now with `approve`, `request_changes` or `reject`.";
|
|
33993
|
+
/** An agent writes the revert when the API cannot (main moved on after the merge). */
|
|
33994
|
+
function revertPrompt(opts) {
|
|
33995
|
+
return [`CI on the default branch failed after pull request #${opts.prNumber} ("${opts.title}") was merged as ${opts.sha}.`, `Revert that commit in this worktree (\`git revert --no-edit ${opts.sha}\`), resolve conflicts conservatively, run the tests, and open a pull request titled \`Revert "${opts.title}"\` with \`create_pull_request\`. Do nothing else. Then call \`finish\`.`].join("\n");
|
|
33996
|
+
}
|
|
33997
|
+
/**
|
|
33998
|
+
* The PM's structured triage instructions for new candidates. `unattended` (dark factory): the
|
|
33999
|
+
* PM makes every judgement call itself and records it, since nobody is watching to answer.
|
|
34000
|
+
*/
|
|
34001
|
+
function triageInstructions(items, unattended = false) {
|
|
34002
|
+
return [
|
|
34003
|
+
`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
|
+
...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
|
+
"",
|
|
34006
|
+
"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.",
|
|
34007
|
+
"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
|
+
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
|
+
].filter((l) => l !== "").join("\n");
|
|
34010
|
+
}
|
|
34011
|
+
// ../server/src/services/factory/sessions.ts
|
|
34012
|
+
/** Who starts factory sessions; `id: "factory"` marks them for limits and the kill switch. */
|
|
34013
|
+
const FACTORY_STARTER = {
|
|
34014
|
+
kind: "pm",
|
|
34015
|
+
id: "factory"
|
|
34016
|
+
};
|
|
34017
|
+
/** Statuses that hold a session slot (`maxConcurrentSessions`). */
|
|
34018
|
+
const SLOT_STATUSES = /* @__PURE__ */ new Set([
|
|
34019
|
+
"created",
|
|
34020
|
+
"queued",
|
|
34021
|
+
"starting",
|
|
34022
|
+
"running",
|
|
34023
|
+
"waiting"
|
|
34024
|
+
]);
|
|
34025
|
+
/** Statuses of sessions the kill switch stops. */
|
|
34026
|
+
const LIVE_STATUSES = /* @__PURE__ */ new Set([
|
|
34027
|
+
...SLOT_STATUSES,
|
|
34028
|
+
"idle",
|
|
34029
|
+
"interrupted"
|
|
34030
|
+
]);
|
|
34031
|
+
function isFactorySession(s) {
|
|
34032
|
+
return s.startedBy.kind === FACTORY_STARTER.kind && s.startedBy.id === FACTORY_STARTER.id;
|
|
34033
|
+
}
|
|
34034
|
+
/** Starts and stops the factory's agent sessions through the normal session service (§8.2). */
|
|
34035
|
+
var FactorySessions = class {
|
|
34036
|
+
deps;
|
|
34037
|
+
constructor(deps) {
|
|
34038
|
+
this.deps = deps;
|
|
34039
|
+
}
|
|
34040
|
+
/** Factory sessions of a project, newest first. */
|
|
34041
|
+
list(projectId) {
|
|
34042
|
+
return this.deps.repo.list({
|
|
34043
|
+
projectId,
|
|
34044
|
+
role: "agent"
|
|
34045
|
+
}).filter(isFactorySession);
|
|
34046
|
+
}
|
|
34047
|
+
/** Factory sessions holding a slot right now. */
|
|
34048
|
+
activeCount(projectId) {
|
|
34049
|
+
return this.list(projectId).filter((s) => SLOT_STATUSES.has(s.status)).length;
|
|
34050
|
+
}
|
|
34051
|
+
/** Providers the runner offers (the Fake provider only when nothing else is there). */
|
|
34052
|
+
providers() {
|
|
34053
|
+
const ids = this.deps.runner.providers().map((p) => p.id);
|
|
34054
|
+
const real = ids.filter((id) => id !== "fake");
|
|
34055
|
+
return real.length > 0 ? real : ids;
|
|
34056
|
+
}
|
|
34057
|
+
startDiscovery(projectId, area, vision, known) {
|
|
34058
|
+
return this.start({
|
|
34059
|
+
projectId,
|
|
34060
|
+
purpose: `Factory: find ${area.replace("_", " ")} improvements`,
|
|
34061
|
+
workKey: `factory:discover:${area}`,
|
|
34062
|
+
scope: { area: `factory-discover-${area}` },
|
|
34063
|
+
title: `Factory discovery: ${area}`,
|
|
34064
|
+
input: {
|
|
34065
|
+
text: discoveryPrompt({
|
|
34066
|
+
area,
|
|
34067
|
+
vision,
|
|
34068
|
+
known
|
|
34069
|
+
}),
|
|
34070
|
+
attachments: []
|
|
34071
|
+
}
|
|
34072
|
+
});
|
|
34073
|
+
}
|
|
34074
|
+
startBuilder(item, vision, provider) {
|
|
34075
|
+
return this.start({
|
|
34076
|
+
projectId: item.projectId,
|
|
34077
|
+
provider,
|
|
34078
|
+
purpose: `Factory: ${item.title}`.slice(0, 300),
|
|
34079
|
+
workKey: item.workKey,
|
|
34080
|
+
title: item.title.slice(0, 200),
|
|
34081
|
+
issue: item.issue ?? void 0,
|
|
34082
|
+
input: {
|
|
34083
|
+
text: builderPrompt(item, vision),
|
|
34084
|
+
attachments: []
|
|
34085
|
+
}
|
|
34086
|
+
});
|
|
34087
|
+
}
|
|
34088
|
+
/**
|
|
34089
|
+
* Start the reviewer offline: in the builder's worktree when there is one, otherwise (a
|
|
34090
|
+
* revert PR opened through the API) in a new worktree on the PR's head, fetched from origin
|
|
34091
|
+
* by CodeDen before the session starts.
|
|
34092
|
+
*/
|
|
34093
|
+
startReviewer(item, vision, provider, note, pr) {
|
|
34094
|
+
const builder = item.builderSessionId ? this.deps.repo.find(item.builderSessionId)?.session : void 0;
|
|
34095
|
+
const worktree = builder?.worktreePath ?? null;
|
|
34096
|
+
let workspace = { mode: "worktree" };
|
|
34097
|
+
let localBase = null;
|
|
34098
|
+
if (worktree) {
|
|
34099
|
+
workspace = {
|
|
34100
|
+
mode: "existing",
|
|
34101
|
+
path: worktree
|
|
34102
|
+
};
|
|
34103
|
+
localBase = builder?.baseBranch ?? "main";
|
|
34104
|
+
} else if (pr) {
|
|
34105
|
+
workspace = {
|
|
34106
|
+
mode: "worktree",
|
|
34107
|
+
branch: `codeden/review-${item.pr?.number ?? item.id.slice(0, 8)}-${item.id.slice(0, 4)}`,
|
|
34108
|
+
baseRef: `origin/${pr.headBranch}`,
|
|
34109
|
+
fetch: true
|
|
34110
|
+
};
|
|
34111
|
+
localBase = `origin/${pr.baseBranch}`;
|
|
34112
|
+
}
|
|
34113
|
+
return this.start({
|
|
34114
|
+
projectId: item.projectId,
|
|
34115
|
+
provider,
|
|
34116
|
+
purpose: `Factory review: ${item.title}`.slice(0, 300),
|
|
34117
|
+
workKey: `factory:review:${item.pr?.number ?? item.id}`,
|
|
34118
|
+
title: `Review: ${item.title}`.slice(0, 200),
|
|
34119
|
+
issue: item.issue ?? void 0,
|
|
34120
|
+
workspace,
|
|
34121
|
+
input: {
|
|
34122
|
+
text: reviewerPrompt(item, vision, note, localBase),
|
|
34123
|
+
attachments: []
|
|
34124
|
+
}
|
|
34125
|
+
});
|
|
34126
|
+
}
|
|
34127
|
+
startRevert(projectId, merge) {
|
|
34128
|
+
return this.start({
|
|
34129
|
+
projectId,
|
|
34130
|
+
purpose: `Factory: revert #${merge.prNumber} after a CI regression`,
|
|
34131
|
+
workKey: `factory:revert:${merge.sha.slice(0, 7)}`,
|
|
34132
|
+
title: `Revert #${merge.prNumber}`,
|
|
34133
|
+
input: {
|
|
34134
|
+
text: revertPrompt(merge),
|
|
34135
|
+
attachments: []
|
|
34136
|
+
}
|
|
34137
|
+
});
|
|
34138
|
+
}
|
|
34139
|
+
/** Send a follow-up to a session (queued behind a running turn). */
|
|
34140
|
+
async message(sessionId, text) {
|
|
34141
|
+
await this.deps.sessions.send(sessionId, {
|
|
34142
|
+
input: {
|
|
34143
|
+
text,
|
|
34144
|
+
attachments: []
|
|
34145
|
+
},
|
|
34146
|
+
whenBusy: "queue"
|
|
34147
|
+
}, "codeden");
|
|
34148
|
+
}
|
|
34149
|
+
/** Stop every live factory session of a project (kill switch). Returns the stopped ids. */
|
|
34150
|
+
async stopAll(projectId) {
|
|
34151
|
+
const stopped = [];
|
|
34152
|
+
for (const s of this.list(projectId)) {
|
|
34153
|
+
if (!LIVE_STATUSES.has(s.status)) continue;
|
|
34154
|
+
try {
|
|
34155
|
+
await this.deps.sessions.stop(s.id);
|
|
34156
|
+
stopped.push(s.id);
|
|
34157
|
+
} catch (error) {
|
|
34158
|
+
this.deps.log?.(`factory: stop ${s.id}: ${errorMessage$1(error)}`);
|
|
34159
|
+
}
|
|
34160
|
+
}
|
|
34161
|
+
return stopped;
|
|
34162
|
+
}
|
|
34163
|
+
start(req) {
|
|
34164
|
+
return this.deps.sessions.start(StartSessionRequest.parse({
|
|
34165
|
+
role: "agent",
|
|
34166
|
+
startedBy: FACTORY_STARTER,
|
|
34167
|
+
...req
|
|
34168
|
+
}));
|
|
34169
|
+
}
|
|
34170
|
+
};
|
|
34171
|
+
// ../server/src/services/factory/unattended.ts
|
|
34172
|
+
/**
|
|
34173
|
+
* Dark factory (§10.1): "set the vision and the limits, then step away". Nobody is watching, so
|
|
34174
|
+
* the factory's agents and the project's PM never wait on the user. A question they would have
|
|
34175
|
+
* asked is answered at once with {@link UNATTENDED_ANSWER}: decide from the vision, record the
|
|
34176
|
+
* decision with `factory_record_decision`, and carry on. The decision reaches the inbox and the
|
|
34177
|
+
* daily digest, where the user can review or override it later.
|
|
34178
|
+
*/
|
|
34179
|
+
/** Prefix of the inbox items `factory_record_decision` raises (the digest lists them). */
|
|
34180
|
+
const DECISION_INBOX_PREFIX = "inbox:factory-decision:";
|
|
34181
|
+
/** The answer an unattended factory agent gets instead of waiting for the user. */
|
|
34182
|
+
const UNATTENDED_ANSWER = [
|
|
34183
|
+
"Not asked: this project runs as a dark factory, so nobody is watching to answer. Decide yourself:",
|
|
34184
|
+
"- Follow the project vision. It wins over the current code: when the code doesn't match the vision (a different stack or architecture, say), the work that brings it in line comes first, as its own item, and work that would be thrown away by it waits.",
|
|
34185
|
+
"- Stay inside the vision's scope and the factory's limits, and prefer the smaller, reversible option.",
|
|
34186
|
+
"Then call `factory_record_decision` with the question, what you decided and why, so the user can review and override it later, and carry on.",
|
|
34187
|
+
"Set `needsHuman: true` only for a protected area, a vision that is missing or contradicts itself, or something irreversible outside the code (deploys, releases, data, money). That flags it for the user without waiting: skip only the work that depends on it.",
|
|
34188
|
+
"If the user is talking to you directly in this conversation, ask them in your reply instead."
|
|
34189
|
+
].join("\n");
|
|
34190
|
+
/**
|
|
34191
|
+
* True when nobody is expected to answer this session's questions: its project is a dark factory
|
|
34192
|
+
* and the session is the PM or one of the factory's own agent sessions. Sessions the user
|
|
34193
|
+
* started themselves still ask as usual.
|
|
34194
|
+
*/
|
|
34195
|
+
function isUnattended(project, session) {
|
|
34196
|
+
if (project?.autonomyMode !== "dark_factory") return false;
|
|
34197
|
+
return session.role === "pm" || isFactorySession(session);
|
|
34198
|
+
}
|
|
34199
|
+
/** The session behind `sessionId` when nobody is expected to answer its questions. */
|
|
34200
|
+
function unattendedSession(ctx, sessionId) {
|
|
34201
|
+
const session = ctx.repo.find(sessionId)?.session;
|
|
34202
|
+
if (!session?.projectId) return void 0;
|
|
34203
|
+
let project;
|
|
34204
|
+
try {
|
|
34205
|
+
project = ctx.projects.get(session.projectId);
|
|
34206
|
+
} catch {
|
|
34207
|
+
return;
|
|
34208
|
+
}
|
|
34209
|
+
return isUnattended(project, session) ? session : void 0;
|
|
34210
|
+
}
|
|
34211
|
+
/** Answers for a provider's own question request, one free-text answer per question. */
|
|
34212
|
+
function unattendedAnswers(item) {
|
|
34213
|
+
return Object.fromEntries(item.questions.map((q) => [q.id, [UNATTENDED_ANSWER]]));
|
|
34214
|
+
}
|
|
34215
|
+
/**
|
|
34216
|
+
* A question the provider asked through its own tool (not `ask_user`) in an unattended
|
|
34217
|
+
* session: answer it straight away with {@link UNATTENDED_ANSWER}, so the turn never waits.
|
|
34218
|
+
*/
|
|
34219
|
+
async function answerProviderQuestion(ctx, respond, e) {
|
|
34220
|
+
if (e.type !== "agent.item.started" || !e.sessionId) return false;
|
|
34221
|
+
const item = e.payload.item;
|
|
34222
|
+
if (item?.kind !== "user_question" || item.deferred || item.status !== "in_progress") return false;
|
|
34223
|
+
if (!unattendedSession(ctx, e.sessionId)) return false;
|
|
34224
|
+
try {
|
|
34225
|
+
await respond(e.sessionId, {
|
|
34226
|
+
requestId: item.requestId,
|
|
34227
|
+
response: {
|
|
34228
|
+
kind: "answer",
|
|
34229
|
+
answers: unattendedAnswers(item)
|
|
34230
|
+
}
|
|
34231
|
+
});
|
|
34232
|
+
return true;
|
|
34233
|
+
} catch (error) {
|
|
34234
|
+
ctx.log?.(`factory: answer question in ${e.sessionId}: ${errorMessage$1(error)}`);
|
|
34235
|
+
return false;
|
|
34236
|
+
}
|
|
34237
|
+
}
|
|
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");
|
|
34241
|
+
const id = `${DECISION_INBOX_PREFIX}${randomUUID()}`;
|
|
34242
|
+
raiseInbox(ctx, {
|
|
34243
|
+
id,
|
|
34244
|
+
kind: "message",
|
|
34245
|
+
priority: req.needsHuman ? "high" : "low",
|
|
34246
|
+
projectId: session.projectId,
|
|
34247
|
+
sessionId: session.id,
|
|
34248
|
+
title: `${req.needsHuman ? "Factory needs you" : "Factory decided"}: ${oneLine(req.question, 120)}`,
|
|
34249
|
+
body: [
|
|
34250
|
+
`**Question:** ${req.question}`,
|
|
34251
|
+
"",
|
|
34252
|
+
`**Decision:** ${req.decision}`,
|
|
34253
|
+
"",
|
|
34254
|
+
`**Why:** ${req.reasoning}`,
|
|
34255
|
+
"",
|
|
34256
|
+
req.needsHuman ? "The factory carried on with everything that doesn't depend on this. Reply in the session, or change the vision, to settle it." : "Nothing waited on you. To change it, tell the project manager or edit the vision."
|
|
34257
|
+
].join("\n")
|
|
34258
|
+
});
|
|
34259
|
+
return {
|
|
34260
|
+
inboxItemId: id,
|
|
34261
|
+
message: req.needsHuman ? "Recorded and flagged for the user. Don't wait: carry on with the work that doesn't depend on it." : "Recorded. Carry on."
|
|
34262
|
+
};
|
|
34263
|
+
}
|
|
34264
|
+
function oneLine(text, max) {
|
|
34265
|
+
const line = text.replace(/\s+/g, " ").trim();
|
|
34266
|
+
return line.length > max ? `${line.slice(0, max - 1)}…` : line;
|
|
34267
|
+
}
|
|
33709
34268
|
// ../server/src/mcp/transcript.ts
|
|
33710
34269
|
function currentTurn(services, sessionId) {
|
|
33711
34270
|
const threadId = services.sessionRepo.get(sessionId).rootThreadId;
|
|
@@ -33749,7 +34308,8 @@ function postSharedArtifact(services, sessionId, artifact, caption) {
|
|
|
33749
34308
|
/**
|
|
33750
34309
|
* `ask_user` (§8.3, §11.1): a question for the user, shown in the transcript and the inbox.
|
|
33751
34310
|
* Blocks for a few minutes; unanswered questions stay in the inbox and the answer arrives
|
|
33752
|
-
* later as a new message (see `QuestionBroker`).
|
|
34311
|
+
* later as a new message (see `QuestionBroker`). In a dark factory the PM and the factory's
|
|
34312
|
+
* agents are unattended: they get {@link UNATTENDED_ANSWER} at once and decide themselves.
|
|
33753
34313
|
*/
|
|
33754
34314
|
const askUser = defineTool({
|
|
33755
34315
|
name: "ask_user",
|
|
@@ -33767,6 +34327,7 @@ const askUser = defineTool({
|
|
|
33767
34327
|
multiSelect: boolean().optional()
|
|
33768
34328
|
}),
|
|
33769
34329
|
handler: async (input, { services, sessionId, signal }) => {
|
|
34330
|
+
if (services.factory.isUnattended(sessionId)) return UNATTENDED_ANSWER;
|
|
33770
34331
|
const turn = currentTurn(services, sessionId);
|
|
33771
34332
|
const outcome = await services.questions.ask(sessionId, turn, {
|
|
33772
34333
|
question: input.question,
|
|
@@ -34151,6 +34712,19 @@ const factoryTools = [
|
|
|
34151
34712
|
workKey: item.workKey
|
|
34152
34713
|
};
|
|
34153
34714
|
})
|
|
34715
|
+
}),
|
|
34716
|
+
defineTool({
|
|
34717
|
+
name: "factory_record_decision",
|
|
34718
|
+
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.",
|
|
34719
|
+
roles: ["agent", "pm"],
|
|
34720
|
+
scope: "session",
|
|
34721
|
+
alwaysLoad: true,
|
|
34722
|
+
isAvailable: (services, caller) => {
|
|
34723
|
+
if (!caller) return true;
|
|
34724
|
+
return caller.sessionId !== null && services.factory.isUnattended(caller.sessionId);
|
|
34725
|
+
},
|
|
34726
|
+
input: FactoryDecisionRequest,
|
|
34727
|
+
handler: async (input, { services, sessionId }) => asTool(() => services.factory.recordDecision(services.sessionRepo.get(sessionId), input))
|
|
34154
34728
|
})
|
|
34155
34729
|
];
|
|
34156
34730
|
// ../server/src/mcp/tools/finish.ts
|
|
@@ -45953,195 +46527,6 @@ function killTree(pid) {
|
|
|
45953
46527
|
process.kill(pid, "SIGKILL");
|
|
45954
46528
|
} catch {}
|
|
45955
46529
|
}
|
|
45956
|
-
// ../server/src/services/factory/vision.ts
|
|
45957
|
-
/**
|
|
45958
|
-
* The project vision (§10.1): `.codeden/VISION.md`, required for autopilot and dark factory.
|
|
45959
|
-
* Sections are `##` headings; bullets under them are the entries. Protected areas become path
|
|
45960
|
-
* globs (anything in backticks, or that looks like a path) and topics (plain words).
|
|
45961
|
-
*/
|
|
45962
|
-
const VISION_TEMPLATE = `# Vision
|
|
45963
|
-
|
|
45964
|
-
Describe what this project is for. The factory scores every piece of work against this file.
|
|
45965
|
-
|
|
45966
|
-
## Goals
|
|
45967
|
-
- What should this project achieve in the next months?
|
|
45968
|
-
|
|
45969
|
-
## Target users
|
|
45970
|
-
- Who uses it, and what do they need?
|
|
45971
|
-
|
|
45972
|
-
## Principles
|
|
45973
|
-
- Small, well-tested changes
|
|
45974
|
-
- Keep the public API stable
|
|
45975
|
-
|
|
45976
|
-
## Out of scope
|
|
45977
|
-
- Things the factory must never propose
|
|
45978
|
-
|
|
45979
|
-
## Quality bar
|
|
45980
|
-
- Every behaviour change comes with tests
|
|
45981
|
-
- CI is green before merge
|
|
45982
|
-
|
|
45983
|
-
## Protected areas
|
|
45984
|
-
Paths (globs in backticks) and topics that always need a human. The factory never merges
|
|
45985
|
-
changes that touch them; they go to your inbox.
|
|
45986
|
-
- \`**/auth/**\`
|
|
45987
|
-
- \`**/migrations/**\`
|
|
45988
|
-
- \`.github/workflows/**\`
|
|
45989
|
-
- \`LICENSE\`
|
|
45990
|
-
- payments
|
|
45991
|
-
- licensing
|
|
45992
|
-
`;
|
|
45993
|
-
/** Paths that deploy or release, protected unless `allowDeploys` is on. */
|
|
45994
|
-
const DEPLOY_GLOBS = [
|
|
45995
|
-
".github/workflows/*deploy*",
|
|
45996
|
-
".github/workflows/*release*",
|
|
45997
|
-
".github/workflows/*publish*",
|
|
45998
|
-
"**/Dockerfile",
|
|
45999
|
-
"**/fly.toml",
|
|
46000
|
-
"**/vercel.json",
|
|
46001
|
-
"**/netlify.toml"
|
|
46002
|
-
];
|
|
46003
|
-
/** True when `topic` appears as a word or phrase in `text`. */
|
|
46004
|
-
function mentionsTopic(text, topic) {
|
|
46005
|
-
const escaped = topic.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\s+/g, "\\s+");
|
|
46006
|
-
return new RegExp(`\\b${escaped}\\b`, "i").test(text);
|
|
46007
|
-
}
|
|
46008
|
-
function visionFile(project) {
|
|
46009
|
-
const file = resolve(project.path, project.visionPath);
|
|
46010
|
-
const rel = relative(project.path, file);
|
|
46011
|
-
if (rel.startsWith("..") || isAbsolute(rel)) throw badRequest("The vision path must stay inside the project", "invalid_vision_path");
|
|
46012
|
-
return file;
|
|
46013
|
-
}
|
|
46014
|
-
/** Read the vision; the template (and `exists: false`) when the file is missing. */
|
|
46015
|
-
function readVision(project) {
|
|
46016
|
-
const file = visionFile(project);
|
|
46017
|
-
const exists = existsSync(file);
|
|
46018
|
-
const contents = exists ? readFileSync(file, "utf8") : VISION_TEMPLATE;
|
|
46019
|
-
return {
|
|
46020
|
-
path: project.visionPath,
|
|
46021
|
-
exists,
|
|
46022
|
-
contents,
|
|
46023
|
-
parsed: parseVision(contents)
|
|
46024
|
-
};
|
|
46025
|
-
}
|
|
46026
|
-
/** Write the vision atomically (in the main checkout; commit it to share it with agents). */
|
|
46027
|
-
function writeVision(project, contents) {
|
|
46028
|
-
const file = visionFile(project);
|
|
46029
|
-
mkdirSync(dirname(file), { recursive: true });
|
|
46030
|
-
const tmp = `${file}.${process.pid}.tmp`;
|
|
46031
|
-
writeFileSync(tmp, contents.endsWith("\n") ? contents : `${contents}\n`);
|
|
46032
|
-
renameSync(tmp, file);
|
|
46033
|
-
return readVision(project);
|
|
46034
|
-
}
|
|
46035
|
-
// ../server/src/services/factory/context.ts
|
|
46036
|
-
/** The vision text and its parsed form ("" and empty when the file is missing). */
|
|
46037
|
-
function visionOf(project) {
|
|
46038
|
-
try {
|
|
46039
|
-
const v = readVision(project);
|
|
46040
|
-
return {
|
|
46041
|
-
text: v.exists ? v.contents : "",
|
|
46042
|
-
parsed: v.parsed,
|
|
46043
|
-
exists: v.exists
|
|
46044
|
-
};
|
|
46045
|
-
} catch {
|
|
46046
|
-
return {
|
|
46047
|
-
text: "",
|
|
46048
|
-
exists: false,
|
|
46049
|
-
parsed: {
|
|
46050
|
-
goals: [],
|
|
46051
|
-
users: [],
|
|
46052
|
-
principles: [],
|
|
46053
|
-
outOfScope: [],
|
|
46054
|
-
qualityBar: [],
|
|
46055
|
-
protectedAreas: {
|
|
46056
|
-
globs: [],
|
|
46057
|
-
topics: []
|
|
46058
|
-
}
|
|
46059
|
-
}
|
|
46060
|
-
};
|
|
46061
|
-
}
|
|
46062
|
-
}
|
|
46063
|
-
/** Raise (or refresh) a factory inbox item. */
|
|
46064
|
-
function raiseInbox(ctx, item) {
|
|
46065
|
-
const existing = ctx.inbox.find(item.id);
|
|
46066
|
-
if (existing?.status === "open" && existing.title === item.title && existing.body === item.body) return;
|
|
46067
|
-
const now = ctx.now().toISOString();
|
|
46068
|
-
ctx.inbox.put({
|
|
46069
|
-
id: item.id,
|
|
46070
|
-
kind: item.kind,
|
|
46071
|
-
status: "open",
|
|
46072
|
-
priority: item.priority ?? "normal",
|
|
46073
|
-
title: item.title,
|
|
46074
|
-
body: item.body,
|
|
46075
|
-
projectId: item.projectId,
|
|
46076
|
-
sessionId: item.sessionId ?? null,
|
|
46077
|
-
threadId: null,
|
|
46078
|
-
itemId: null,
|
|
46079
|
-
requestId: null,
|
|
46080
|
-
issue: item.issue ?? null,
|
|
46081
|
-
pr: item.pr ?? null,
|
|
46082
|
-
automationRunId: null,
|
|
46083
|
-
createdAt: existing?.createdAt ?? now,
|
|
46084
|
-
updatedAt: now,
|
|
46085
|
-
resolvedAt: null,
|
|
46086
|
-
resolution: null
|
|
46087
|
-
});
|
|
46088
|
-
}
|
|
46089
|
-
/** Notice for the PM or the orchestrator about the loop. */
|
|
46090
|
-
function factoryNotice(ctx, opts) {
|
|
46091
|
-
ctx.notify({
|
|
46092
|
-
id: opts.id,
|
|
46093
|
-
type: "factory.update",
|
|
46094
|
-
projectId: opts.projectId,
|
|
46095
|
-
target: opts.target,
|
|
46096
|
-
sessionId: null,
|
|
46097
|
-
title: opts.title,
|
|
46098
|
-
data: {
|
|
46099
|
-
event: opts.event,
|
|
46100
|
-
text: opts.text,
|
|
46101
|
-
itemIds: opts.itemIds ?? []
|
|
46102
|
-
}
|
|
46103
|
-
}, opts.wake ?? true);
|
|
46104
|
-
}
|
|
46105
|
-
/** Pause the loop (failures, regression, budget) with an inbox item for everything but budget. */
|
|
46106
|
-
function pauseLoop(ctx, loop, reason, opts = { inbox: true }) {
|
|
46107
|
-
if (loop.state === "paused" && loop.pausedReason === reason) return loop;
|
|
46108
|
-
ctx.log?.(`factory: pausing ${loop.projectId}: ${reason}`);
|
|
46109
|
-
const next = {
|
|
46110
|
-
...loop,
|
|
46111
|
-
state: "paused",
|
|
46112
|
-
pausedReason: reason
|
|
46113
|
-
};
|
|
46114
|
-
if (opts.inbox) {
|
|
46115
|
-
raiseInbox(ctx, {
|
|
46116
|
-
id: `inbox:factory-paused:${loop.projectId}`,
|
|
46117
|
-
kind: "run_failed",
|
|
46118
|
-
priority: "high",
|
|
46119
|
-
projectId: loop.projectId,
|
|
46120
|
-
title: "Factory loop paused",
|
|
46121
|
-
body: `${reason}. Resume it from the factory panel once you have looked.`
|
|
46122
|
-
});
|
|
46123
|
-
factoryNotice(ctx, {
|
|
46124
|
-
id: `factory.paused:${loop.projectId}:${ctx.now().toISOString()}`,
|
|
46125
|
-
projectId: loop.projectId,
|
|
46126
|
-
target: "pm",
|
|
46127
|
-
event: "paused",
|
|
46128
|
-
title: "Factory loop paused",
|
|
46129
|
-
text: `${reason}. No new work starts until the user resumes the loop.`,
|
|
46130
|
-
itemIds: opts.items?.map((i) => i.id),
|
|
46131
|
-
wake: false
|
|
46132
|
-
});
|
|
46133
|
-
}
|
|
46134
|
-
return next;
|
|
46135
|
-
}
|
|
46136
|
-
/**
|
|
46137
|
-
* Spend since `day` that counts against the project's daily factory budget: the factory's own
|
|
46138
|
-
* sessions (`budgetScope: "factory"`, the default) or the whole project (`"project"`).
|
|
46139
|
-
*/
|
|
46140
|
-
function budgetSpend(ctx, projectId, day, limits) {
|
|
46141
|
-
if (limits.budgetScope === "project") return ctx.usage.spendSince(day, projectId);
|
|
46142
|
-
const ids = new Set(ctx.sessions.list(projectId).map((s) => s.id));
|
|
46143
|
-
return ctx.usage.spendSince(day, projectId, ids);
|
|
46144
|
-
}
|
|
46145
46530
|
// ../server/src/services/factory/globs.ts
|
|
46146
46531
|
/**
|
|
46147
46532
|
* Small glob matcher for protected paths (§10.1): `**` matches any number of path segments,
|
|
@@ -46388,112 +46773,6 @@ async function runAudit(ctx, command, cwd) {
|
|
|
46388
46773
|
};
|
|
46389
46774
|
}
|
|
46390
46775
|
}
|
|
46391
|
-
// ../server/src/services/factory/prompts.ts
|
|
46392
|
-
/**
|
|
46393
|
-
* Prompts of the factory loop (§10.1): improvement finders, builders, independent reviewers,
|
|
46394
|
-
* the PM's triage instructions and the follow-ups the loop sends. The server enforces limits;
|
|
46395
|
-
* these prompts only carry the judgement work.
|
|
46396
|
-
*/
|
|
46397
|
-
const AREA_BRIEF = {
|
|
46398
|
-
features: "feature ideas that move the project towards its goals and serve its target users (small, shippable increments)",
|
|
46399
|
-
ux: "user-experience problems: confusing flows, missing feedback, accessibility gaps, rough edges",
|
|
46400
|
-
performance: "performance problems: slow paths, wasteful work, bundle size, blocking I/O",
|
|
46401
|
-
tech_debt: "technical debt that slows work down: duplication, dead code, unclear modules, outdated patterns",
|
|
46402
|
-
test_gaps: "behaviour that is not covered by tests, or tests that are flaky or misleading",
|
|
46403
|
-
security: "security weaknesses: unsafe input handling, secrets, injection, outdated vulnerable dependencies"
|
|
46404
|
-
};
|
|
46405
|
-
function visionBlock(vision) {
|
|
46406
|
-
return [
|
|
46407
|
-
"<vision>",
|
|
46408
|
-
vision.trim(),
|
|
46409
|
-
"</vision>"
|
|
46410
|
-
].join("\n");
|
|
46411
|
-
}
|
|
46412
|
-
function criteriaList(item) {
|
|
46413
|
-
return item.acceptanceCriteria.map((c, i) => `${i + 1}. ${c}`).join("\n");
|
|
46414
|
-
}
|
|
46415
|
-
/** An improvement finder for one area. */
|
|
46416
|
-
function discoveryPrompt(opts) {
|
|
46417
|
-
return [
|
|
46418
|
-
`You are an improvement finder for this repository's factory loop. Area: **${opts.area}** — ${AREA_BRIEF[opts.area]}.`,
|
|
46419
|
-
"",
|
|
46420
|
-
"Read the code (do not modify any files, do not commit, do not open pull requests). Find at most 3 concrete, independently shippable improvements in this area that are grounded in the vision below. Each must be small enough for one pull request, must respect the out-of-scope list and must not touch protected areas.",
|
|
46421
|
-
"",
|
|
46422
|
-
visionBlock(opts.vision),
|
|
46423
|
-
"",
|
|
46424
|
-
opts.known.length > 0 ? `Already known (do not repeat): ${opts.known.map((t) => `"${t}"`).join(", ")}.` : "",
|
|
46425
|
-
"When done, call `factory_report_candidates` once with your candidates (title, summary of why it matters, testable acceptance criteria, paths it would touch). If you found nothing worth doing, call it with an empty list. Then call `finish` with a one-line summary."
|
|
46426
|
-
].filter((l) => l !== "").join("\n");
|
|
46427
|
-
}
|
|
46428
|
-
/** The builder of one accepted item. */
|
|
46429
|
-
function builderPrompt(item, vision) {
|
|
46430
|
-
return [
|
|
46431
|
-
`Implement ${item.issue ? `GitHub issue #${item.issue.number} (${item.issue.url})` : "the item"}: **${item.title}**`,
|
|
46432
|
-
"",
|
|
46433
|
-
item.summary,
|
|
46434
|
-
"",
|
|
46435
|
-
"Acceptance criteria:",
|
|
46436
|
-
criteriaList(item),
|
|
46437
|
-
"",
|
|
46438
|
-
visionBlock(vision),
|
|
46439
|
-
"",
|
|
46440
|
-
"How to work:",
|
|
46441
|
-
"- Work only in this worktree. Keep the change small and focused on the criteria; stay out of protected areas and never deploy or release anything.",
|
|
46442
|
-
"- Add or update tests that prove every criterion, and run the project's tests and linters until they pass.",
|
|
46443
|
-
"- Commit, then open the pull request with `create_pull_request` (it links the issue with `Fixes #N`).",
|
|
46444
|
-
"- If you changed anything visible in a UI, start the dev server, take browser screenshots of the change and attach them with `attach_evidence`.",
|
|
46445
|
-
"- Call `finish` with the outcome, a summary, the tests you ran and the changed files.",
|
|
46446
|
-
"An independent reviewer on another provider checks your pull request against the criteria and the vision before CodeDen merges it automatically."
|
|
46447
|
-
].join("\n");
|
|
46448
|
-
}
|
|
46449
|
-
/** The independent reviewer of one pull request. */
|
|
46450
|
-
function reviewerPrompt(item, vision, note, localBase) {
|
|
46451
|
-
const pr = item.pr ? `#${item.pr.number} (${item.pr.url})` : "the pull request";
|
|
46452
|
-
const read = localBase ? `You are in the author's worktree, on the PR branch. Read the change locally and offline: \`git diff ${localBase}...HEAD\`, \`git log --oneline ${localBase}..HEAD\`, and the code around it; run the tests if useful. Do not use the network (no \`gh\`, no fetch). Do not modify files, commit or push.` : `Read the change (for example \`gh pr diff ${item.pr?.number ?? ""}\` and \`gh pr view ${item.pr?.number ?? ""}\`), and the code around it. Do not modify files, commit, push or comment on GitHub.`;
|
|
46453
|
-
return [
|
|
46454
|
-
`You are the independent reviewer of pull request ${pr}: **${item.title}**. Another agent wrote it; CodeDen merges it automatically only if you approve and every other merge check passes.`,
|
|
46455
|
-
note ? `Note: ${note}` : "",
|
|
46456
|
-
"",
|
|
46457
|
-
"Acceptance criteria:",
|
|
46458
|
-
criteriaList(item),
|
|
46459
|
-
"",
|
|
46460
|
-
visionBlock(vision),
|
|
46461
|
-
"",
|
|
46462
|
-
read,
|
|
46463
|
-
"Judge: does it meet every criterion? Does it fit the vision and stay out of its out-of-scope list and protected areas? Are there tests that prove the behaviour (or are none needed)? Any security problems (injection, secrets, unsafe input, risky dependencies)?",
|
|
46464
|
-
"",
|
|
46465
|
-
"Then call `factory_submit_review` exactly once with your verdict: `approve` only when everything holds, `request_changes` for fixable problems, `reject` when the change should not exist. Be specific in the summary."
|
|
46466
|
-
].filter((l) => l !== "").join("\n");
|
|
46467
|
-
}
|
|
46468
|
-
/** Message to the builder after failed merge checks. */
|
|
46469
|
-
function fixPrompt(item, failed) {
|
|
46470
|
-
return [
|
|
46471
|
-
`The merge checks for your pull request${item.pr ? ` #${item.pr.number}` : ""} failed:`,
|
|
46472
|
-
...failed.map((c) => `- ${c.name}: ${c.detail ?? "failed"}`),
|
|
46473
|
-
item.review && item.review.verdict !== "approve" ? `Reviewer: ${item.review.summary}` : "",
|
|
46474
|
-
"",
|
|
46475
|
-
"Fix these in the same branch, run the tests, commit and push (call `create_pull_request` again to update the PR), then call `finish`."
|
|
46476
|
-
].filter((l) => l !== "").join("\n");
|
|
46477
|
-
}
|
|
46478
|
-
/** Ask a reviewer to look again after fixes. */
|
|
46479
|
-
function reReviewPrompt(item) {
|
|
46480
|
-
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.`;
|
|
46481
|
-
}
|
|
46482
|
-
const BUILDER_NUDGE = "You ended your turn without an open pull request. If the work is done, commit it and call `create_pull_request`; if you are blocked, call `finish` with outcome `blocked` and explain why.";
|
|
46483
|
-
const REVIEWER_NUDGE = "You ended your turn without a verdict. Call `factory_submit_review` now with `approve`, `request_changes` or `reject`.";
|
|
46484
|
-
/** An agent writes the revert when the API cannot (main moved on after the merge). */
|
|
46485
|
-
function revertPrompt(opts) {
|
|
46486
|
-
return [`CI on the default branch failed after pull request #${opts.prNumber} ("${opts.title}") was merged as ${opts.sha}.`, `Revert that commit in this worktree (\`git revert --no-edit ${opts.sha}\`), resolve conflicts conservatively, run the tests, and open a pull request titled \`Revert "${opts.title}"\` with \`create_pull_request\`. Do nothing else. Then call \`finish\`.`].join("\n");
|
|
46487
|
-
}
|
|
46488
|
-
/** The PM's structured triage instructions for new candidates. */
|
|
46489
|
-
function triageInstructions(items) {
|
|
46490
|
-
return [
|
|
46491
|
-
`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:`,
|
|
46492
|
-
...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)}` : ""}`),
|
|
46493
|
-
"",
|
|
46494
|
-
"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."
|
|
46495
|
-
].join("\n");
|
|
46496
|
-
}
|
|
46497
46776
|
// ../server/src/services/factory/progress.ts
|
|
46498
46777
|
const BUSY$2 = /* @__PURE__ */ new Set([
|
|
46499
46778
|
"created",
|
|
@@ -47367,7 +47646,7 @@ function requestTriage(ctx, project) {
|
|
|
47367
47646
|
target: "pm",
|
|
47368
47647
|
event: "triage_needed",
|
|
47369
47648
|
title: `Factory: ${fresh.length} candidate${fresh.length === 1 ? "" : "s"} to triage`,
|
|
47370
|
-
text: triageInstructions(fresh),
|
|
47649
|
+
text: triageInstructions(fresh, project.autonomyMode === "dark_factory"),
|
|
47371
47650
|
itemIds: fresh.map((i) => i.id)
|
|
47372
47651
|
});
|
|
47373
47652
|
for (const item of fresh) ctx.store.updateItem(item.id, (i) => ({
|
|
@@ -47555,11 +47834,17 @@ function buildDigest(opts) {
|
|
|
47555
47834
|
title: `${i.title}: ${i.reason ?? "needs a human"}`,
|
|
47556
47835
|
inboxItemId: null,
|
|
47557
47836
|
itemId: i.id
|
|
47558
|
-
})), ...opts.inbox.filter((x) => x.status === "open" && !x.id.startsWith("inbox:factory-digest:")).map((x) => ({
|
|
47837
|
+
})), ...opts.inbox.filter((x) => x.status === "open" && !x.id.startsWith("inbox:factory-digest:") && !(x.id.startsWith("inbox:factory-decision:") && x.priority !== "high")).map((x) => ({
|
|
47559
47838
|
title: x.title,
|
|
47560
47839
|
inboxItemId: x.id,
|
|
47561
47840
|
itemId: null
|
|
47562
47841
|
}))];
|
|
47842
|
+
const decisions = opts.inbox.filter((x) => x.id.startsWith("inbox:factory-decision:") && dayOf(x.createdAt) === opts.day).sort((a, b) => a.createdAt.localeCompare(b.createdAt)).map((x) => ({
|
|
47843
|
+
title: x.title,
|
|
47844
|
+
decision: /\*\*Decision:\*\* (.*)/.exec(x.body ?? "")?.[1] ?? "",
|
|
47845
|
+
needsHuman: x.priority === "high",
|
|
47846
|
+
inboxItemId: x.id
|
|
47847
|
+
}));
|
|
47563
47848
|
const spend = {
|
|
47564
47849
|
usd: Math.round(opts.spend.usd * 100) / 100,
|
|
47565
47850
|
tokens: opts.spend.tokens,
|
|
@@ -47574,6 +47859,7 @@ function buildDigest(opts) {
|
|
|
47574
47859
|
rejected,
|
|
47575
47860
|
spend,
|
|
47576
47861
|
waiting,
|
|
47862
|
+
decisions,
|
|
47577
47863
|
createdAt: (opts.now ?? /* @__PURE__ */ new Date()).toISOString()
|
|
47578
47864
|
};
|
|
47579
47865
|
return {
|
|
@@ -47596,171 +47882,15 @@ function renderDigest(projectName, d, pausedReason) {
|
|
|
47596
47882
|
lines.push("", `Rejected or failed (${d.rejected.length}):`);
|
|
47597
47883
|
for (const r of d.rejected) lines.push(`- ${r.title}: ${r.reason}`);
|
|
47598
47884
|
if (d.rejected.length === 0) lines.push("- nothing");
|
|
47885
|
+
if (d.decisions.length > 0) {
|
|
47886
|
+
lines.push("", `Decided on its own (${d.decisions.length}):`);
|
|
47887
|
+
for (const x of d.decisions) lines.push(`- ${x.title}${x.decision ? ` → ${x.decision}` : ""}`);
|
|
47888
|
+
}
|
|
47599
47889
|
lines.push("", `Spend: ${budget}`, "", `Waiting on you (${d.waiting.length}):`);
|
|
47600
47890
|
for (const w of d.waiting) lines.push(`- ${w.title}`);
|
|
47601
47891
|
if (d.waiting.length === 0) lines.push("- nothing");
|
|
47602
47892
|
return lines.join("\n");
|
|
47603
47893
|
}
|
|
47604
|
-
// ../server/src/services/factory/sessions.ts
|
|
47605
|
-
/** Who starts factory sessions; `id: "factory"` marks them for limits and the kill switch. */
|
|
47606
|
-
const FACTORY_STARTER = {
|
|
47607
|
-
kind: "pm",
|
|
47608
|
-
id: "factory"
|
|
47609
|
-
};
|
|
47610
|
-
/** Statuses that hold a session slot (`maxConcurrentSessions`). */
|
|
47611
|
-
const SLOT_STATUSES = /* @__PURE__ */ new Set([
|
|
47612
|
-
"created",
|
|
47613
|
-
"queued",
|
|
47614
|
-
"starting",
|
|
47615
|
-
"running",
|
|
47616
|
-
"waiting"
|
|
47617
|
-
]);
|
|
47618
|
-
/** Statuses of sessions the kill switch stops. */
|
|
47619
|
-
const LIVE_STATUSES = /* @__PURE__ */ new Set([
|
|
47620
|
-
...SLOT_STATUSES,
|
|
47621
|
-
"idle",
|
|
47622
|
-
"interrupted"
|
|
47623
|
-
]);
|
|
47624
|
-
function isFactorySession(s) {
|
|
47625
|
-
return s.startedBy.kind === FACTORY_STARTER.kind && s.startedBy.id === FACTORY_STARTER.id;
|
|
47626
|
-
}
|
|
47627
|
-
/** Starts and stops the factory's agent sessions through the normal session service (§8.2). */
|
|
47628
|
-
var FactorySessions = class {
|
|
47629
|
-
deps;
|
|
47630
|
-
constructor(deps) {
|
|
47631
|
-
this.deps = deps;
|
|
47632
|
-
}
|
|
47633
|
-
/** Factory sessions of a project, newest first. */
|
|
47634
|
-
list(projectId) {
|
|
47635
|
-
return this.deps.repo.list({
|
|
47636
|
-
projectId,
|
|
47637
|
-
role: "agent"
|
|
47638
|
-
}).filter(isFactorySession);
|
|
47639
|
-
}
|
|
47640
|
-
/** Factory sessions holding a slot right now. */
|
|
47641
|
-
activeCount(projectId) {
|
|
47642
|
-
return this.list(projectId).filter((s) => SLOT_STATUSES.has(s.status)).length;
|
|
47643
|
-
}
|
|
47644
|
-
/** Providers the runner offers (the Fake provider only when nothing else is there). */
|
|
47645
|
-
providers() {
|
|
47646
|
-
const ids = this.deps.runner.providers().map((p) => p.id);
|
|
47647
|
-
const real = ids.filter((id) => id !== "fake");
|
|
47648
|
-
return real.length > 0 ? real : ids;
|
|
47649
|
-
}
|
|
47650
|
-
startDiscovery(projectId, area, vision, known) {
|
|
47651
|
-
return this.start({
|
|
47652
|
-
projectId,
|
|
47653
|
-
purpose: `Factory: find ${area.replace("_", " ")} improvements`,
|
|
47654
|
-
workKey: `factory:discover:${area}`,
|
|
47655
|
-
scope: { area: `factory-discover-${area}` },
|
|
47656
|
-
title: `Factory discovery: ${area}`,
|
|
47657
|
-
input: {
|
|
47658
|
-
text: discoveryPrompt({
|
|
47659
|
-
area,
|
|
47660
|
-
vision,
|
|
47661
|
-
known
|
|
47662
|
-
}),
|
|
47663
|
-
attachments: []
|
|
47664
|
-
}
|
|
47665
|
-
});
|
|
47666
|
-
}
|
|
47667
|
-
startBuilder(item, vision, provider) {
|
|
47668
|
-
return this.start({
|
|
47669
|
-
projectId: item.projectId,
|
|
47670
|
-
provider,
|
|
47671
|
-
purpose: `Factory: ${item.title}`.slice(0, 300),
|
|
47672
|
-
workKey: item.workKey,
|
|
47673
|
-
title: item.title.slice(0, 200),
|
|
47674
|
-
issue: item.issue ?? void 0,
|
|
47675
|
-
input: {
|
|
47676
|
-
text: builderPrompt(item, vision),
|
|
47677
|
-
attachments: []
|
|
47678
|
-
}
|
|
47679
|
-
});
|
|
47680
|
-
}
|
|
47681
|
-
/**
|
|
47682
|
-
* Start the reviewer offline: in the builder's worktree when there is one, otherwise (a
|
|
47683
|
-
* revert PR opened through the API) in a new worktree on the PR's head, fetched from origin
|
|
47684
|
-
* by CodeDen before the session starts.
|
|
47685
|
-
*/
|
|
47686
|
-
startReviewer(item, vision, provider, note, pr) {
|
|
47687
|
-
const builder = item.builderSessionId ? this.deps.repo.find(item.builderSessionId)?.session : void 0;
|
|
47688
|
-
const worktree = builder?.worktreePath ?? null;
|
|
47689
|
-
let workspace = { mode: "worktree" };
|
|
47690
|
-
let localBase = null;
|
|
47691
|
-
if (worktree) {
|
|
47692
|
-
workspace = {
|
|
47693
|
-
mode: "existing",
|
|
47694
|
-
path: worktree
|
|
47695
|
-
};
|
|
47696
|
-
localBase = builder?.baseBranch ?? "main";
|
|
47697
|
-
} else if (pr) {
|
|
47698
|
-
workspace = {
|
|
47699
|
-
mode: "worktree",
|
|
47700
|
-
branch: `codeden/review-${item.pr?.number ?? item.id.slice(0, 8)}-${item.id.slice(0, 4)}`,
|
|
47701
|
-
baseRef: `origin/${pr.headBranch}`,
|
|
47702
|
-
fetch: true
|
|
47703
|
-
};
|
|
47704
|
-
localBase = `origin/${pr.baseBranch}`;
|
|
47705
|
-
}
|
|
47706
|
-
return this.start({
|
|
47707
|
-
projectId: item.projectId,
|
|
47708
|
-
provider,
|
|
47709
|
-
purpose: `Factory review: ${item.title}`.slice(0, 300),
|
|
47710
|
-
workKey: `factory:review:${item.pr?.number ?? item.id}`,
|
|
47711
|
-
title: `Review: ${item.title}`.slice(0, 200),
|
|
47712
|
-
issue: item.issue ?? void 0,
|
|
47713
|
-
workspace,
|
|
47714
|
-
input: {
|
|
47715
|
-
text: reviewerPrompt(item, vision, note, localBase),
|
|
47716
|
-
attachments: []
|
|
47717
|
-
}
|
|
47718
|
-
});
|
|
47719
|
-
}
|
|
47720
|
-
startRevert(projectId, merge) {
|
|
47721
|
-
return this.start({
|
|
47722
|
-
projectId,
|
|
47723
|
-
purpose: `Factory: revert #${merge.prNumber} after a CI regression`,
|
|
47724
|
-
workKey: `factory:revert:${merge.sha.slice(0, 7)}`,
|
|
47725
|
-
title: `Revert #${merge.prNumber}`,
|
|
47726
|
-
input: {
|
|
47727
|
-
text: revertPrompt(merge),
|
|
47728
|
-
attachments: []
|
|
47729
|
-
}
|
|
47730
|
-
});
|
|
47731
|
-
}
|
|
47732
|
-
/** Send a follow-up to a session (queued behind a running turn). */
|
|
47733
|
-
async message(sessionId, text) {
|
|
47734
|
-
await this.deps.sessions.send(sessionId, {
|
|
47735
|
-
input: {
|
|
47736
|
-
text,
|
|
47737
|
-
attachments: []
|
|
47738
|
-
},
|
|
47739
|
-
whenBusy: "queue"
|
|
47740
|
-
}, "codeden");
|
|
47741
|
-
}
|
|
47742
|
-
/** Stop every live factory session of a project (kill switch). Returns the stopped ids. */
|
|
47743
|
-
async stopAll(projectId) {
|
|
47744
|
-
const stopped = [];
|
|
47745
|
-
for (const s of this.list(projectId)) {
|
|
47746
|
-
if (!LIVE_STATUSES.has(s.status)) continue;
|
|
47747
|
-
try {
|
|
47748
|
-
await this.deps.sessions.stop(s.id);
|
|
47749
|
-
stopped.push(s.id);
|
|
47750
|
-
} catch (error) {
|
|
47751
|
-
this.deps.log?.(`factory: stop ${s.id}: ${errorMessage$1(error)}`);
|
|
47752
|
-
}
|
|
47753
|
-
}
|
|
47754
|
-
return stopped;
|
|
47755
|
-
}
|
|
47756
|
-
start(req) {
|
|
47757
|
-
return this.deps.sessions.start(StartSessionRequest.parse({
|
|
47758
|
-
role: "agent",
|
|
47759
|
-
startedBy: FACTORY_STARTER,
|
|
47760
|
-
...req
|
|
47761
|
-
}));
|
|
47762
|
-
}
|
|
47763
|
-
};
|
|
47764
47894
|
// ../server/src/services/factory/status.ts
|
|
47765
47895
|
function workItem(i) {
|
|
47766
47896
|
return {
|
|
@@ -48083,6 +48213,14 @@ var FactoryService = class {
|
|
|
48083
48213
|
if (session.projectId) this.soon(session.projectId);
|
|
48084
48214
|
return res;
|
|
48085
48215
|
}
|
|
48216
|
+
/** `factory_record_decision`: a decision an unattended agent made instead of asking. */
|
|
48217
|
+
recordDecision(session, req) {
|
|
48218
|
+
return recordDecision(this.ctx, session, req);
|
|
48219
|
+
}
|
|
48220
|
+
/** True when nobody answers this session's questions (dark factory PM or factory agent). */
|
|
48221
|
+
isUnattended(sessionId) {
|
|
48222
|
+
return unattendedSession(this.ctx, sessionId) !== void 0;
|
|
48223
|
+
}
|
|
48086
48224
|
async halt(projectId) {
|
|
48087
48225
|
const stopped = await this.ctx.sessions.stopAll(projectId);
|
|
48088
48226
|
for (const item of this.data.items(projectId, ["building", "verifying"])) this.data.updateItem(item.id, (i) => i.stage === "building" && !i.pr ? {
|
|
@@ -48146,7 +48284,10 @@ var FactoryService = class {
|
|
|
48146
48284
|
return digest;
|
|
48147
48285
|
}
|
|
48148
48286
|
onEvents(events) {
|
|
48149
|
-
for (const e of events) if (e.type === "
|
|
48287
|
+
for (const e of events) if (e.type === "agent.item.started") {
|
|
48288
|
+
if (e.payload.item?.kind !== "user_question") continue;
|
|
48289
|
+
setImmediate(() => void answerProviderQuestion(this.ctx, (id, req) => this.deps.sessions.respond(id, req), e));
|
|
48290
|
+
} else if (e.type === "session.updated") {
|
|
48150
48291
|
const { session } = e.payload;
|
|
48151
48292
|
if (session.projectId && isFactorySession(session)) this.soon(session.projectId);
|
|
48152
48293
|
} else if (e.type.startsWith("pr.")) {
|
|
@@ -61226,4 +61367,4 @@ main().then((code) => process.exit(code), (error) => {
|
|
|
61226
61367
|
});
|
|
61227
61368
|
export {};
|
|
61228
61369
|
|
|
61229
|
-
//# sourceMappingURL=main-
|
|
61370
|
+
//# sourceMappingURL=main-sdPbRDgf.js.map
|