toolroll 0.9.82 → 0.9.84
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/CHANGELOG.md +41 -0
- package/README.md +4 -3
- package/dist/assignment-ui.js +5 -1
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +22 -22
- package/dist/browser-crew.js +5 -1
- package/dist/browser-workspace.d.ts +7 -0
- package/dist/contracts/console-api.d.ts +2 -1
- package/dist/contracts/console-api.js +5 -1
- package/dist/control-setup.d.ts +9 -0
- package/dist/control-setup.js +31 -4
- package/dist/control-ui.d.ts +7 -1
- package/dist/control-ui.js +3 -2
- package/dist/dispatch.d.ts +15 -0
- package/dist/dispatch.js +26 -4
- package/dist/first-run.d.ts +5 -2
- package/dist/first-run.js +13 -2
- package/dist/first-tasks-child.d.ts +1 -0
- package/dist/first-tasks-child.js +11 -0
- package/dist/folder-look.d.ts +110 -0
- package/dist/folder-look.js +306 -0
- package/dist/guides.js +4 -3
- package/dist/learning-child.d.ts +1 -0
- package/dist/learning-child.js +34 -0
- package/dist/learning-look.d.ts +25 -0
- package/dist/learning-look.js +72 -0
- package/dist/model-catalog.d.ts +5 -0
- package/dist/model-catalog.js +11 -2
- package/dist/model-choices.d.ts +188 -0
- package/dist/model-choices.js +405 -0
- package/dist/model-names.d.ts +6 -0
- package/dist/model-names.js +34 -0
- package/dist/models-ui.d.ts +69 -5
- package/dist/models-ui.js +53 -10
- package/dist/needs-you.d.ts +8 -0
- package/dist/needs-you.js +8 -0
- package/dist/operate.d.ts +53 -0
- package/dist/operate.js +277 -49
- package/dist/project-admission.d.ts +4 -1
- package/dist/project-admission.js +23 -2
- package/dist/project-identity.d.ts +44 -0
- package/dist/project-identity.js +153 -0
- package/dist/project-knowledge.d.ts +6 -6
- package/dist/project-tools.d.ts +4 -1
- package/dist/project-tools.js +7 -2
- package/dist/project.d.ts +4 -1
- package/dist/project.js +11 -4
- package/dist/repos.d.ts +7 -0
- package/dist/repos.js +57 -0
- package/dist/serve.js +91 -17
- package/dist/server/chat.js +5 -4
- package/dist/server/chrome.d.ts +3 -0
- package/dist/server/chrome.js +5 -2
- package/dist/server/console.css +5 -2
- package/dist/server/flows.js +13 -3
- package/dist/server/guards.d.ts +4 -0
- package/dist/server/guards.js +12 -3
- package/dist/server/http.d.ts +12 -0
- package/dist/server/http.js +9 -0
- package/dist/server/pages.js +102 -92
- package/dist/server/people-tokens.js +40 -20
- package/dist/server/render-pages.d.ts +12 -16
- package/dist/server/render-pages.js +30 -69
- package/dist/server/render-people.d.ts +14 -2
- package/dist/server/render-people.js +35 -12
- package/dist/server/render-settings.js +1 -0
- package/dist/server/render-tasks.js +7 -14
- package/dist/server/request-context.d.ts +2 -0
- package/dist/server/route-table.js +8 -1
- package/dist/server/runtime.d.ts +4 -0
- package/dist/server/runtime.js +1 -0
- package/dist/server/settings.d.ts +58 -1
- package/dist/server/settings.js +211 -71
- package/dist/server/signup-gate.d.ts +9 -0
- package/dist/server/signup-gate.js +40 -0
- package/dist/server/tasks.js +19 -5
- package/dist/settings-kit-script.d.ts +21 -0
- package/dist/settings-kit-script.js +67 -0
- package/dist/settings-kit.d.ts +137 -0
- package/dist/settings-kit.js +186 -0
- package/dist/settings-proofs.d.ts +64 -0
- package/dist/settings-proofs.js +101 -0
- package/dist/setup-guide.js +4 -2
- package/dist/spend-report.d.ts +87 -0
- package/dist/spend-report.js +265 -0
- package/dist/spend-ui.d.ts +29 -8
- package/dist/spend-ui.js +74 -39
- package/dist/task-sizing.d.ts +5 -3
- package/dist/task-sizing.js +4 -4
- package/dist/up-processes.d.ts +13 -0
- package/dist/up-processes.js +6 -2
- package/package.json +1 -1
package/dist/operate.js
CHANGED
|
@@ -110,7 +110,7 @@ import { claimInstance, clearInstance, openInstanceSocket } from "./console-inst
|
|
|
110
110
|
import { authorizePlanUnderMode } from "./plan-auto.js";
|
|
111
111
|
import { ghDispatchAdapter, mirrorTaskId, syncPass } from "./sync.js";
|
|
112
112
|
import { LIVE_FILE_BOUND, LIVE_RETAIN_MS, sweepLiveLogs } from "./live.js";
|
|
113
|
-
import { addRepos, removeRepos, updateRepos, loadRepos, loadProjectRegistry, updateProjectRegistry } from "./repos.js";
|
|
113
|
+
import { addRepos, removeRepos, updateRepos, loadRepos, loadProjectRegistry, updateProjectRegistry, registryRemovals } from "./repos.js";
|
|
114
114
|
import { deleteProject, holdingsWords, projectHoldings, projectRunning } from "./project-delete.js";
|
|
115
115
|
import { pushPass } from "./push.js";
|
|
116
116
|
import { chmodSync, closeSync, constants as fsConstants, existsSync, fstatSync, fsyncSync, openSync, readFileSync, readSync, realpathSync, renameSync, unlinkSync, writeSync, writeFileSync } from "node:fs";
|
|
@@ -181,6 +181,8 @@ import { createDecisionServer } from "./serve.js";
|
|
|
181
181
|
import { readTailnetNames } from "./tailnet.js";
|
|
182
182
|
import { daemonLaunchCommand, daemonStatus, installReplacingLegacy, installedLegacyDaemon, planDaemon, uninstallDaemon, awaitFreshHeartbeat, } from "./daemon.js";
|
|
183
183
|
import { bridgePass, clearBotToken, createTransport, followBridge, hashPairingCode, loadBotToken, mintPairingCode, networkFence, networkFenceLine, redactToken, saveBotToken, PAIRING_TTL_MS, TOKEN_ENV, telegramHookSecret, telegramPushUrl, } from "./telegram.js";
|
|
184
|
+
import { projectIdentities } from "./project-identity.js";
|
|
185
|
+
import { lookIdentities } from "./folder-look.js";
|
|
184
186
|
import { scanRepo } from "./capscan.js";
|
|
185
187
|
import { computeGaps, describeCapability } from "./gaps.js";
|
|
186
188
|
import { ask as promptAsk, askHidden as promptHidden, confirm as promptConfirm, interactive as promptInteractive, underAgent } from "./prompt.js";
|
|
@@ -220,6 +222,7 @@ import { fileTaskProposal, validateTaskText } from "./proposal.js";
|
|
|
220
222
|
import { TEMPLATES, templateByName } from "./templates.js";
|
|
221
223
|
import { isDirectChatProvider, isSubscriptionChatProvider, priceOf, PRICED_MODELS } from "./converse.js";
|
|
222
224
|
import { resolvePhaseAgent, resolveScopeProfile, resolveRouteCandidates, routeOfTask, INSTALLATION_SCOPE } from "./agentconfig.js";
|
|
225
|
+
import { applyModelChoices, stateDigest } from "./model-choices.js";
|
|
223
226
|
import { isTaskSize, legOf, projectRoute, routeDigestOf, routeWords, TASK_SIZES, PHASES as ROUTE_PHASES } from "./phase-routing.js";
|
|
224
227
|
import { observeProviderReadiness, reportProviderReadinessAuthed } from "./runner.js";
|
|
225
228
|
import { parseDemoUrl, projectDemoUrl, saveProjectDemo } from "./project-demo.js";
|
|
@@ -965,6 +968,7 @@ async function dispatchOn(command, positional, flags, repoList, base, options, e
|
|
|
965
968
|
...(options.onboardSeams === undefined ? {} : { onboardSeams: options.onboardSeams }),
|
|
966
969
|
...(options.upSeams === undefined ? {} : { upSeams: options.upSeams }),
|
|
967
970
|
...(options.upRole === undefined ? {} : { upRole: options.upRole }),
|
|
971
|
+
...(options.folderLook === undefined ? {} : { folderLook: options.folderLook }),
|
|
968
972
|
...(upNeedsOneProcess(options) ? { upOneProcess: true } : {}),
|
|
969
973
|
...(activeRemote() === null ? {} : { principal: activeRemote() }),
|
|
970
974
|
});
|
|
@@ -3939,7 +3943,13 @@ async function startConsole(options) {
|
|
|
3939
3943
|
const { context } = options;
|
|
3940
3944
|
// A git incident (teardown-git.ts) raises an attention notification in this console's outbox.
|
|
3941
3945
|
noticeGitIncidents(context.store, context.clock);
|
|
3946
|
+
// Every project folder this console starts with is resolved off its thread, under a deadline, after it is listening
|
|
3947
|
+
// (consoleIdentitiesInBackground): the console answers at once, and a project shows up once its folder answers. Until
|
|
3948
|
+
// then it is unresolved and kept restrictive; one that doesn't answer (a drive away, an iCloud folder macOS is
|
|
3949
|
+
// holding) is looked at again by the background pass (project-identity.ts).
|
|
3950
|
+
const identities = projectIdentities(context.folderLook);
|
|
3942
3951
|
const server = createDecisionServer({
|
|
3952
|
+
identities,
|
|
3943
3953
|
store: context.store,
|
|
3944
3954
|
// Sign-in, integration and model checks, and the tidy-ups no page visit does (docs/plans/get-side-effects.md).
|
|
3945
3955
|
backgroundChecks: true,
|
|
@@ -3974,6 +3984,7 @@ async function startConsole(options) {
|
|
|
3974
3984
|
}).catch(error => {
|
|
3975
3985
|
throw new Error(`could not listen on ${options.host}:${options.port} — ${describe(error)}`);
|
|
3976
3986
|
});
|
|
3987
|
+
consoleIdentitiesInBackground(identities, [...options.repos ?? [], ...options.projectRoots ?? [], ...context.store.listProjects().map(one => one.path), ...context.store.knownRepos()]);
|
|
3977
3988
|
// v104: monitoring runs beside the console (every setup has one): one loop sends the audit stream and traces where
|
|
3978
3989
|
// Settings → Monitoring says, and stops when the console does. A lease keeps a second process from sending too.
|
|
3979
3990
|
const stopMonitoring = startMonitoring({
|
|
@@ -5034,6 +5045,9 @@ async function configCommand(positional, flags, context) {
|
|
|
5034
5045
|
if (phase === undefined || !["plan", "build", "repair", "review"].includes(phase))
|
|
5035
5046
|
return fail(write, json, `config ${action}`, "usage", "which phase? plan, build, repair, review — or chat", EXIT.usage);
|
|
5036
5047
|
const acting = await askCredentials(flags, context);
|
|
5048
|
+
// The credential generation the proof below is checked against, read BEFORE it: a rotation after this read makes
|
|
5049
|
+
// the write refuse (model-choices.ts re-checks it inside the transaction), never borrow the new generation.
|
|
5050
|
+
const provedGeneration = acting === null ? -1 : store.accountOf(acting.name)?.generation ?? -1;
|
|
5037
5051
|
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
5038
5052
|
if (acting === null || !verified?.ok)
|
|
5039
5053
|
return refuseApprover(context, `config ${action}`, verified, "changing spend routing takes `--as <you> --token <t>`", phase);
|
|
@@ -5049,6 +5063,18 @@ async function configCommand(positional, flags, context) {
|
|
|
5049
5063
|
// Clearing one left from before stays possible.
|
|
5050
5064
|
if (tierGiven === "light" && phase !== "build" && !(action === "clear" && phase === "plan" && !flag(flags, "also")))
|
|
5051
5065
|
return fail(write, json, `config ${action}`, "usage", "--tier light is for build only — a small change makes no plan, and repairs resume the builder's session", EXIT.usage);
|
|
5066
|
+
// Every row below — plain, tier, `--also`, and a leftover light planner's removal — is written through the one write
|
|
5067
|
+
// path (model-choices.ts): one transaction, the actor re-proved as an instance operator at the credential generation
|
|
5068
|
+
// proved above by a proof that still stands, the target and the resulting configuration checked, every row ledgered.
|
|
5069
|
+
// A project named with --repo must be one Toolroll knows (the editor's own rule).
|
|
5070
|
+
const projectKnown = repoGiven === undefined || (await knownProject(context, repoGiven, { listed: true, canonical: true })) !== undefined;
|
|
5071
|
+
const proof = { actor: acting.name, generation: provedGeneration, method: verified.method };
|
|
5072
|
+
const written = (slot, to, extra = []) => writeModelRow(store, proof, scope, () => projectKnown, slot, to, clock(), extra);
|
|
5073
|
+
// A configuration still missing a role's model is saved, and said (JSON: `incomplete`).
|
|
5074
|
+
const said = (command, result, payload, lines) => {
|
|
5075
|
+
const incomplete = result.ok ? result.unresolved : null;
|
|
5076
|
+
return succeed(write, json, command, { ...payload, ...(incomplete === null ? {} : { incomplete }) }, () => [...lines(), ...(incomplete === null ? [] : [` ! new tasks can't be filed here yet: ${incomplete}`])]);
|
|
5077
|
+
};
|
|
5052
5078
|
// Another provider's candidate on the same tier (`--also`): one per provider, and at filing the one whose plan
|
|
5053
5079
|
// has more room runs.
|
|
5054
5080
|
if (flag(flags, "also")) {
|
|
@@ -5059,8 +5085,11 @@ async function configCommand(positional, flags, context) {
|
|
|
5059
5085
|
if (alsoProvider !== undefined && !isProviderId(alsoProvider))
|
|
5060
5086
|
return fail(write, json, `config ${action}`, "usage", `--provider is one of ${PROVIDER_IDS.join(", ")}`, EXIT.usage);
|
|
5061
5087
|
if (action === "clear") {
|
|
5062
|
-
const
|
|
5063
|
-
|
|
5088
|
+
const alsoClear = written(null, null, [{ kind: "also-clear", phase, tier, provider: alsoProvider ?? null }]);
|
|
5089
|
+
if (!alsoClear.ok)
|
|
5090
|
+
return fail(write, json, "config clear", alsoClear.reason, alsoClear.problem, EXIT.refused);
|
|
5091
|
+
const removed = alsoClear.extra;
|
|
5092
|
+
return said("config clear", alsoClear, { scope, phase, tier, also: true, provider: alsoProvider ?? null, cleared: removed }, () => [
|
|
5064
5093
|
removed > 0 ? `Removed ${removed} other-provider ${tier} ${phase} agent${removed === 1 ? "" : "s"} at ${scope}; sealed routes are untouched.` : `No other-provider ${tier} ${phase} agent${alsoProvider === undefined ? "" : ` on ${alsoProvider}`} was configured at ${scope}.`,
|
|
5065
5094
|
]);
|
|
5066
5095
|
}
|
|
@@ -5070,10 +5099,12 @@ async function configCommand(positional, flags, context) {
|
|
|
5070
5099
|
const alsoValid = validateSpec({ provider: alsoProvider, model: alsoModel });
|
|
5071
5100
|
if (!alsoValid.ok)
|
|
5072
5101
|
return fail(write, json, "config set", "invalid", alsoValid.problem, EXIT.usage);
|
|
5073
|
-
|
|
5102
|
+
const alsoSet = written(null, null, [{ kind: "also-set", phase, tier, provider: alsoProvider, model: alsoModel }]);
|
|
5103
|
+
if (!alsoSet.ok)
|
|
5104
|
+
return fail(write, json, "config set", alsoSet.reason, alsoSet.problem, EXIT.refused);
|
|
5074
5105
|
const resolvedNow = resolveRouteCandidates(store, scope === INSTALLATION_SCOPE ? null : scope);
|
|
5075
5106
|
const own = !resolvedNow.ok ? null : tier === "routine" ? resolvedNow.candidates[phase].routine : tier === "light" ? resolvedNow.candidates[phase].light ?? null : resolvedNow.candidates[phase].strong;
|
|
5076
|
-
return
|
|
5107
|
+
return said("config set", alsoSet, { scope, phase, tier, also: true, provider: alsoProvider, model: alsoModel }, () => [
|
|
5077
5108
|
`${tier} ${phase} at ${scope === INSTALLATION_SCOPE ? "the installation" : scope} can also run ${alsoProvider} · ${alsoModel}, set by ${acting.name}.`,
|
|
5078
5109
|
own !== null && own.provider === alsoProvider && !own.source.includes("also")
|
|
5079
5110
|
? ` ! the ${tier} ${phase} agent already runs ${alsoProvider} (${own.model}) — this one is not used while it does.`
|
|
@@ -5081,20 +5112,39 @@ async function configCommand(positional, flags, context) {
|
|
|
5081
5112
|
]);
|
|
5082
5113
|
}
|
|
5083
5114
|
if (action === "clear") {
|
|
5115
|
+
if (tierGiven === "light" && phase === "build") {
|
|
5116
|
+
const lightWrite = written({ phase, tier: "light" }, null);
|
|
5117
|
+
if (!lightWrite.ok)
|
|
5118
|
+
return fail(write, json, "config clear", lightWrite.reason, lightWrite.problem, EXIT.refused);
|
|
5119
|
+
const clearedLight = lightWrite.diff.length > 0;
|
|
5120
|
+
return said("config clear", lightWrite, { scope, phase, tier: "light", cleared: clearedLight }, () => [
|
|
5121
|
+
clearedLight ? `Cleared the light ${phase} agent at ${scope} — small changes keep the default and say so; sealed routes are untouched.` : `No light ${phase} agent was configured at ${scope}.`,
|
|
5122
|
+
]);
|
|
5123
|
+
}
|
|
5084
5124
|
if (tierGiven === "light") {
|
|
5085
|
-
|
|
5086
|
-
|
|
5125
|
+
// A light planner from before light became build-only: removed here, never written.
|
|
5126
|
+
const legacyClear = written(null, null, [{ kind: "legacy-light-clear", phase: phase }]);
|
|
5127
|
+
if (!legacyClear.ok)
|
|
5128
|
+
return fail(write, json, "config clear", legacyClear.reason, legacyClear.problem, EXIT.refused);
|
|
5129
|
+
const clearedLight = legacyClear.extra > 0;
|
|
5130
|
+
return said("config clear", legacyClear, { scope, phase, tier: "light", cleared: clearedLight }, () => [
|
|
5087
5131
|
clearedLight ? `Cleared the light ${phase} agent at ${scope} — small changes keep the default and say so; sealed routes are untouched.` : `No light ${phase} agent was configured at ${scope}.`,
|
|
5088
5132
|
]);
|
|
5089
5133
|
}
|
|
5090
5134
|
if (tierGiven === "strong") {
|
|
5091
|
-
const
|
|
5092
|
-
|
|
5135
|
+
const strongWrite = written({ phase: phase, tier: "strong" }, null);
|
|
5136
|
+
if (!strongWrite.ok)
|
|
5137
|
+
return fail(write, json, "config clear", strongWrite.reason, strongWrite.problem, EXIT.refused);
|
|
5138
|
+
const clearedStrong = strongWrite.diff.length > 0;
|
|
5139
|
+
return said("config clear", strongWrite, { scope, phase, tier: "strong", cleared: clearedStrong }, () => [
|
|
5093
5140
|
clearedStrong ? `Cleared the strong ${phase} agent at ${scope} — demanding routes keep the default and say so; sealed routes are untouched.` : `No strong ${phase} agent was configured at ${scope}.`,
|
|
5094
5141
|
]);
|
|
5095
5142
|
}
|
|
5096
|
-
const
|
|
5097
|
-
|
|
5143
|
+
const routineWrite = written({ phase: phase, tier: "routine" }, null);
|
|
5144
|
+
if (!routineWrite.ok)
|
|
5145
|
+
return fail(write, json, "config clear", routineWrite.reason, routineWrite.problem, EXIT.refused);
|
|
5146
|
+
const cleared = routineWrite.diff.length > 0;
|
|
5147
|
+
return said("config clear", routineWrite, { scope, phase, cleared }, () => [
|
|
5098
5148
|
cleared ? `Cleared ${phase} at ${scope} — it resolves one layer down now.` : `Nothing was configured for ${phase} at ${scope}.`,
|
|
5099
5149
|
]);
|
|
5100
5150
|
}
|
|
@@ -5108,8 +5158,10 @@ async function configCommand(positional, flags, context) {
|
|
|
5108
5158
|
if (tierGiven === "light") {
|
|
5109
5159
|
if (modelGiven === null)
|
|
5110
5160
|
return fail(write, json, "config set", "usage", `a light ${phase} agent names an exact --model — approvals bind exact routing`, EXIT.usage);
|
|
5111
|
-
|
|
5112
|
-
|
|
5161
|
+
const lightSet = written({ phase: phase, tier: "light" }, { provider: providerGiven, model: modelGiven });
|
|
5162
|
+
if (!lightSet.ok)
|
|
5163
|
+
return fail(write, json, "config set", lightSet.reason, lightSet.problem, EXIT.refused);
|
|
5164
|
+
return said("config set", lightSet, { scope, phase, tier: "light", provider: providerGiven, model: modelGiven }, () => [
|
|
5113
5165
|
`light ${phase} at ${scope === INSTALLATION_SCOPE ? "the installation" : scope} is ${providerGiven} · ${modelGiven}, set by ${acting.name}.`,
|
|
5114
5166
|
" small changes filed from now on build on it with no plan; sealed routes are untouched.",
|
|
5115
5167
|
]);
|
|
@@ -5119,14 +5171,19 @@ async function configCommand(positional, flags, context) {
|
|
|
5119
5171
|
return fail(write, json, "config set", "invalid", "gemini has no isolation posture for the review phase yet — name claude or codex as the strong reviewer", EXIT.usage);
|
|
5120
5172
|
if (modelGiven === null)
|
|
5121
5173
|
return fail(write, json, "config set", "usage", `a strong ${phase} agent names an exact --model — approvals bind exact routing`, EXIT.usage);
|
|
5122
|
-
|
|
5123
|
-
|
|
5174
|
+
const strongSet = written({ phase: phase, tier: "strong" }, { provider: providerGiven, model: modelGiven });
|
|
5175
|
+
if (!strongSet.ok)
|
|
5176
|
+
return fail(write, json, "config set", strongSet.reason, strongSet.problem, EXIT.refused);
|
|
5177
|
+
return said("config set", strongSet, { scope, phase, tier: "strong", provider: providerGiven, model: modelGiven }, () => [
|
|
5124
5178
|
`strong ${phase} at ${scope === INSTALLATION_SCOPE ? "the installation" : scope} is ${providerGiven}${modelGiven === null ? "" : ` · ${modelGiven}`}, set by ${acting.name}.`,
|
|
5125
5179
|
" large or risky, strict, screenshot-proof, and automerge routes filed from now on reach for it; sealed routes are untouched.",
|
|
5126
5180
|
...(phase === "repair" ? [" a strong repair agent applies only when its provider is the build's — cross-provider repair does not exist."] : []),
|
|
5127
5181
|
]);
|
|
5128
5182
|
}
|
|
5129
|
-
|
|
5183
|
+
// A routine row may still name no model on the command line (said below); the editor never writes one.
|
|
5184
|
+
const routineSet = written({ phase: phase, tier: "routine" }, { provider: providerGiven, model: modelGiven });
|
|
5185
|
+
if (!routineSet.ok)
|
|
5186
|
+
return fail(write, json, "config set", routineSet.reason, routineSet.problem, EXIT.refused);
|
|
5130
5187
|
const warnings = [];
|
|
5131
5188
|
if (modelGiven === null) {
|
|
5132
5189
|
warnings.push(`no --model named: approvals bind exact routing, so scopes cannot file until ${phase} has an exact model — \`config set ${phase} --provider ${providerGiven} --model <model>\``);
|
|
@@ -5137,7 +5194,7 @@ async function configCommand(positional, flags, context) {
|
|
|
5137
5194
|
if (providerGiven !== "claude") {
|
|
5138
5195
|
warnings.push(`${providerGiven} does not report dollar cost: its runs land as UNMEASURED, and any schedule with a weekly limit fails closed on them by design.`);
|
|
5139
5196
|
}
|
|
5140
|
-
return
|
|
5197
|
+
return said("config set", routineSet, { scope, phase, provider: providerGiven, model: modelGiven, warnings }, () => [
|
|
5141
5198
|
`${phase} at ${scope === INSTALLATION_SCOPE ? "the installation" : scope} now runs ${providerGiven}${modelGiven === null ? "" : ` · ${modelGiven}`}, set by ${acting.name}.`,
|
|
5142
5199
|
...warnings.map(one => ` ! ${one}`),
|
|
5143
5200
|
]);
|
|
@@ -7055,6 +7112,8 @@ async function upCommand(flags, context) {
|
|
|
7055
7112
|
// folders they may be added from are installation state, not arguments a
|
|
7056
7113
|
// person must repeat on every start.
|
|
7057
7114
|
const registryPath = registryPathOf(context);
|
|
7115
|
+
// Read before the list itself: a line removed after this (even if saved again) counts differently from now on.
|
|
7116
|
+
const removalsAtStart = registryRemovals(registryPath);
|
|
7058
7117
|
const loadedRegistry = await loadProjectRegistry(registryPath);
|
|
7059
7118
|
if ("error" in loadedRegistry)
|
|
7060
7119
|
return fail(write, json, "up", "registry", loadedRegistry.error, EXIT.refused);
|
|
@@ -7078,23 +7137,28 @@ async function upCommand(flags, context) {
|
|
|
7078
7137
|
// soon as a projects folder or saved repository exists.
|
|
7079
7138
|
const repos = [];
|
|
7080
7139
|
const proveRepo = async (input) => {
|
|
7081
|
-
|
|
7082
|
-
|
|
7083
|
-
|
|
7084
|
-
try {
|
|
7085
|
-
return realpathSync(top.stdout.trim());
|
|
7086
|
-
}
|
|
7087
|
-
catch {
|
|
7088
|
-
return null;
|
|
7089
|
-
}
|
|
7140
|
+
// The checkout's real path is read off the thread, under a deadline (folder-look.ts): a folder that doesn't answer
|
|
7141
|
+
// is unavailable, never a wait.
|
|
7142
|
+
return (await upProveSaved(gitRun, input))?.root ?? null;
|
|
7090
7143
|
};
|
|
7144
|
+
// A saved project that doesn't answer now (a drive not connected, a folder macOS is holding) is looked at again
|
|
7145
|
+
// while this start runs, and connects once it answers (upDeferredRetry).
|
|
7146
|
+
const deferredRepos = [];
|
|
7147
|
+
const deferredGenerations = {};
|
|
7148
|
+
// The saved line(s) each project was proved from, so its removal is judged by its own line alone.
|
|
7149
|
+
const repoLines = {};
|
|
7091
7150
|
for (const input of loadedRegistry.repos) {
|
|
7092
7151
|
const root = await proveRepo(input);
|
|
7093
7152
|
if (root === null) {
|
|
7094
|
-
progress(`saved project unavailable — skipped ${input}`);
|
|
7153
|
+
progress(`saved project unavailable — skipped ${input}; it connects when it's back`);
|
|
7154
|
+
deferredRepos.push(input);
|
|
7155
|
+
// An unreadable removal record leaves no count that could match later: the authority can't be used.
|
|
7156
|
+
deferredGenerations[input] = removalsAtStart === null ? -1 : removalsAtStart[input] ?? 0;
|
|
7095
7157
|
}
|
|
7096
|
-
else
|
|
7097
|
-
repos.
|
|
7158
|
+
else {
|
|
7159
|
+
if (!repos.includes(root))
|
|
7160
|
+
repos.push(root);
|
|
7161
|
+
(repoLines[root] ??= []).push(input);
|
|
7098
7162
|
}
|
|
7099
7163
|
}
|
|
7100
7164
|
const explicitRepos = context.repoList ?? [];
|
|
@@ -7118,7 +7182,9 @@ async function upCommand(flags, context) {
|
|
|
7118
7182
|
if (cwdRepo !== null)
|
|
7119
7183
|
pinnedRepos.add(cwdRepo);
|
|
7120
7184
|
}
|
|
7121
|
-
|
|
7185
|
+
// Saved projects that are only away for now still start the service: it serves an empty, restrictive set until they
|
|
7186
|
+
// answer (never the unscoped mode), and connects each as it comes back.
|
|
7187
|
+
if (repos.length === 0 && projectRoots.length === 0 && deferredRepos.length === 0)
|
|
7122
7188
|
return fail(write, json, "up", "no-projects", "choose where your projects live once: `toolroll up --project-root <dir>`", EXIT.refused);
|
|
7123
7189
|
// A person at a terminal reads three lines (onboarding); the folders are for --verbose, a service log, or an agent.
|
|
7124
7190
|
const quietStart = !json && !flags.has("verbose") && (context.upSeams?.terminal ?? process.stdout.isTTY === true) && !underAgent(context.upSeams?.env ?? process.env);
|
|
@@ -7376,6 +7442,9 @@ async function upCommand(flags, context) {
|
|
|
7376
7442
|
type: "start", role: "worker", databaseFile: context.databaseFile, containment: containmentFlag, build,
|
|
7377
7443
|
config: {
|
|
7378
7444
|
runner: runnerName, token: runnerToken, repos: generation === 1 ? [...repos] : [...currentRepos], pinnedRepos: [...pinnedRepos], projectRoots, registryPath, pool,
|
|
7445
|
+
// What this start still holds for its saved projects that were away: a restarted worker never gets back an
|
|
7446
|
+
// authority an earlier one gave up (connected, or removed from the saved list).
|
|
7447
|
+
deferredRepos: [...deferredRepos], deferredGenerations: { ...deferredGenerations }, repoLines,
|
|
7379
7448
|
forMs: forGiven === undefined ? null : Number(forGiven), publicUrl: publicUrlFlag ?? null, stopGraceMs, verbose: flags.has("verbose"),
|
|
7380
7449
|
terminal: context.upSeams?.terminal ?? process.stdout.isTTY === true,
|
|
7381
7450
|
// A restart waits out a crashed predecessor's lease (at most a minute), then recovers it: as a fresh `up` after a crash.
|
|
@@ -7385,6 +7454,12 @@ async function upCommand(flags, context) {
|
|
|
7385
7454
|
onMessage: message => {
|
|
7386
7455
|
if (message.type === "log")
|
|
7387
7456
|
roleLine("worker")(message.line);
|
|
7457
|
+
else if (message.type === "deferred") {
|
|
7458
|
+
// Only ever narrows: a worker's word can't give back an authority this start no longer holds.
|
|
7459
|
+
const narrowed = upNarrowDeferred(deferredRepos, message.repos);
|
|
7460
|
+
deferredRepos.length = 0;
|
|
7461
|
+
deferredRepos.push(...narrowed);
|
|
7462
|
+
}
|
|
7388
7463
|
else if (message.type === "repos") {
|
|
7389
7464
|
// The console's admitted projects widen only to what the builder is bound to in the store (the worker binds
|
|
7390
7465
|
// through the authenticated door before it reports): a worker's word alone admits nothing.
|
|
@@ -7467,11 +7542,14 @@ async function upCommand(flags, context) {
|
|
|
7467
7542
|
pendingLinks.set(id, value => { clearTimeout(timer); resolveLink(value); });
|
|
7468
7543
|
consoleRole?.send({ type: "sign-in-link", id, account });
|
|
7469
7544
|
});
|
|
7470
|
-
|
|
7471
|
-
|
|
7545
|
+
// Every start says where the saved password is and the exact command that shows it, browser or not (Settings
|
|
7546
|
+
// revamp S4, D2). Only the path is printed, never what the file holds: absolute (a relative --db names this
|
|
7547
|
+
// directory), single-quoted (no ~ or $ expands), after `--` (a leading dash is never an option).
|
|
7548
|
+
const saved = approverPlan2.passwordFile === null ? null : resolve(approverPlan2.passwordFile);
|
|
7549
|
+
const showCommand = saved === null ? null : `cat -- '${saved.replaceAll("'", "'\\''")}'`;
|
|
7472
7550
|
const signInWords = account === null
|
|
7473
7551
|
? `sign in as one of ${approverPlan2.approvers.join(", ")}`
|
|
7474
|
-
:
|
|
7552
|
+
: showCommand === null ? `sign in as ${account} with your password` : `sign in as ${account}`;
|
|
7475
7553
|
write("");
|
|
7476
7554
|
write(`Toolroll is on ${url}`);
|
|
7477
7555
|
if (link !== null) {
|
|
@@ -7481,8 +7559,11 @@ async function upCommand(flags, context) {
|
|
|
7481
7559
|
}
|
|
7482
7560
|
else {
|
|
7483
7561
|
write(`${signInWords.charAt(0).toUpperCase()}${signInWords.slice(1)}.`);
|
|
7484
|
-
write("Ctrl-C stops it.");
|
|
7485
7562
|
}
|
|
7563
|
+
if (showCommand !== null)
|
|
7564
|
+
write(`Your password: run \`${showCommand}\``);
|
|
7565
|
+
if (link === null)
|
|
7566
|
+
write("Ctrl-C stops it.");
|
|
7486
7567
|
}
|
|
7487
7568
|
}
|
|
7488
7569
|
// A bounded run (--for) counts from readiness, so a slow start never eats the window it asked for.
|
|
@@ -11718,7 +11799,7 @@ function upNeedsOneProcess(options) {
|
|
|
11718
11799
|
options.flowEveryMs !== undefined || options.platform !== undefined || options.fenceAvailable !== undefined || options.flowFetch !== undefined ||
|
|
11719
11800
|
options.integrationIo !== undefined || options.shouldStop !== undefined || options.leadSeams !== undefined || options.waitSleep !== undefined ||
|
|
11720
11801
|
options.releaseIo !== undefined || options.installBin !== undefined || options.tempRoots !== undefined || options.filingSizer !== undefined ||
|
|
11721
|
-
options.now !== undefined;
|
|
11802
|
+
options.now !== undefined || options.folderLook !== undefined;
|
|
11722
11803
|
}
|
|
11723
11804
|
/** The entry a role child runs: the built sibling when installed, the TypeScript source through tsx in a checkout. */
|
|
11724
11805
|
/**
|
|
@@ -12093,15 +12174,9 @@ async function runUpWorkerRole(context, config, port, build) {
|
|
|
12093
12174
|
const pinnedRepos = new Set(config.pinnedRepos);
|
|
12094
12175
|
const gitRun = context.gitRunner ?? ((file, args, opts) => run(file, [...args], { ...(opts?.cwd === undefined ? {} : { cwd: opts.cwd }), timeoutMs: 10_000 }));
|
|
12095
12176
|
const proveRepo = async (input) => {
|
|
12096
|
-
|
|
12097
|
-
|
|
12098
|
-
|
|
12099
|
-
try {
|
|
12100
|
-
return realpathSync(top.stdout.trim());
|
|
12101
|
-
}
|
|
12102
|
-
catch {
|
|
12103
|
-
return null;
|
|
12104
|
-
}
|
|
12177
|
+
// The checkout's real path is read off the thread, under a deadline (folder-look.ts): a folder that doesn't answer
|
|
12178
|
+
// is unavailable, never a wait.
|
|
12179
|
+
return (await upProveSaved(gitRun, input))?.root ?? null;
|
|
12105
12180
|
};
|
|
12106
12181
|
// The proved set this worker builds; `up` relays each change to the console.
|
|
12107
12182
|
const activeRepos = new Set(config.repos);
|
|
@@ -12286,8 +12361,17 @@ async function runUpWorkerRole(context, config, port, build) {
|
|
|
12286
12361
|
await pause(follower === null ? 2_000 : 5_000);
|
|
12287
12362
|
}
|
|
12288
12363
|
})();
|
|
12289
|
-
|
|
12364
|
+
// The start's own projects and folders were resolved by `up`: taken as they are, never read again on this thread.
|
|
12365
|
+
const dynamicCeiling = resolveCeiling(config.repos, config.projectRoots, path => resolve(path)).ceiling;
|
|
12290
12366
|
const rejected = new Set();
|
|
12367
|
+
// Saved lines' identities, resolved off the thread under a deadline (project-identity.ts): a line whose folder
|
|
12368
|
+
// doesn't answer is skipped this pass, and never holds up the others.
|
|
12369
|
+
const lineIdentities = projectIdentities();
|
|
12370
|
+
/** The saved line(s) each active project came from: `up`'s proof for the start's own, the line admitted for later ones. */
|
|
12371
|
+
const admittedLines = new Map(Object.entries(config.repoLines ?? {}));
|
|
12372
|
+
const deferred = upDeferredRetry(config.deferredRepos ?? [], input => upProveSaved(gitRun, input));
|
|
12373
|
+
const deferredGenerations = config.deferredGenerations ?? {};
|
|
12374
|
+
const reportDeferred = () => port.send({ type: "deferred", repos: deferred.list() });
|
|
12291
12375
|
const registrySupervisor = (async () => {
|
|
12292
12376
|
const sleep = (ms) => new Promise(resolveSleep => setTimeout(resolveSleep, ms));
|
|
12293
12377
|
while (!stopping) {
|
|
@@ -12297,11 +12381,19 @@ async function runUpWorkerRole(context, config, port, build) {
|
|
|
12297
12381
|
stop();
|
|
12298
12382
|
break;
|
|
12299
12383
|
}
|
|
12384
|
+
// A saved line removed while this start runs takes the start's authority for it with it, for good.
|
|
12385
|
+
if (deferred.keepOnly(loaded.repos))
|
|
12386
|
+
reportDeferred();
|
|
12387
|
+
const plain = loaded.repos.filter(one => !deferred.has(one));
|
|
12388
|
+
await lineIdentities.refresh(plain);
|
|
12300
12389
|
// Removed from the saved list: stop building it and take it out of the console. A pinned start keeps its own.
|
|
12301
|
-
|
|
12390
|
+
// Each project is judged by its own saved line (the one it was proved or admitted from, or itself), never by
|
|
12391
|
+
// whether some other folder answered: an unrelated folder that is away never keeps a removed project building.
|
|
12392
|
+
const saved = new Set(loaded.repos);
|
|
12302
12393
|
let removed = false;
|
|
12303
12394
|
for (const repo of [...activeRepos]) {
|
|
12304
|
-
|
|
12395
|
+
const lines = admittedLines.get(repo) ?? [repo];
|
|
12396
|
+
if (stopping || pinnedRepos.has(repo) || saved.has(repo) || lines.some(line => saved.has(line)))
|
|
12305
12397
|
continue;
|
|
12306
12398
|
activeRepos.delete(repo);
|
|
12307
12399
|
removed = true;
|
|
@@ -12314,12 +12406,23 @@ async function runUpWorkerRole(context, config, port, build) {
|
|
|
12314
12406
|
for (const candidate of loaded.repos) {
|
|
12315
12407
|
if (stopping)
|
|
12316
12408
|
break;
|
|
12317
|
-
|
|
12409
|
+
// Skipped at start because it didn't answer: connected on the start's own authority once it does.
|
|
12410
|
+
if (deferred.has(candidate)) {
|
|
12411
|
+
await connectDeferred(candidate);
|
|
12412
|
+
continue;
|
|
12413
|
+
}
|
|
12414
|
+
const canonical = lineIdentities.known(candidate);
|
|
12415
|
+
// Not answered yet: looked at again on the next pass.
|
|
12416
|
+
if (canonical === undefined)
|
|
12417
|
+
continue;
|
|
12318
12418
|
// Still letting go of an earlier removal: it rejoins on the next pass after its watch has stopped.
|
|
12319
12419
|
if (canonical !== null && (activeRepos.has(canonical) || releasedRepos.has(canonical)))
|
|
12320
12420
|
continue;
|
|
12321
12421
|
const approvedAdditions = context.additionalProjectRepos?.() ?? [];
|
|
12322
|
-
|
|
12422
|
+
// A start serving nothing yet (its saved projects all away) is an empty ceiling, never the unscoped mode: a
|
|
12423
|
+
// name that matches no path stands in, so a line saved later still needs its recorded admission.
|
|
12424
|
+
const admittedRepos = [...dynamicCeiling.repos, ...approvedAdditions];
|
|
12425
|
+
const admittedCeiling = { repos: admittedRepos.length === 0 && dynamicCeiling.roots.length === 0 ? ["/\0nothing-admitted"] : admittedRepos, roots: dynamicCeiling.roots, canon: (path) => lineIdentities.known(path) ?? null };
|
|
12323
12426
|
// A project added after this start (`repos add`, the lead, the console) carries its recorded admission; a
|
|
12324
12427
|
// registry line alone is not authority. Either way the folder is proved to be that Git checkout again.
|
|
12325
12428
|
const allowed = canonical !== null &&
|
|
@@ -12333,6 +12436,7 @@ async function runUpWorkerRole(context, config, port, build) {
|
|
|
12333
12436
|
continue;
|
|
12334
12437
|
}
|
|
12335
12438
|
rejected.delete(candidate);
|
|
12439
|
+
admittedLines.set(canonical, [...new Set([...admittedLines.get(canonical) ?? [], candidate])]);
|
|
12336
12440
|
await launchRepo(canonical, true);
|
|
12337
12441
|
}
|
|
12338
12442
|
if (!stopping)
|
|
@@ -12342,6 +12446,61 @@ async function runUpWorkerRole(context, config, port, build) {
|
|
|
12342
12446
|
fatal = `the project registry stopped (${describe(error)})`;
|
|
12343
12447
|
stop();
|
|
12344
12448
|
});
|
|
12449
|
+
/**
|
|
12450
|
+
* A saved project the start couldn't reach, back now: connected only as the very folder that was saved (a parent
|
|
12451
|
+
* checkout found because the saved one lost its .git is another project, needing its own admission), and only while
|
|
12452
|
+
* the line is still saved, checked under the saved list's lock with the binding made inside it, so a removal racing
|
|
12453
|
+
* the proof always wins.
|
|
12454
|
+
*/
|
|
12455
|
+
async function connectDeferred(candidate) {
|
|
12456
|
+
const proved = await deferred.retry(candidate);
|
|
12457
|
+
if (proved === null || stopping)
|
|
12458
|
+
return;
|
|
12459
|
+
// Only the very folder that was saved: the saved line must still resolve to itself, and git's checkout must be that
|
|
12460
|
+
// folder. A parent checkout, or the saved path now leading somewhere else, is another project needing admission.
|
|
12461
|
+
if (proved.line !== candidate || proved.root !== candidate) {
|
|
12462
|
+
deferred.revoke(candidate);
|
|
12463
|
+
reportDeferred();
|
|
12464
|
+
progress(proved.root !== proved.line
|
|
12465
|
+
? `saved project not connected — ${candidate} came back as part of ${proved.root}; add it again to connect it`
|
|
12466
|
+
: `saved project not connected — ${candidate} now leads to ${proved.line}; add it again to connect it`);
|
|
12467
|
+
return;
|
|
12468
|
+
}
|
|
12469
|
+
const root = candidate;
|
|
12470
|
+
if (activeRepos.has(root) || releasedRepos.has(root)) {
|
|
12471
|
+
deferred.revoke(candidate);
|
|
12472
|
+
reportDeferred();
|
|
12473
|
+
return;
|
|
12474
|
+
}
|
|
12475
|
+
// Bound under the saved list's lock: the line is still saved AND has not been removed since this start read it
|
|
12476
|
+
// (its removal count unchanged, so a removal and a save again during the proof still counts as removed).
|
|
12477
|
+
let launching = null;
|
|
12478
|
+
let removedSince = false;
|
|
12479
|
+
await updateProjectRegistry(registryPath, current => {
|
|
12480
|
+
if (stopping || !deferred.has(candidate))
|
|
12481
|
+
return current;
|
|
12482
|
+
const count = registryRemovals(registryPath)?.[candidate] ?? (registryRemovals(registryPath) === null ? -2 : 0);
|
|
12483
|
+
if (!current.repos.includes(candidate) || count !== deferredGenerations[candidate]) {
|
|
12484
|
+
deferred.revoke(candidate);
|
|
12485
|
+
removedSince = true;
|
|
12486
|
+
return current;
|
|
12487
|
+
}
|
|
12488
|
+
deferred.revoke(candidate);
|
|
12489
|
+
admittedLines.set(root, [candidate]);
|
|
12490
|
+
launching = launchRepo(root, true);
|
|
12491
|
+
return current;
|
|
12492
|
+
});
|
|
12493
|
+
if (removedSince) {
|
|
12494
|
+
reportDeferred();
|
|
12495
|
+
progress(`saved project not connected — ${candidate} was removed while it was away; add it again to connect it`);
|
|
12496
|
+
return;
|
|
12497
|
+
}
|
|
12498
|
+
if (launching === null)
|
|
12499
|
+
return;
|
|
12500
|
+
reportDeferred();
|
|
12501
|
+
progress(`saved project is back — connecting ${root}`);
|
|
12502
|
+
await launching;
|
|
12503
|
+
}
|
|
12345
12504
|
// Provider readiness (v47): this machine's non-spending observations, recorded under its own runner name ONCE at
|
|
12346
12505
|
// start — so every route projection can say ready / unavailable / unknown per provider, and the dispatch gate can
|
|
12347
12506
|
// halt a leg this machine cannot run. Production only: a test's injected agent runner is a fake, and its readiness
|
|
@@ -12530,3 +12689,72 @@ async function evalsAbandonCommand(flags, context) {
|
|
|
12530
12689
|
return fail(write, json, command, ended.reason, ended.message, EXIT.refused);
|
|
12531
12690
|
return succeed(write, json, command, { campaign: id, state: "incomplete", attempts: ended.attempts, spending: "unknown" }, () => [`Campaign ${id} ended incomplete. ${ended.attempts} unfinished attempt${ended.attempts === 1 ? "" : "s"} recorded as errors; what it spent is unknown. Nothing was stopped: check for its processes yourself.`]);
|
|
12532
12691
|
}
|
|
12692
|
+
/**
|
|
12693
|
+
* The saved projects an `up` start couldn't reach (upCommand): each is proved again every `everyMs` while the worker
|
|
12694
|
+
* runs, by `prove` (upProveSaved), so a folder that answers again connects without a restart. Only what the start itself
|
|
12695
|
+
* read from the saved list is here, and the set only shrinks: a line connected, removed from the saved list
|
|
12696
|
+
* (`keepOnly`) or come back as some other checkout (`revoke`) is gone for good, so a line saved again later needs its
|
|
12697
|
+
* recorded admission like any other addition. `retry` answers the proof when one is due and answered, else null.
|
|
12698
|
+
*/
|
|
12699
|
+
export function upDeferredRetry(inputs, prove, everyMs = 5_000, now = Date.now) {
|
|
12700
|
+
// Wall time, not the store's clock (a test's may stand still): this is only how often a folder is asked.
|
|
12701
|
+
const nextAt = new Map(inputs.map(input => [input, now() + everyMs]));
|
|
12702
|
+
return {
|
|
12703
|
+
has: input => nextAt.has(input),
|
|
12704
|
+
list: () => [...nextAt.keys()],
|
|
12705
|
+
retry: async (input) => {
|
|
12706
|
+
const due = nextAt.get(input);
|
|
12707
|
+
if (due === undefined || now() < due)
|
|
12708
|
+
return null;
|
|
12709
|
+
nextAt.set(input, now() + everyMs);
|
|
12710
|
+
return prove(input);
|
|
12711
|
+
},
|
|
12712
|
+
revoke: input => { nextAt.delete(input); },
|
|
12713
|
+
keepOnly: saved => {
|
|
12714
|
+
const keep = new Set(saved);
|
|
12715
|
+
let changed = false;
|
|
12716
|
+
for (const input of [...nextAt.keys()])
|
|
12717
|
+
if (!keep.has(input)) {
|
|
12718
|
+
nextAt.delete(input);
|
|
12719
|
+
changed = true;
|
|
12720
|
+
}
|
|
12721
|
+
return changed;
|
|
12722
|
+
},
|
|
12723
|
+
};
|
|
12724
|
+
}
|
|
12725
|
+
/**
|
|
12726
|
+
* Whether a saved project's folder is a Git checkout now, read without blocking this thread: `git rev-parse
|
|
12727
|
+
* --show-toplevel` (bounded, its own process), then the real paths of the saved line and of the checkout, resolved off
|
|
12728
|
+
* the thread under a deadline (folder-look.ts). Null when it isn't one, or didn't answer.
|
|
12729
|
+
*/
|
|
12730
|
+
export async function upProveSaved(gitRun, input, look = {}) {
|
|
12731
|
+
const top = await gitRun("git", ["rev-parse", "--show-toplevel"], { cwd: resolve(input) });
|
|
12732
|
+
if (top.code !== 0)
|
|
12733
|
+
return null;
|
|
12734
|
+
const answered = await lookIdentities([resolve(input), top.stdout.trim()], look);
|
|
12735
|
+
if (!answered.ok)
|
|
12736
|
+
return null;
|
|
12737
|
+
const line = answered.value.get(resolve(input)) ?? null, root = answered.value.get(top.stdout.trim()) ?? null;
|
|
12738
|
+
return line === null || root === null ? null : { line, root };
|
|
12739
|
+
}
|
|
12740
|
+
/** What `up` hands its next worker after the worker reports which saved projects it still holds start authority for:
|
|
12741
|
+
* only what both still hold. A worker's report can give up an authority, never add or give back one. */
|
|
12742
|
+
export function upNarrowDeferred(held, reported) {
|
|
12743
|
+
const kept = new Set(reported);
|
|
12744
|
+
return held.filter(one => kept.has(one));
|
|
12745
|
+
}
|
|
12746
|
+
/** Resolve the console's project identities after it is listening, never before: the deploy's health check and every
|
|
12747
|
+
* page answer at once, and each project shows up as its folder answers (project-identity.ts `refresh`, its looks a
|
|
12748
|
+
* few at a time under their deadline). Nothing waits on it; a pass that fails leaves those paths unresolved, which
|
|
12749
|
+
* fails closed, for the background pass to try again. */
|
|
12750
|
+
export function consoleIdentitiesInBackground(identities, paths) {
|
|
12751
|
+
void identities.refresh(paths).catch(() => false);
|
|
12752
|
+
}
|
|
12753
|
+
/** `config set` and `config clear` on the command line, through Settings → Models' one write path (model-choices.ts,
|
|
12754
|
+
* docs/plans/spend-dashboard.md slice 3), under the same target and resulting-configuration checks as the editor: the
|
|
12755
|
+
* person proved themselves with their own password or login at `proved.generation`; a machine credential (a remote
|
|
12756
|
+
* token, the lead's token) never changes which models run. The write re-proves them inside its transaction: still an
|
|
12757
|
+
* unrestricted instance operator, at that very generation, by a proof that still stands. */
|
|
12758
|
+
export function writeModelRow(store, proved, scope, knownProject, slot, to, now, extra = []) {
|
|
12759
|
+
return applyModelChoices(store, { scope, change: slot === null ? [] : [{ slot, to }], expected: null, strict: false, knownProject, extra }, { ...proved, live: () => true }, now);
|
|
12760
|
+
}
|
|
@@ -21,7 +21,7 @@ export type AdmissionActor = {
|
|
|
21
21
|
};
|
|
22
22
|
export type AdmissionFailure = {
|
|
23
23
|
ok: false;
|
|
24
|
-
reason: "unavailable" | "not-git" | "locked" | "registry" | "abandoned" | "failed";
|
|
24
|
+
reason: "unavailable" | "not-git" | "locked" | "registry" | "abandoned" | "failed" | "moved";
|
|
25
25
|
message: string;
|
|
26
26
|
};
|
|
27
27
|
/** `remote`: the origin URL this add recorded as the project's trusted remote (null: none), and why one wasn't. */
|
|
@@ -60,6 +60,9 @@ export declare function admitProject(store: Store, input: {
|
|
|
60
60
|
captureOrigin?: boolean;
|
|
61
61
|
/** Explicit value from the owner/instance-operator CLI boundary. */
|
|
62
62
|
remoteUrl?: string;
|
|
63
|
+
/** The caller's authority, asked again about the proved repo inside the locked registry update and inside the
|
|
64
|
+
* database transaction (the console: nothing it proved before an await moved, and the repo is still visible). */
|
|
65
|
+
stillAllowed?: (repo: string) => boolean;
|
|
63
66
|
}): Promise<Admitted | AdmissionFailure>;
|
|
64
67
|
/**
|
|
65
68
|
* Remove one project from the lists and the builder. Its tasks, results and
|
|
@@ -61,13 +61,29 @@ export async function admitProject(store, input) {
|
|
|
61
61
|
? await originOf(repo, input.exec ?? run) : null;
|
|
62
62
|
let remote = { url: store.projectRemote(repo), why: null };
|
|
63
63
|
let registryHad = true;
|
|
64
|
+
const allowed = () => input.stillAllowed === undefined || input.stillAllowed(repo);
|
|
65
|
+
const moved = { ok: false, reason: "moved", message: `${projectName(repo)} wasn't added: a project folder changed while it was being checked. Try again.` };
|
|
64
66
|
if (input.registryFile !== null) {
|
|
65
|
-
|
|
67
|
+
let refused = false;
|
|
68
|
+
let had = false;
|
|
69
|
+
const enrolled = await updateRepos(input.registryFile, repos => {
|
|
70
|
+
had = repos.includes(repo);
|
|
71
|
+
if (!allowed()) {
|
|
72
|
+
refused = true;
|
|
73
|
+
return repos;
|
|
74
|
+
}
|
|
75
|
+
return addRepos(repos, [repo]);
|
|
76
|
+
});
|
|
66
77
|
if (!enrolled.ok)
|
|
67
78
|
return { ok: false, reason: enrolled.reason, message: `${projectName(repo)} wasn't added: ${enrolled.message}` };
|
|
68
|
-
|
|
79
|
+
if (refused)
|
|
80
|
+
return moved;
|
|
81
|
+
registryHad = had;
|
|
69
82
|
}
|
|
70
83
|
const added = store.transact(() => {
|
|
84
|
+
// Re-asked with the write; a refusal writes nothing here and takes back the registry line this call added below.
|
|
85
|
+
if (!allowed())
|
|
86
|
+
return null;
|
|
71
87
|
const rowHad = store.listProjects().some(one => one.path === repo);
|
|
72
88
|
const previous = store.projectRemote(repo);
|
|
73
89
|
const first = !known && !store.projectAdmissionKnown(repo);
|
|
@@ -81,6 +97,11 @@ export async function admitProject(store, input) {
|
|
|
81
97
|
store.recordAction({ at: input.now.toISOString(), actor: input.actor.label, repo, taskId: null, runId: null, action: PROJECT_ADDED, outcome: "added", source: "access", detail: `from ${input.actor.origin}` });
|
|
82
98
|
return true;
|
|
83
99
|
});
|
|
100
|
+
if (added === null) {
|
|
101
|
+
if (input.registryFile !== null && !registryHad)
|
|
102
|
+
await updateRepos(input.registryFile, repos => removeRepos(repos, [repo]));
|
|
103
|
+
return moved;
|
|
104
|
+
}
|
|
84
105
|
return { ok: true, repo, name: projectName(repo), added, remote };
|
|
85
106
|
}
|
|
86
107
|
/**
|