volute 0.53.0 → 0.54.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auto-upgrade-HEVZKPTF.js +46 -0
- package/dist/{backup-WWXVIMYQ.js → backup-QRNX6RK7.js} +11 -11
- package/dist/channels-CKCY64TD.js +203 -0
- package/dist/{chat-WK2BLM7B.js → chat-ZUGNL5GS.js} +9 -9
- package/dist/{chunk-UTKUO2KY.js → chunk-3LHUXY3E.js} +2 -2
- package/dist/{chunk-PIZDPY4G.js → chunk-444RR3YF.js} +4 -4
- package/dist/{chunk-VDZ4I4ZP.js → chunk-56IT2ZXF.js} +3 -3
- package/dist/{chunk-OXWUEU5V.js → chunk-5U5GLZSD.js} +2 -2
- package/dist/{chunk-LN7XJ56R.js → chunk-7XKPG66D.js} +9 -9
- package/dist/{chunk-R772ET2M.js → chunk-AAPNU7GA.js} +4 -0
- package/dist/{chunk-7ARONCZW.js → chunk-ATZF2RNJ.js} +3 -3
- package/dist/{chunk-6NLW72HP.js → chunk-CKPAVQNK.js} +1 -1
- package/dist/{chunk-RMADJFA7.js → chunk-CLCHOXLR.js} +1 -1
- package/dist/{chunk-IQ7VNWH6.js → chunk-HKGYZVWN.js} +1 -1
- package/dist/{chunk-HASADR3E.js → chunk-LGXVGIWN.js} +66 -25
- package/dist/{chunk-P2TBUKLW.js → chunk-LKUGUQUW.js} +3 -3
- package/dist/{chunk-AP2KQMMW.js → chunk-OAYY3WF3.js} +11 -3
- package/dist/{chunk-UO4MZQNO.js → chunk-OGPVJ53N.js} +14 -6
- package/dist/{chunk-M77B73VV.js → chunk-OIOW3FPT.js} +7 -2
- package/dist/chunk-OT5SXF7L.js +904 -0
- package/dist/{chunk-EDR47UE2.js → chunk-OYF25B62.js} +1 -1
- package/dist/{chunk-TAWRJ24T.js → chunk-PPSN2MLC.js} +2 -2
- package/dist/{chunk-5DBGQBQR.js → chunk-RGHF3OK5.js} +21 -10
- package/dist/{chunk-RQRO6PC7.js → chunk-VDUCLVZI.js} +305 -31
- package/dist/{chunk-23UJGVYP.js → chunk-WHL6PKYP.js} +10 -10
- package/dist/{chunk-HTB7KDRU.js → chunk-WZZCA23D.js} +5 -4
- package/dist/{chunk-UDZLPR34.js → chunk-YSRYCS3N.js} +13 -8
- package/dist/{chunk-5N32EN4O.js → chunk-Z4Q6TQWL.js} +1 -1
- package/dist/{chunk-BDHKCLWK.js → chunk-ZNHL5KQJ.js} +1 -1
- package/dist/cli.js +19 -19
- package/dist/{clock-SH4NQC2F.js → clock-X7V3C7NL.js} +3 -3
- package/dist/{cloud-sync-MJVJ33LD.js → cloud-sync-52YNJRMR.js} +11 -11
- package/dist/{daemon-restart-BIGU4OKS.js → daemon-restart-QWHN5VMU.js} +4 -4
- package/dist/daemon.js +330 -746
- package/dist/{db-IPXRJUI7.js → db-I4EH5SLF.js} +3 -1
- package/dist/{delivery-manager-FDHCZ7PU.js → delivery-manager-7R2YIQ2W.js} +11 -7
- package/dist/{delivery-notices-65TB3CBX.js → delivery-notices-RZQ6GONP.js} +3 -3
- package/dist/{delivery-router-RHANNTAC.js → delivery-router-QS4NLJR6.js} +3 -1
- package/dist/{down-QSNEMTTX.js → down-UN2EDX2Y.js} +3 -3
- package/dist/{echo-text-KU6G4Z7V.js → echo-text-73CYQRL7.js} +14 -14
- package/dist/{exec-ABD3DYA2.js → exec-Z34TTUO3.js} +1 -1
- package/dist/{extensions-VACUKFA5.js → extensions-QT65JGV6.js} +21 -11
- package/dist/{isolation-GVV2IBAU.js → isolation-UQPDO34R.js} +1 -1
- package/dist/{message-delivery-TUXM3AAE.js → message-delivery-TRKT4T2P.js} +11 -11
- package/dist/migrate-name-placeholder-C7NCNLWF.js +37 -0
- package/dist/{mind-IX5AWUST.js → mind-CT6ANXK4.js} +12 -12
- package/dist/{mind-contacts-BKUOUJPV.js → mind-contacts-PEXCDRXA.js} +8 -8
- package/dist/{mind-list-KNU34SUD.js → mind-list-7KBLPLES.js} +2 -2
- package/dist/{mind-service-WHFBUKZE.js → mind-service-7EFIJ3ZA.js} +11 -11
- package/dist/{mind-sleep-D3DEYAWG.js → mind-sleep-24HV4RCA.js} +5 -5
- package/dist/{mind-status-BPTLQ5Z6.js → mind-status-4YW4PHMO.js} +6 -3
- package/dist/{package-BN5KMU54.js → package-F6S5NHY2.js} +4 -4
- package/dist/{prompts-HNKGCAEY.js → prompts-JECJIFPQ.js} +1 -1
- package/dist/{scheduler-5X5UIQOQ.js → scheduler-73Q4MQ7Y.js} +6 -6
- package/dist/{seed-LHZ33UB4.js → seed-DRKAP3XV.js} +1 -1
- package/dist/{seed-cmd-U4RVJ3UA.js → seed-cmd-6IMTO3LG.js} +2 -2
- package/dist/{seed-create-OQU4KNCB.js → seed-create-VNLDYRFK.js} +1 -1
- package/dist/{seed-readiness-WJYYRRCK.js → seed-readiness-H5TTPAQH.js} +2 -2
- package/dist/{seed-sprout-SFUYH2RG.js → seed-sprout-PPGYGK4G.js} +8 -8
- package/dist/{send-FWZAMDBN.js → send-UXUQKO4N.js} +9 -9
- package/dist/{service-XQPMZAGI.js → service-T4P5W6RS.js} +2 -2
- package/dist/{service-install-2EWD7KXD.js → service-install-R4SNYUVO.js} +3 -3
- package/dist/{setup-RVSGCIF3.js → setup-YUAQMDVZ.js} +4 -4
- package/dist/skills/dreaming/SKILL.md +2 -2
- package/dist/skills/dreaming/references/INSTALL.md +1 -1
- package/dist/skills/tending/SKILL.md +2 -0
- package/dist/skills/volute-mind/references/integrations.md +3 -2
- package/dist/skills/volute-mind/references/routing.md +18 -5
- package/dist/{skills-QRPU6BWY.js → skills-GMQ5UIIW.js} +2 -2
- package/dist/{sleep-manager-NWUCRBZB.js → sleep-manager-KHI65FEX.js} +11 -11
- package/dist/{spirit-I5ECLGRI.js → spirit-Q5VGFWUI.js} +5 -3
- package/dist/{spirit-availability-NIUUYKU2.js → spirit-availability-G5FMWKRH.js} +12 -12
- package/dist/{sprout-A2PMEF7Z.js → sprout-PMTRL7QN.js} +1 -1
- package/dist/{src-LCSOQY6Z.js → src-3FIW7DLH.js} +241 -87
- package/dist/src-H3UCOP6B.js +516 -0
- package/dist/{src-HGNE2XG4.js → src-LTTQVN2Z.js} +1 -1
- package/dist/{status-EYG4A3OK.js → status-LCKTTIWS.js} +5 -5
- package/dist/{system-events-YTOU66QI.js → system-events-Q7IVZ5T4.js} +1 -1
- package/dist/{systems-2DIJY4RF.js → systems-HL7SGSW7.js} +6 -6
- package/dist/{turn-tracker-NNHS6MAR.js → turn-tracker-HZDPMSCC.js} +2 -2
- package/dist/{up-UGBHH743.js → up-XZDSJVMU.js} +3 -3
- package/dist/{update-CIWI2JYA.js → update-ILCPVNDD.js} +2 -2
- package/dist/{upgrade-LIE46UBB.js → upgrade-XY2J7NWZ.js} +9 -1
- package/dist/{variant-cleanup-CZDLIY3F.js → variant-cleanup-QHGGY7ND.js} +6 -6
- package/dist/{version-notify-SJVAQLZI.js → version-notify-VNVFX6UP.js} +19 -4
- package/dist/web-assets/assets/{index-b7G7ZsVk.js → index-CwtxGwe_.js} +16 -16
- package/dist/web-assets/assets/index-DhM1yYRN.css +1 -0
- package/dist/web-assets/index.html +2 -2
- package/package.json +4 -4
- package/packages/extensions/intentions/dist/ui/assets/index-3FneLCKR.css +1 -0
- package/packages/extensions/intentions/dist/ui/assets/index-CQz4aceM.js +67 -0
- package/packages/extensions/{plan → intentions}/dist/ui/index.html +3 -3
- package/packages/extensions/intentions/skills/intention-review/SKILL.md +38 -0
- package/packages/extensions/intentions/skills/intentions/SKILL.md +49 -0
- package/packages/extensions/intentions/skills/intentions/scripts/intentions-hook.sh +45 -0
- package/packages/extensions/pages/skills/commons-gardening/SKILL.md +50 -0
- package/packages/extensions/pages/skills/pages/SKILL.md +18 -13
- package/templates/_base/.init/.config/prompts.json +2 -2
- package/templates/{pi → _base}/.init/.config/routes.json +1 -0
- package/templates/_base/.init/memory/dreams/.gitkeep +0 -0
- package/templates/_base/home/.config/routes.json +1 -1
- package/templates/_base/home/VOLUTE.md +1 -1
- package/templates/_base/src/lib/context-breakdown.ts +11 -3
- package/templates/_base/src/lib/routing.ts +2 -2
- package/templates/_base/src/lib/startup.ts +4 -2
- package/templates/claude/.init/CLAUDE.md +4 -0
- package/templates/claude/src/agent.ts +28 -12
- package/templates/claude/src/lib/message-channel.ts +43 -15
- package/templates/claude/src/lib/recover.ts +69 -0
- package/templates/claude/src/lib/stream-consumer.ts +23 -5
- package/templates/claude/volute-template.json +1 -1
- package/templates/codex/.init/AGENTS.md +4 -0
- package/templates/codex/volute-template.json +1 -1
- package/templates/pi/.init/MINDS.md +4 -0
- package/templates/pi/src/lib/mechanics-doc.ts +30 -0
- package/templates/pi/src/server.ts +3 -1
- package/templates/pi/volute-template.json +1 -1
- package/dist/channels-LW5ZLMSQ.js +0 -90
- package/dist/skills/plan-coordinator/SKILL.md +0 -60
- package/dist/src-UDMJZ6MH.js +0 -426
- package/dist/web-assets/assets/index-BHSU9r-4.css +0 -1
- package/packages/extensions/plan/dist/ui/assets/index-B8s8wxyt.js +0 -67
- package/packages/extensions/plan/dist/ui/assets/index-jIkrt-vI.css +0 -1
- package/packages/extensions/plan/skills/plan/SKILL.md +0 -43
- package/packages/extensions/plan/skills/plan/scripts/plan-hook.sh +0 -37
- package/templates/claude/.init/.config/routes.json +0 -11
- package/templates/codex/.init/.config/routes.json +0 -11
- package/dist/{accept-I4GVGWA3.js → accept-RSDIVJL6.js} +3 -3
- package/dist/{bridge-KU2IKSH3.js → bridge-YK7NTRU5.js} +3 -3
- package/dist/{create-C3XZACTV.js → create-M727VUDF.js} +3 -3
- package/dist/{env-BJGM6FBA.js → env-7AZFBPWX.js} +7 -7
- package/dist/{extension-B2W52WZY.js → extension-MRYJWSMV.js} +3 -3
- package/dist/{files-CHPNEQXN.js → files-PCKBQHAY.js} +3 -3
- package/dist/{list-R2GDWOEE.js → list-UQGIW7OF.js} +3 -3
- package/dist/{login-EZ4RRDQX.js → login-4NUS72Z5.js} +3 -3
- package/dist/{login-JDOAK465.js → login-57MMYILL.js} +4 -4
- package/dist/{logout-MWSIUOQU.js → logout-BYV433X6.js} +3 -3
- package/dist/{logout-K6ZGAJY5.js → logout-YFZAJRPY.js} +3 -3
- package/dist/{mind-history-RVF6KJ7K.js → mind-history-RZMMCHMX.js} +9 -9
- package/dist/{mind-wake-DCDFFQKX.js → mind-wake-LDGPRA42.js} +3 -3
- package/dist/{read-J5RM674N.js → read-RJ5NAW5J.js} +3 -3
- package/dist/{register-53LBGSDC.js → register-WNCSOTAU.js} +3 -3
- package/dist/{reject-K2A4UK2Y.js → reject-4NFLXKRN.js} +3 -3
- package/dist/{restart-R2WFNF5K.js → restart-B4GNMVF2.js} +7 -7
- package/dist/{skill-DXLLSUHB.js → skill-QYTBUDNQ.js} +7 -7
- package/dist/{start-XDOUAWN3.js → start-BDHJVOVX.js} +6 -6
- package/dist/{stop-O62RTEMD.js → stop-45QYSGYS.js} +7 -7
|
@@ -0,0 +1,904 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
isTemplateStale
|
|
4
|
+
} from "./chunk-CV6BNGFK.js";
|
|
5
|
+
import {
|
|
6
|
+
computeTemplateHash
|
|
7
|
+
} from "./chunk-HTRSSOHY.js";
|
|
8
|
+
import {
|
|
9
|
+
cleanupVariant
|
|
10
|
+
} from "./chunk-OGPVJ53N.js";
|
|
11
|
+
import {
|
|
12
|
+
getSleepManagerIfReady,
|
|
13
|
+
readSystemsConfig
|
|
14
|
+
} from "./chunk-LGXVGIWN.js";
|
|
15
|
+
import {
|
|
16
|
+
applyTemplateHomeFiles,
|
|
17
|
+
composeTemplate,
|
|
18
|
+
copyTemplateToDir,
|
|
19
|
+
findTemplatesRoot
|
|
20
|
+
} from "./chunk-X67BDDZR.js";
|
|
21
|
+
import {
|
|
22
|
+
getMindManager
|
|
23
|
+
} from "./chunk-WHL6PKYP.js";
|
|
24
|
+
import {
|
|
25
|
+
migrateSkillsToTemplate
|
|
26
|
+
} from "./chunk-PPSN2MLC.js";
|
|
27
|
+
import {
|
|
28
|
+
deliverEvent
|
|
29
|
+
} from "./chunk-HKGYZVWN.js";
|
|
30
|
+
import {
|
|
31
|
+
readVoluteConfig
|
|
32
|
+
} from "./chunk-WCFSBH33.js";
|
|
33
|
+
import {
|
|
34
|
+
chownMindDir,
|
|
35
|
+
exec,
|
|
36
|
+
gitExec,
|
|
37
|
+
isIsolationEnabled,
|
|
38
|
+
wrapForIsolation
|
|
39
|
+
} from "./chunk-OIOW3FPT.js";
|
|
40
|
+
import {
|
|
41
|
+
logger_default
|
|
42
|
+
} from "./chunk-QUSMYGNQ.js";
|
|
43
|
+
import {
|
|
44
|
+
findMind,
|
|
45
|
+
mindDir,
|
|
46
|
+
readRegistry,
|
|
47
|
+
setMindTemplate,
|
|
48
|
+
setMindTemplateHash
|
|
49
|
+
} from "./chunk-H6X6CKDN.js";
|
|
50
|
+
|
|
51
|
+
// packages/daemon/src/lib/mind/upgrade.ts
|
|
52
|
+
import { existsSync as existsSync2, mkdirSync, readdirSync, readFileSync, rmSync } from "fs";
|
|
53
|
+
import { resolve as resolve3 } from "path";
|
|
54
|
+
|
|
55
|
+
// packages/daemon/src/lib/mind/npm-install.ts
|
|
56
|
+
import { existsSync } from "fs";
|
|
57
|
+
import { resolve } from "path";
|
|
58
|
+
var NPM_INSTALL_ARGS = [
|
|
59
|
+
"install",
|
|
60
|
+
"--no-audit",
|
|
61
|
+
"--no-fund",
|
|
62
|
+
"--prefer-offline",
|
|
63
|
+
"--loglevel=error"
|
|
64
|
+
];
|
|
65
|
+
function lowPriorityArgv(cmd, args, ionice) {
|
|
66
|
+
return ionice ? ["ionice", ["-c2", "-n7", "nice", "-n19", cmd, ...args]] : ["nice", ["-n19", cmd, ...args]];
|
|
67
|
+
}
|
|
68
|
+
function hasIonice() {
|
|
69
|
+
return process.platform === "linux" && (existsSync("/usr/bin/ionice") || existsSync("/bin/ionice"));
|
|
70
|
+
}
|
|
71
|
+
async function npmInstallAsMind(cwd, mindName) {
|
|
72
|
+
const [cmd, args] = lowPriorityArgv("npm", NPM_INSTALL_ARGS, hasIonice());
|
|
73
|
+
if (isIsolationEnabled()) {
|
|
74
|
+
const [wrappedCmd, wrappedArgs] = await wrapForIsolation(cmd, args, mindName);
|
|
75
|
+
await exec(wrappedCmd, wrappedArgs, {
|
|
76
|
+
cwd,
|
|
77
|
+
env: { ...process.env, HOME: resolve(cwd, "home") }
|
|
78
|
+
});
|
|
79
|
+
} else {
|
|
80
|
+
await exec(cmd, args, { cwd });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function depsChangedSince(dir, fromRef) {
|
|
84
|
+
try {
|
|
85
|
+
await gitExec(["diff", "--quiet", fromRef, "HEAD", "--", "package.json", "package-lock.json"], {
|
|
86
|
+
cwd: dir
|
|
87
|
+
});
|
|
88
|
+
return false;
|
|
89
|
+
} catch {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async function npmInstallNeeded(dir, preMergeRef) {
|
|
94
|
+
if (!existsSync(resolve(dir, "node_modules"))) return true;
|
|
95
|
+
return depsChangedSince(dir, preMergeRef);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// packages/daemon/src/lib/mind/variants.ts
|
|
99
|
+
import { statSync } from "fs";
|
|
100
|
+
import { resolve as resolve2 } from "path";
|
|
101
|
+
var SAFE_BRANCH_RE = /^[a-zA-Z0-9._\-/]+$/;
|
|
102
|
+
function validateBranchName(branch) {
|
|
103
|
+
if (!SAFE_BRANCH_RE.test(branch)) {
|
|
104
|
+
return `Invalid branch name: ${branch}. Only alphanumeric, '.', '_', '-', '/' allowed.`;
|
|
105
|
+
}
|
|
106
|
+
if (branch.includes("..")) {
|
|
107
|
+
return `Invalid branch name: ${branch}. '..' not allowed.`;
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
var MERGE_EXCLUDED_PATHS = ["home/MEMORY.md", "home/memory/journal/"];
|
|
112
|
+
var MAX_DELTA_CHARS = 12e3;
|
|
113
|
+
var NEVER_REPORT_PREFIXES = ["home/.claude/", "home/.pi/", "home/.agents/"];
|
|
114
|
+
var MAX_LISTED_UNRESOLVED_FILES = 20;
|
|
115
|
+
async function findUnresolvedHomeFiles(cwd) {
|
|
116
|
+
const raw = await gitExec(
|
|
117
|
+
["ls-files", "--others", "--ignored", "--exclude-standard", "-z", "--", "home"],
|
|
118
|
+
{ cwd }
|
|
119
|
+
);
|
|
120
|
+
const allPaths = raw.split("\0").filter((path) => path && !NEVER_REPORT_PREFIXES.some((prefix) => path.startsWith(prefix)));
|
|
121
|
+
const files = [];
|
|
122
|
+
let totalBytes = 0;
|
|
123
|
+
for (const path of allPaths) {
|
|
124
|
+
let bytes = -1;
|
|
125
|
+
try {
|
|
126
|
+
bytes = statSync(resolve2(cwd, path)).size;
|
|
127
|
+
} catch {
|
|
128
|
+
}
|
|
129
|
+
if (bytes > 0) totalBytes += bytes;
|
|
130
|
+
if (files.length < MAX_LISTED_UNRESOLVED_FILES) files.push({ path, bytes });
|
|
131
|
+
}
|
|
132
|
+
return { files, totalCount: allPaths.length, totalBytes };
|
|
133
|
+
}
|
|
134
|
+
function formatBytes(bytes) {
|
|
135
|
+
if (bytes < 0) return "size unknown";
|
|
136
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
137
|
+
const units = ["KB", "MB", "GB", "TB"];
|
|
138
|
+
let value = bytes / 1024;
|
|
139
|
+
for (const unit of units) {
|
|
140
|
+
if (value < 1024) return `${value.toFixed(1)} ${unit}`;
|
|
141
|
+
value /= 1024;
|
|
142
|
+
}
|
|
143
|
+
return `${value.toFixed(1)} PB`;
|
|
144
|
+
}
|
|
145
|
+
function formatUnresolvedHomeFilesMessage(variantName, result) {
|
|
146
|
+
const lines = result.files.map(
|
|
147
|
+
(f) => ` - ${f.path.replace(/^home\//, "")} (${formatBytes(f.bytes)})`
|
|
148
|
+
);
|
|
149
|
+
const remaining = result.totalCount - result.files.length;
|
|
150
|
+
if (remaining > 0) lines.push(` ...and ${remaining} more`);
|
|
151
|
+
return `Cannot join variant "${variantName}": it has ${result.totalCount} untracked file${result.totalCount === 1 ? "" : "s"} under home/ that .gitignore is hiding from git \u2014 never committed, and joining would destroy them when the variant is cleaned up (total: ${formatBytes(result.totalBytes)}):
|
|
152
|
+
|
|
153
|
+
${lines.join("\n")}
|
|
154
|
+
|
|
155
|
+
Before retrying the join, resolve each file in the variant \u2014 one of:
|
|
156
|
+
1. Track it: git add -f -- <path> (then commit it, or let auto-commit pick it up next turn)
|
|
157
|
+
2. Copy it out of home/ to somewhere safe, then delete it
|
|
158
|
+
3. Delete it if it's disposable
|
|
159
|
+
|
|
160
|
+
Once resolved, retry the join. To join anyway and discard everything listed above, pass discardUnresolved: true.`;
|
|
161
|
+
}
|
|
162
|
+
var RESTORE_PATH_CHUNK = 500;
|
|
163
|
+
var RESTORE_DIFF_MAX_BUFFER = 64 * 1024 * 1024;
|
|
164
|
+
async function restoreMergeDeletedHomeFiles(cwd, sourceCommit) {
|
|
165
|
+
const raw = await gitExec(
|
|
166
|
+
[
|
|
167
|
+
"diff",
|
|
168
|
+
"--name-only",
|
|
169
|
+
"--diff-filter=D",
|
|
170
|
+
"--no-renames",
|
|
171
|
+
"-z",
|
|
172
|
+
sourceCommit,
|
|
173
|
+
"HEAD",
|
|
174
|
+
"--",
|
|
175
|
+
"home/"
|
|
176
|
+
],
|
|
177
|
+
{ cwd, maxBuffer: RESTORE_DIFF_MAX_BUFFER }
|
|
178
|
+
);
|
|
179
|
+
const deleted = raw.split("\0").filter(Boolean);
|
|
180
|
+
if (deleted.length === 0) return [];
|
|
181
|
+
let paths = [];
|
|
182
|
+
try {
|
|
183
|
+
const ignoredRaw = await gitExec(["check-ignore", "--stdin", "-z"], {
|
|
184
|
+
cwd,
|
|
185
|
+
maxBuffer: RESTORE_DIFF_MAX_BUFFER,
|
|
186
|
+
stdin: deleted.join("\0")
|
|
187
|
+
});
|
|
188
|
+
paths = ignoredRaw.split("\0").filter(Boolean);
|
|
189
|
+
} catch (err) {
|
|
190
|
+
if (err.code !== 1) throw err;
|
|
191
|
+
}
|
|
192
|
+
if (paths.length === 0) return [];
|
|
193
|
+
for (let i = 0; i < paths.length; i += RESTORE_PATH_CHUNK) {
|
|
194
|
+
const pathspecs = paths.slice(i, i + RESTORE_PATH_CHUNK).map((p) => `:(literal)${p}`);
|
|
195
|
+
await gitExec(["restore", `--source=${sourceCommit}`, "--worktree", "--", ...pathspecs], {
|
|
196
|
+
cwd
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
return paths;
|
|
200
|
+
}
|
|
201
|
+
async function mergeVariantExcludingMemory(projectRoot, variantBranch) {
|
|
202
|
+
const opts = { cwd: projectRoot };
|
|
203
|
+
let memoryDelta = "";
|
|
204
|
+
try {
|
|
205
|
+
const base = (await gitExec(["merge-base", "HEAD", variantBranch], opts)).trim();
|
|
206
|
+
memoryDelta = (await gitExec(["diff", `${base}..${variantBranch}`, "--", ...MERGE_EXCLUDED_PATHS], opts)).trim();
|
|
207
|
+
} catch (err) {
|
|
208
|
+
logger_default.error(
|
|
209
|
+
`variant join: failed to compute memory delta for ${variantBranch}`,
|
|
210
|
+
logger_default.errorData(err)
|
|
211
|
+
);
|
|
212
|
+
memoryDelta = `(journal delta unavailable \u2014 ${err instanceof Error ? err.message : String(err)})`;
|
|
213
|
+
}
|
|
214
|
+
if (memoryDelta.length > MAX_DELTA_CHARS) {
|
|
215
|
+
memoryDelta = `${memoryDelta.slice(0, MAX_DELTA_CHARS)}
|
|
216
|
+
\u2026 (delta truncated)`;
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
await gitExec(["merge", "--no-ff", "--no-commit", variantBranch], opts).catch(() => {
|
|
220
|
+
});
|
|
221
|
+
await gitExec(["reset", "-q", "HEAD", "--", ...MERGE_EXCLUDED_PATHS], opts);
|
|
222
|
+
for (const path of MERGE_EXCLUDED_PATHS) {
|
|
223
|
+
await gitExec(["checkout", "--", path], opts).catch(() => {
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
await gitExec(["clean", "-fdq", "--", ...MERGE_EXCLUDED_PATHS], opts);
|
|
227
|
+
await assertExcludedPathsRestored(opts);
|
|
228
|
+
if (await mergeInProgress(opts)) {
|
|
229
|
+
await gitExec(["commit", "--no-edit"], opts);
|
|
230
|
+
}
|
|
231
|
+
} catch (err) {
|
|
232
|
+
const conflicts = await listConflicts(opts);
|
|
233
|
+
const abortFailed = await gitExec(["merge", "--abort"], opts).then(
|
|
234
|
+
() => false,
|
|
235
|
+
() => true
|
|
236
|
+
);
|
|
237
|
+
throw new VariantMergeError(
|
|
238
|
+
`variant join aborted: ${err instanceof Error ? err.message : String(err)}`,
|
|
239
|
+
conflicts,
|
|
240
|
+
abortFailed
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
return memoryDelta;
|
|
244
|
+
}
|
|
245
|
+
var VariantMergeError = class extends Error {
|
|
246
|
+
constructor(message, conflicts, abortFailed = false) {
|
|
247
|
+
super(message);
|
|
248
|
+
this.conflicts = conflicts;
|
|
249
|
+
this.abortFailed = abortFailed;
|
|
250
|
+
this.name = "VariantMergeError";
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
async function mergeInProgress(opts) {
|
|
254
|
+
return gitExec(["rev-parse", "-q", "--verify", "MERGE_HEAD"], opts).then(
|
|
255
|
+
() => true,
|
|
256
|
+
() => false
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
async function listConflicts(opts) {
|
|
260
|
+
try {
|
|
261
|
+
const out = (await gitExec(["diff", "--name-only", "--diff-filter=U"], opts)).trim();
|
|
262
|
+
return out ? out.split("\n") : [];
|
|
263
|
+
} catch {
|
|
264
|
+
return [];
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
async function assertExcludedPathsRestored(opts) {
|
|
268
|
+
await gitExec(["diff", "--cached", "--quiet", "HEAD", "--", ...MERGE_EXCLUDED_PATHS], opts);
|
|
269
|
+
await gitExec(["diff", "--quiet", "HEAD", "--", ...MERGE_EXCLUDED_PATHS], opts);
|
|
270
|
+
const untracked = (await gitExec(
|
|
271
|
+
["ls-files", "--others", "--exclude-standard", "--", ...MERGE_EXCLUDED_PATHS],
|
|
272
|
+
opts
|
|
273
|
+
)).trim();
|
|
274
|
+
if (untracked) {
|
|
275
|
+
throw new Error(
|
|
276
|
+
`untracked memory files remain after restore: ${untracked.replace(/\n/g, ", ")}`
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// packages/daemon/src/lib/mind/upgrade.ts
|
|
282
|
+
var UpgradeInProgressError = class extends Error {
|
|
283
|
+
worktreeDir;
|
|
284
|
+
constructor(worktreeDir) {
|
|
285
|
+
super("Upgrade variant already exists. Use continue or delete it first.");
|
|
286
|
+
this.name = "UpgradeInProgressError";
|
|
287
|
+
this.worktreeDir = worktreeDir;
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
var TEMPLATE_BRANCH = "volute/template";
|
|
291
|
+
var UPGRADE_BRANCH = "upgrade";
|
|
292
|
+
var FINAL_MERGE_CONFLICTS_MESSAGE = "Merge conflicts detected at the final merge step. The mind's directory has been restored to its pre-merge state; the upgrade worktree has been left in place for manual resolution.";
|
|
293
|
+
var upgradeLocks = /* @__PURE__ */ new Map();
|
|
294
|
+
async function withUpgradeLock(mindName, fn) {
|
|
295
|
+
const prior = upgradeLocks.get(mindName) ?? Promise.resolve();
|
|
296
|
+
const run = prior.catch(() => {
|
|
297
|
+
}).then(fn);
|
|
298
|
+
upgradeLocks.set(
|
|
299
|
+
mindName,
|
|
300
|
+
run.catch(() => {
|
|
301
|
+
})
|
|
302
|
+
);
|
|
303
|
+
return run;
|
|
304
|
+
}
|
|
305
|
+
async function configureGitIdentity(mindName, opts) {
|
|
306
|
+
const systemsConfig = readSystemsConfig();
|
|
307
|
+
const system = systemsConfig?.system ?? "local";
|
|
308
|
+
await gitExec(["config", "user.name", mindName], opts);
|
|
309
|
+
await gitExec(["config", "user.email", `${mindName}.${system}@volute.systems`], opts);
|
|
310
|
+
}
|
|
311
|
+
async function updateTemplateBranch(projectRoot, template, mindName) {
|
|
312
|
+
const tempWorktree = resolve3(projectRoot, ".variants", "_template_update");
|
|
313
|
+
let branchExists = false;
|
|
314
|
+
try {
|
|
315
|
+
await gitExec(["rev-parse", "--verify", TEMPLATE_BRANCH], { cwd: projectRoot });
|
|
316
|
+
branchExists = true;
|
|
317
|
+
} catch {
|
|
318
|
+
}
|
|
319
|
+
try {
|
|
320
|
+
await gitExec(["worktree", "remove", "--force", tempWorktree], { cwd: projectRoot });
|
|
321
|
+
} catch {
|
|
322
|
+
}
|
|
323
|
+
if (existsSync2(tempWorktree)) {
|
|
324
|
+
rmSync(tempWorktree, { recursive: true, force: true });
|
|
325
|
+
}
|
|
326
|
+
const templatesRoot = findTemplatesRoot();
|
|
327
|
+
const { composedDir, manifest } = composeTemplate(templatesRoot, template);
|
|
328
|
+
try {
|
|
329
|
+
if (branchExists) {
|
|
330
|
+
await gitExec(["worktree", "add", tempWorktree, TEMPLATE_BRANCH], {
|
|
331
|
+
cwd: projectRoot
|
|
332
|
+
});
|
|
333
|
+
} else {
|
|
334
|
+
await gitExec(["worktree", "add", "--detach", tempWorktree], { cwd: projectRoot });
|
|
335
|
+
await gitExec(["checkout", "--orphan", TEMPLATE_BRANCH], { cwd: tempWorktree });
|
|
336
|
+
await gitExec(["rm", "-rf", "--cached", "."], { cwd: tempWorktree });
|
|
337
|
+
await gitExec(["clean", "-fd"], { cwd: tempWorktree });
|
|
338
|
+
}
|
|
339
|
+
if (branchExists) {
|
|
340
|
+
await gitExec(["rm", "-rf", "."], { cwd: tempWorktree }).catch(() => {
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
copyTemplateToDir(composedDir, tempWorktree, mindName, manifest);
|
|
344
|
+
const initDir = resolve3(tempWorktree, ".init");
|
|
345
|
+
if (existsSync2(initDir)) {
|
|
346
|
+
rmSync(initDir, { recursive: true, force: true });
|
|
347
|
+
}
|
|
348
|
+
const homeDir = resolve3(tempWorktree, "home");
|
|
349
|
+
if (existsSync2(homeDir)) {
|
|
350
|
+
for (const entry of readdirSync(homeDir)) {
|
|
351
|
+
if (entry !== "VOLUTE.md") {
|
|
352
|
+
rmSync(resolve3(homeDir, entry), { recursive: true, force: true });
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
await gitExec(["add", "-A"], { cwd: tempWorktree });
|
|
357
|
+
try {
|
|
358
|
+
await gitExec(["diff", "--cached", "--quiet"], { cwd: tempWorktree });
|
|
359
|
+
} catch {
|
|
360
|
+
await gitExec(["commit", "-m", "template update"], { cwd: tempWorktree });
|
|
361
|
+
}
|
|
362
|
+
} finally {
|
|
363
|
+
try {
|
|
364
|
+
await gitExec(["worktree", "remove", "--force", tempWorktree], { cwd: projectRoot });
|
|
365
|
+
} catch {
|
|
366
|
+
}
|
|
367
|
+
if (existsSync2(tempWorktree)) {
|
|
368
|
+
rmSync(tempWorktree, { recursive: true, force: true });
|
|
369
|
+
}
|
|
370
|
+
rmSync(composedDir, { recursive: true, force: true });
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
async function mergeTemplateBranch(worktreeDir) {
|
|
374
|
+
try {
|
|
375
|
+
await gitExec(
|
|
376
|
+
["merge", TEMPLATE_BRANCH, "--allow-unrelated-histories", "-m", "merge template update"],
|
|
377
|
+
{ cwd: worktreeDir }
|
|
378
|
+
);
|
|
379
|
+
return false;
|
|
380
|
+
} catch (e) {
|
|
381
|
+
try {
|
|
382
|
+
const status = await gitExec(["status", "--porcelain"], { cwd: worktreeDir });
|
|
383
|
+
const hasConflictMarkers = status.split("\n").some((line) => line.startsWith("UU") || line.startsWith("AA"));
|
|
384
|
+
if (hasConflictMarkers) return true;
|
|
385
|
+
} catch {
|
|
386
|
+
}
|
|
387
|
+
throw e;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
async function mergeWithUntrackResolution(dir, branch) {
|
|
391
|
+
try {
|
|
392
|
+
await gitExec(["merge", branch], { cwd: dir });
|
|
393
|
+
return { merged: true };
|
|
394
|
+
} catch {
|
|
395
|
+
}
|
|
396
|
+
try {
|
|
397
|
+
const unmergedRaw = await gitExec(["diff", "--name-only", "--diff-filter=U"], { cwd: dir });
|
|
398
|
+
const unmerged = unmergedRaw.split("\n").filter(Boolean);
|
|
399
|
+
if (unmerged.length === 0) {
|
|
400
|
+
throw new Error(`git merge ${branch} failed without conflicts`);
|
|
401
|
+
}
|
|
402
|
+
const resolvable = [];
|
|
403
|
+
if (!unmerged.includes(".gitignore")) {
|
|
404
|
+
for (const file of unmerged) {
|
|
405
|
+
try {
|
|
406
|
+
await gitExec(["check-ignore", "--no-index", "-q", "--", file], { cwd: dir });
|
|
407
|
+
resolvable.push(file);
|
|
408
|
+
} catch {
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
const remaining = unmerged.filter((f) => !resolvable.includes(f));
|
|
413
|
+
if (remaining.length > 0) {
|
|
414
|
+
await gitExec(["merge", "--abort"], { cwd: dir });
|
|
415
|
+
return { merged: false, files: unmerged };
|
|
416
|
+
}
|
|
417
|
+
for (const file of resolvable) {
|
|
418
|
+
await gitExec(["checkout", "--ours", "--", file], { cwd: dir });
|
|
419
|
+
await gitExec(["rm", "--cached", "--", file], { cwd: dir });
|
|
420
|
+
}
|
|
421
|
+
await gitExec(["commit", "-m", "merge template update (auto-untrack ignored files)"], {
|
|
422
|
+
cwd: dir
|
|
423
|
+
});
|
|
424
|
+
return { merged: true };
|
|
425
|
+
} catch (err) {
|
|
426
|
+
await gitExec(["merge", "--abort"], { cwd: dir }).catch(() => {
|
|
427
|
+
});
|
|
428
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
async function mergeUpgradeAndRestart(mindName, dir, worktreeDir, upgradeVariantName2, upgradeBranch, template, oldTemplate, restart) {
|
|
432
|
+
const templateChanged = template !== oldTemplate;
|
|
433
|
+
const mainStatus = (await gitExec(["status", "--porcelain"], { cwd: dir })).trim();
|
|
434
|
+
if (mainStatus) {
|
|
435
|
+
await gitExec(["add", "-A"], { cwd: dir });
|
|
436
|
+
await gitExec(["commit", "-m", "Auto-commit before upgrade merge"], { cwd: dir });
|
|
437
|
+
}
|
|
438
|
+
const preMergeHead = (await gitExec(["rev-parse", "HEAD"], { cwd: dir })).trim();
|
|
439
|
+
const mergeResult = await mergeWithUntrackResolution(dir, upgradeBranch);
|
|
440
|
+
if (!mergeResult.merged) {
|
|
441
|
+
return { ok: false, conflicts: true, files: mergeResult.files };
|
|
442
|
+
}
|
|
443
|
+
let restoreWarning;
|
|
444
|
+
let restored = [];
|
|
445
|
+
try {
|
|
446
|
+
restored = await restoreMergeDeletedHomeFiles(dir, preMergeHead);
|
|
447
|
+
if (restored.length > 0) {
|
|
448
|
+
logger_default.info(
|
|
449
|
+
`restored ${restored.length} home files untracked by the allowlist migration for ${mindName}`
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
} catch (err) {
|
|
453
|
+
logger_default.error(`failed to restore merge-deleted home files for ${mindName}`, logger_default.errorData(err));
|
|
454
|
+
restoreWarning = `Upgrade merged but restoring home files the allowlist migration deleted failed: ${err instanceof Error ? err.message : String(err)}. Recover them manually: list them with \`git diff --name-only --diff-filter=D --no-renames ${preMergeHead} HEAD -- home/\`, then restore each with \`git restore --source=${preMergeHead} --worktree -- <path>\`.`;
|
|
455
|
+
}
|
|
456
|
+
if (restored.length > 0) {
|
|
457
|
+
try {
|
|
458
|
+
await chownMindDir(dir, mindName);
|
|
459
|
+
} catch (err) {
|
|
460
|
+
logger_default.warn(`failed to chown restored home files for ${mindName}`, logger_default.errorData(err));
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
const withRestoreWarning = (warning) => [restoreWarning, warning].filter(Boolean).join(" ") || void 0;
|
|
464
|
+
try {
|
|
465
|
+
await cleanupVariant(upgradeVariantName2, mindName, dir, worktreeDir, {
|
|
466
|
+
branch: UPGRADE_BRANCH
|
|
467
|
+
});
|
|
468
|
+
} catch (err) {
|
|
469
|
+
logger_default.warn(`failed to clean up upgrade worktree for ${mindName}`, logger_default.errorData(err));
|
|
470
|
+
}
|
|
471
|
+
try {
|
|
472
|
+
await gitExec(["branch", "-D", upgradeBranch], { cwd: dir });
|
|
473
|
+
} catch {
|
|
474
|
+
}
|
|
475
|
+
let switchWarning;
|
|
476
|
+
if (templateChanged) {
|
|
477
|
+
try {
|
|
478
|
+
applyTemplateHomeFiles(resolve3(dir, "home"), template);
|
|
479
|
+
const migratedSkills = migrateSkillsToTemplate(dir, oldTemplate, template);
|
|
480
|
+
await gitExec(["add", "home/"], { cwd: dir });
|
|
481
|
+
try {
|
|
482
|
+
await gitExec(["diff", "--cached", "--quiet"], { cwd: dir });
|
|
483
|
+
} catch {
|
|
484
|
+
await gitExec(["commit", "-m", `swap template-owned home files for ${template}`], {
|
|
485
|
+
cwd: dir
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
await chownMindDir(dir, mindName);
|
|
489
|
+
const skillNote = migratedSkills.length > 0 ? ` Migrated skills to the ${template} skills dir: ${migratedSkills.join(", ")}.` : "";
|
|
490
|
+
switchWarning = `Switched ${oldTemplate}\u2192${template}: config reset to ${template} defaults, mechanics doc replaced, conversation starts fresh (sessions aren't portable across runtimes).${skillNote}`;
|
|
491
|
+
} catch (err) {
|
|
492
|
+
logger_default.warn(`failed to swap template home files for ${mindName}`, logger_default.errorData(err));
|
|
493
|
+
return {
|
|
494
|
+
ok: true,
|
|
495
|
+
warning: withRestoreWarning(
|
|
496
|
+
`Upgrade merged but template switch ${oldTemplate}\u2192${template} failed: ${err instanceof Error ? err.message : String(err)}. The mind is still registered as ${oldTemplate}; re-run the switch or fix home/ manually.`
|
|
497
|
+
)
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
try {
|
|
502
|
+
await setMindTemplateHash(mindName, computeTemplateHash(template));
|
|
503
|
+
await setMindTemplate(mindName, template);
|
|
504
|
+
} catch (err) {
|
|
505
|
+
logger_default.warn(`failed to update template for ${mindName}`, logger_default.errorData(err));
|
|
506
|
+
}
|
|
507
|
+
if (await npmInstallNeeded(dir, preMergeHead)) {
|
|
508
|
+
try {
|
|
509
|
+
await npmInstallAsMind(dir, mindName);
|
|
510
|
+
} catch (err) {
|
|
511
|
+
logger_default.warn(`npm install failed after upgrade merge for ${mindName}`, logger_default.errorData(err));
|
|
512
|
+
return {
|
|
513
|
+
ok: true,
|
|
514
|
+
warning: withRestoreWarning(
|
|
515
|
+
`Upgrade merged but npm install failed: ${err instanceof Error ? err.message : String(err)}. You may need to run npm install manually.`
|
|
516
|
+
)
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
} else {
|
|
520
|
+
logger_default.info(`skipping npm install for ${mindName} \u2014 dependencies unchanged by upgrade`);
|
|
521
|
+
}
|
|
522
|
+
const manager = getMindManager();
|
|
523
|
+
if (!restart) {
|
|
524
|
+
manager.setPendingContext(mindName, { type: "upgraded" });
|
|
525
|
+
return { ok: true, warning: withRestoreWarning(switchWarning) };
|
|
526
|
+
}
|
|
527
|
+
try {
|
|
528
|
+
if (manager.isRunning(mindName)) {
|
|
529
|
+
await manager.stopMind(mindName);
|
|
530
|
+
}
|
|
531
|
+
manager.setPendingContext(mindName, { type: "upgraded" });
|
|
532
|
+
await manager.startMind(mindName, { healthTimeoutMs: 12e4 });
|
|
533
|
+
} catch (e) {
|
|
534
|
+
return {
|
|
535
|
+
ok: true,
|
|
536
|
+
warning: withRestoreWarning(
|
|
537
|
+
`Upgrade merged but mind restart failed: ${e instanceof Error ? e.message : String(e)}`
|
|
538
|
+
)
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
return { ok: true, warning: withRestoreWarning(switchWarning) };
|
|
542
|
+
}
|
|
543
|
+
function upgradeVariantName(mindName) {
|
|
544
|
+
return `${mindName}-upgrade`;
|
|
545
|
+
}
|
|
546
|
+
function upgradeWorktreeDir(dir) {
|
|
547
|
+
return resolve3(dir, ".variants", UPGRADE_BRANCH);
|
|
548
|
+
}
|
|
549
|
+
function upgradeInProgress(mindName) {
|
|
550
|
+
return existsSync2(upgradeWorktreeDir(mindDir(mindName)));
|
|
551
|
+
}
|
|
552
|
+
function upgradeMidResolution(worktreeDir) {
|
|
553
|
+
try {
|
|
554
|
+
const gitDirContent = readFileSync(resolve3(worktreeDir, ".git"), "utf-8").trim();
|
|
555
|
+
const gitDir = gitDirContent.replace("gitdir: ", "");
|
|
556
|
+
return existsSync2(resolve3(gitDir, "MERGE_HEAD"));
|
|
557
|
+
} catch {
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
async function upgradeDiff(mindName, template) {
|
|
562
|
+
const entry = await findMind(mindName);
|
|
563
|
+
if (!entry) throw new Error("Mind not found");
|
|
564
|
+
const dir = mindDir(mindName);
|
|
565
|
+
const tmpl = template ?? entry.template ?? "claude";
|
|
566
|
+
await updateTemplateBranch(dir, tmpl, mindName);
|
|
567
|
+
try {
|
|
568
|
+
return await gitExec(["diff", "HEAD...volute/template"], { cwd: dir });
|
|
569
|
+
} catch {
|
|
570
|
+
return await gitExec(["diff", "HEAD", "volute/template"], { cwd: dir });
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
async function runUpgrade(mindName, opts) {
|
|
574
|
+
return withUpgradeLock(mindName, () => runUpgradeCore(mindName, opts));
|
|
575
|
+
}
|
|
576
|
+
async function runUpgradeCore(mindName, opts) {
|
|
577
|
+
const entry = await findMind(mindName);
|
|
578
|
+
if (!entry) throw new Error("Mind not found");
|
|
579
|
+
const dir = mindDir(mindName);
|
|
580
|
+
const oldTemplate = entry.template ?? "claude";
|
|
581
|
+
const template = opts?.template ?? oldTemplate;
|
|
582
|
+
const restart = opts?.restart ?? true;
|
|
583
|
+
const variantName = upgradeVariantName(mindName);
|
|
584
|
+
const worktreeDir = upgradeWorktreeDir(dir);
|
|
585
|
+
if (existsSync2(worktreeDir)) {
|
|
586
|
+
if (upgradeMidResolution(worktreeDir)) {
|
|
587
|
+
throw new UpgradeInProgressError(worktreeDir);
|
|
588
|
+
}
|
|
589
|
+
logger_default.warn(`clearing stale orphaned upgrade worktree for ${mindName}`);
|
|
590
|
+
await abortUpgradeCore(mindName);
|
|
591
|
+
}
|
|
592
|
+
if (!existsSync2(resolve3(dir, ".git"))) {
|
|
593
|
+
try {
|
|
594
|
+
const env = isIsolationEnabled() ? { ...process.env, HOME: resolve3(dir, "home") } : void 0;
|
|
595
|
+
await gitExec(["init"], { cwd: dir, mindName, env });
|
|
596
|
+
await configureGitIdentity(mindName, { cwd: dir, mindName, env });
|
|
597
|
+
await gitExec(["add", "-A"], { cwd: dir, mindName, env });
|
|
598
|
+
await gitExec(["commit", "-m", "initial commit"], { cwd: dir, mindName, env });
|
|
599
|
+
await chownMindDir(dir, mindName);
|
|
600
|
+
} catch (err) {
|
|
601
|
+
rmSync(resolve3(dir, ".git"), { recursive: true, force: true });
|
|
602
|
+
throw new Error(
|
|
603
|
+
`Git initialization failed: ${err instanceof Error ? err.message : String(err)}`
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
await gitExec(["worktree", "prune"], { cwd: dir });
|
|
608
|
+
try {
|
|
609
|
+
await gitExec(["branch", "-D", UPGRADE_BRANCH], { cwd: dir });
|
|
610
|
+
} catch {
|
|
611
|
+
}
|
|
612
|
+
await updateTemplateBranch(dir, template, mindName);
|
|
613
|
+
const parentDir = resolve3(dir, ".variants");
|
|
614
|
+
if (!existsSync2(parentDir)) {
|
|
615
|
+
mkdirSync(parentDir, { recursive: true });
|
|
616
|
+
}
|
|
617
|
+
await gitExec(["worktree", "add", "-b", UPGRADE_BRANCH, worktreeDir], { cwd: dir });
|
|
618
|
+
await gitExec(["rm", "-r", "--cached", "--ignore-unmatch", "home/"], {
|
|
619
|
+
cwd: worktreeDir
|
|
620
|
+
});
|
|
621
|
+
try {
|
|
622
|
+
await gitExec(["checkout", "HEAD", "--", "home/VOLUTE.md"], { cwd: worktreeDir });
|
|
623
|
+
await gitExec(["add", "home/VOLUTE.md"], { cwd: worktreeDir });
|
|
624
|
+
} catch (err) {
|
|
625
|
+
const msg = String(err?.message ?? err);
|
|
626
|
+
if (!msg.includes("did not match")) {
|
|
627
|
+
logger_default.warn(
|
|
628
|
+
`unexpected error restoring VOLUTE.md during upgrade for ${mindName}`,
|
|
629
|
+
logger_default.errorData(err)
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
try {
|
|
634
|
+
await gitExec(["diff", "--cached", "--quiet"], { cwd: worktreeDir });
|
|
635
|
+
} catch {
|
|
636
|
+
await gitExec(["commit", "-m", "prepare for home/ allowlist migration"], {
|
|
637
|
+
cwd: worktreeDir
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
const hasConflicts = await mergeTemplateBranch(worktreeDir);
|
|
641
|
+
if (!hasConflicts) {
|
|
642
|
+
try {
|
|
643
|
+
await gitExec(["add", "home/"], { cwd: worktreeDir });
|
|
644
|
+
} catch (err) {
|
|
645
|
+
logger_default.warn(`failed to re-add home files during upgrade for ${mindName}`, logger_default.errorData(err));
|
|
646
|
+
}
|
|
647
|
+
try {
|
|
648
|
+
await gitExec(["diff", "--cached", "--quiet"], { cwd: worktreeDir });
|
|
649
|
+
} catch {
|
|
650
|
+
await gitExec(["commit", "-m", "re-add allowlisted home files"], {
|
|
651
|
+
cwd: worktreeDir
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
await chownMindDir(dir, mindName);
|
|
656
|
+
if (hasConflicts) {
|
|
657
|
+
const filesRaw = await gitExec(["diff", "--name-only", "--diff-filter=U"], {
|
|
658
|
+
cwd: worktreeDir
|
|
659
|
+
});
|
|
660
|
+
const files = filesRaw.split("\n").map((f) => f.trim()).filter(Boolean);
|
|
661
|
+
return { status: "conflicts", worktreeDir, files };
|
|
662
|
+
}
|
|
663
|
+
try {
|
|
664
|
+
const result = await mergeUpgradeAndRestart(
|
|
665
|
+
mindName,
|
|
666
|
+
dir,
|
|
667
|
+
worktreeDir,
|
|
668
|
+
variantName,
|
|
669
|
+
UPGRADE_BRANCH,
|
|
670
|
+
template,
|
|
671
|
+
oldTemplate,
|
|
672
|
+
restart
|
|
673
|
+
);
|
|
674
|
+
if (!result.ok) {
|
|
675
|
+
return {
|
|
676
|
+
status: "conflicts",
|
|
677
|
+
worktreeDir,
|
|
678
|
+
files: result.files,
|
|
679
|
+
message: FINAL_MERGE_CONFLICTS_MESSAGE
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
return { status: "upgraded", warning: result.warning };
|
|
683
|
+
} catch (err) {
|
|
684
|
+
try {
|
|
685
|
+
await cleanupVariant(variantName, mindName, dir, worktreeDir, { branch: UPGRADE_BRANCH });
|
|
686
|
+
} catch (cleanupErr) {
|
|
687
|
+
logger_default.warn(`cleanup failed after upgrade error for ${mindName}`, logger_default.errorData(cleanupErr));
|
|
688
|
+
}
|
|
689
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
async function continueUpgrade(mindName, opts) {
|
|
693
|
+
return withUpgradeLock(mindName, () => continueUpgradeCore(mindName, opts));
|
|
694
|
+
}
|
|
695
|
+
async function continueUpgradeCore(mindName, opts) {
|
|
696
|
+
const entry = await findMind(mindName);
|
|
697
|
+
if (!entry) throw new Error("Mind not found");
|
|
698
|
+
const dir = mindDir(mindName);
|
|
699
|
+
const oldTemplate = entry.template ?? "claude";
|
|
700
|
+
const template = opts?.template ?? oldTemplate;
|
|
701
|
+
const restart = opts?.restart ?? true;
|
|
702
|
+
const variantName = upgradeVariantName(mindName);
|
|
703
|
+
const worktreeDir = upgradeWorktreeDir(dir);
|
|
704
|
+
if (!existsSync2(worktreeDir)) {
|
|
705
|
+
throw new Error("No upgrade in progress");
|
|
706
|
+
}
|
|
707
|
+
const status = await gitExec(["status", "--porcelain"], { cwd: worktreeDir });
|
|
708
|
+
const hasConflicts = status.split("\n").some((line) => line.startsWith("UU") || line.startsWith("AA"));
|
|
709
|
+
if (hasConflicts) {
|
|
710
|
+
throw new Error("Unresolved conflicts remain");
|
|
711
|
+
}
|
|
712
|
+
try {
|
|
713
|
+
await gitExec(["add", "-A"], { cwd: worktreeDir });
|
|
714
|
+
await gitExec(["commit", "-m", "merge template update"], { cwd: worktreeDir });
|
|
715
|
+
} catch (e) {
|
|
716
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
717
|
+
const stderr = e?.stderr ?? "";
|
|
718
|
+
const stdout = e?.stdout ?? "";
|
|
719
|
+
if (!msg.includes("nothing to commit") && !stderr.includes("nothing to commit") && !stdout.includes("nothing to commit"))
|
|
720
|
+
throw e;
|
|
721
|
+
}
|
|
722
|
+
try {
|
|
723
|
+
await gitExec(["add", "home/"], { cwd: worktreeDir });
|
|
724
|
+
} catch (err) {
|
|
725
|
+
logger_default.warn(`failed to re-add home files during upgrade for ${mindName}`, logger_default.errorData(err));
|
|
726
|
+
}
|
|
727
|
+
try {
|
|
728
|
+
await gitExec(["diff", "--cached", "--quiet"], { cwd: worktreeDir });
|
|
729
|
+
} catch {
|
|
730
|
+
await gitExec(["commit", "-m", "re-add allowlisted home files"], {
|
|
731
|
+
cwd: worktreeDir
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
await chownMindDir(dir, mindName);
|
|
735
|
+
const result = await mergeUpgradeAndRestart(
|
|
736
|
+
mindName,
|
|
737
|
+
dir,
|
|
738
|
+
worktreeDir,
|
|
739
|
+
variantName,
|
|
740
|
+
UPGRADE_BRANCH,
|
|
741
|
+
template,
|
|
742
|
+
oldTemplate,
|
|
743
|
+
restart
|
|
744
|
+
);
|
|
745
|
+
if (!result.ok) {
|
|
746
|
+
return {
|
|
747
|
+
status: "conflicts",
|
|
748
|
+
worktreeDir,
|
|
749
|
+
files: result.files,
|
|
750
|
+
message: FINAL_MERGE_CONFLICTS_MESSAGE
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
return { status: "upgraded", warning: result.warning };
|
|
754
|
+
}
|
|
755
|
+
async function abortUpgrade(mindName) {
|
|
756
|
+
return withUpgradeLock(mindName, () => abortUpgradeCore(mindName));
|
|
757
|
+
}
|
|
758
|
+
async function abortUpgradeCore(mindName) {
|
|
759
|
+
const dir = mindDir(mindName);
|
|
760
|
+
const variantName = upgradeVariantName(mindName);
|
|
761
|
+
const worktreeDir = upgradeWorktreeDir(dir);
|
|
762
|
+
if (!existsSync2(worktreeDir)) {
|
|
763
|
+
throw new Error("No upgrade in progress");
|
|
764
|
+
}
|
|
765
|
+
if (upgradeMidResolution(worktreeDir)) {
|
|
766
|
+
await gitExec(["merge", "--abort"], { cwd: worktreeDir }).catch(() => {
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
await cleanupVariant(variantName, mindName, dir, worktreeDir, {
|
|
770
|
+
stop: true,
|
|
771
|
+
branch: UPGRADE_BRANCH
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// packages/daemon/src/lib/daemon/auto-upgrade.ts
|
|
776
|
+
var alog = logger_default.child("auto-upgrade");
|
|
777
|
+
var RETRY_DELAY_MS = 5e3;
|
|
778
|
+
var blocked = /* @__PURE__ */ new Map();
|
|
779
|
+
function getUpgradeBlocked(name) {
|
|
780
|
+
return blocked.get(name);
|
|
781
|
+
}
|
|
782
|
+
function selectEligible(entries, deps) {
|
|
783
|
+
const eligible = entries.filter((entry) => {
|
|
784
|
+
if (entry.mindType !== "mind") return false;
|
|
785
|
+
if (entry.stage === "seed") return false;
|
|
786
|
+
if (entry.parent) return false;
|
|
787
|
+
if (!deps.isStale(entry)) return false;
|
|
788
|
+
if (deps.readConfig(entry.name)?.upgrades === "manual") return false;
|
|
789
|
+
return true;
|
|
790
|
+
});
|
|
791
|
+
return eligible.sort((a, b) => {
|
|
792
|
+
const aSleeping = deps.isSleeping(a.name) ? 0 : 1;
|
|
793
|
+
const bSleeping = deps.isSleeping(b.name) ? 0 : 1;
|
|
794
|
+
return aSleeping - bSleeping;
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
async function sleep(ms) {
|
|
798
|
+
await new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
799
|
+
}
|
|
800
|
+
var defaultAutoUpgradeOneDeps = {
|
|
801
|
+
isRunning: (name) => getMindManager().isRunning(name),
|
|
802
|
+
runUpgrade,
|
|
803
|
+
abortUpgrade,
|
|
804
|
+
deliverEvent: (name, input) => deliverEvent(name, input),
|
|
805
|
+
delay: sleep
|
|
806
|
+
};
|
|
807
|
+
function pruneBlocked(blockedMap, eligibleNames) {
|
|
808
|
+
for (const name of blockedMap.keys()) {
|
|
809
|
+
if (!eligibleNames.has(name)) blockedMap.delete(name);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
async function autoUpgradeOne(entry, sleeping, deps = defaultAutoUpgradeOneDeps) {
|
|
813
|
+
const wasRunning = deps.isRunning(entry.name);
|
|
814
|
+
const restart = wasRunning && !sleeping;
|
|
815
|
+
const attempt = () => deps.runUpgrade(entry.name, { restart });
|
|
816
|
+
let outcome;
|
|
817
|
+
try {
|
|
818
|
+
outcome = await attempt();
|
|
819
|
+
} catch (err) {
|
|
820
|
+
if (err instanceof UpgradeInProgressError) {
|
|
821
|
+
blocked.set(entry.name, {
|
|
822
|
+
reason: "upgrade already in progress (mid conflict resolution)",
|
|
823
|
+
at: /* @__PURE__ */ new Date()
|
|
824
|
+
});
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
await deps.delay(RETRY_DELAY_MS);
|
|
828
|
+
try {
|
|
829
|
+
outcome = await attempt();
|
|
830
|
+
} catch (err2) {
|
|
831
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
832
|
+
alog.error(`auto-upgrade failed for ${entry.name} after retry`, logger_default.errorData(err2));
|
|
833
|
+
blocked.set(entry.name, { reason: message, at: /* @__PURE__ */ new Date() });
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
if (outcome.status === "conflicts") {
|
|
838
|
+
await deps.abortUpgrade(entry.name).catch((err) => {
|
|
839
|
+
alog.warn(`failed to abort conflicted auto-upgrade for ${entry.name}`, logger_default.errorData(err));
|
|
840
|
+
});
|
|
841
|
+
const fileList = outcome.files.join(", ");
|
|
842
|
+
await deps.deliverEvent(entry.name, {
|
|
843
|
+
type: "notice",
|
|
844
|
+
body: `Auto-upgrade hit merge conflicts in: ${fileList}. Run \`volute mind upgrade ${entry.name}\` to resolve them manually (or set "upgrades": "manual" in home/.config/volute.json to stop auto attempts).`
|
|
845
|
+
});
|
|
846
|
+
blocked.set(entry.name, { reason: `merge conflicts: ${fileList}`, at: /* @__PURE__ */ new Date() });
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
async function runAutoUpgrades() {
|
|
850
|
+
let entries;
|
|
851
|
+
try {
|
|
852
|
+
entries = await readRegistry();
|
|
853
|
+
} catch (err) {
|
|
854
|
+
alog.error("failed to read registry for auto-upgrade pass", logger_default.errorData(err));
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
const sleepManager = getSleepManagerIfReady();
|
|
858
|
+
const isSleeping = (name) => sleepManager?.isSleeping(name) ?? false;
|
|
859
|
+
const eligible = selectEligible(entries, {
|
|
860
|
+
isStale: (entry) => isTemplateStale(entry),
|
|
861
|
+
isSleeping,
|
|
862
|
+
readConfig: (name) => readVoluteConfig(mindDir(name))
|
|
863
|
+
});
|
|
864
|
+
pruneBlocked(blocked, new Set(eligible.map((e) => e.name)));
|
|
865
|
+
if (eligible.length === 0) return;
|
|
866
|
+
alog.info(`auto-upgrade pass starting for ${eligible.length} mind(s)`, {
|
|
867
|
+
minds: eligible.map((e) => e.name)
|
|
868
|
+
});
|
|
869
|
+
for (const entry of eligible) {
|
|
870
|
+
blocked.delete(entry.name);
|
|
871
|
+
try {
|
|
872
|
+
await autoUpgradeOne(entry, isSleeping(entry.name));
|
|
873
|
+
} catch (err) {
|
|
874
|
+
alog.error(`unexpected error auto-upgrading ${entry.name}`, logger_default.errorData(err));
|
|
875
|
+
blocked.set(entry.name, {
|
|
876
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
877
|
+
at: /* @__PURE__ */ new Date()
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
export {
|
|
884
|
+
npmInstallAsMind,
|
|
885
|
+
npmInstallNeeded,
|
|
886
|
+
validateBranchName,
|
|
887
|
+
findUnresolvedHomeFiles,
|
|
888
|
+
formatUnresolvedHomeFilesMessage,
|
|
889
|
+
mergeVariantExcludingMemory,
|
|
890
|
+
VariantMergeError,
|
|
891
|
+
UpgradeInProgressError,
|
|
892
|
+
TEMPLATE_BRANCH,
|
|
893
|
+
configureGitIdentity,
|
|
894
|
+
upgradeInProgress,
|
|
895
|
+
upgradeDiff,
|
|
896
|
+
runUpgrade,
|
|
897
|
+
continueUpgrade,
|
|
898
|
+
abortUpgrade,
|
|
899
|
+
getUpgradeBlocked,
|
|
900
|
+
selectEligible,
|
|
901
|
+
pruneBlocked,
|
|
902
|
+
autoUpgradeOne,
|
|
903
|
+
runAutoUpgrades
|
|
904
|
+
};
|