toolroll 0.9.97 → 0.9.99
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 +15 -0
- package/dist/approval-authority.d.ts +10 -1
- package/dist/approval-authority.js +18 -2
- package/dist/browser/workspace.js +1 -1
- package/dist/browser-workspace.d.ts +4 -3
- package/dist/chat-actions.d.ts +40 -5
- package/dist/chat-actions.js +124 -13
- package/dist/chat-card.js +4 -1
- package/dist/chat-channel.d.ts +3 -0
- package/dist/chat-channel.js +24 -7
- package/dist/chat-decide.js +4 -2
- package/dist/chat-task-actions.js +1 -1
- package/dist/chat-terms.d.ts +8 -0
- package/dist/chat-terms.js +14 -0
- package/dist/chat-voice.js +1 -1
- package/dist/checkout-cleanup.js +2 -1
- package/dist/contracts/chat-actions.d.ts +6 -0
- package/dist/contracts/chat-actions.js +2 -0
- package/dist/contracts/cli.d.ts +6 -0
- package/dist/contracts/cli.js +3 -0
- package/dist/contracts/lead-tools.d.ts +17 -0
- package/dist/contracts/lead-tools.js +4 -1
- package/dist/contracts/store-json.d.ts +1 -0
- package/dist/deploy-phase-data.json +21 -0
- package/dist/deploy-phase.d.ts +37 -0
- package/dist/deploy-phase.js +64 -0
- package/dist/deploy-rescue.d.ts +2 -1
- package/dist/deploy-rescue.js +6 -7
- package/dist/desktop-update.d.ts +2 -2
- package/dist/fast-start.d.ts +83 -6
- package/dist/fast-start.js +112 -15
- package/dist/home-groups.d.ts +39 -0
- package/dist/home-groups.js +33 -0
- package/dist/lead-answer-eval.d.ts +7 -0
- package/dist/lead-answer-eval.js +6 -2
- package/dist/lead-capabilities.js +1 -1
- package/dist/lead-context.js +2 -2
- package/dist/lead-contract.d.ts +1 -1
- package/dist/lead-contract.js +10 -7
- package/dist/lead-doors.d.ts +32 -0
- package/dist/lead-doors.js +80 -14
- package/dist/lead-status.d.ts +3 -0
- package/dist/lead-status.js +8 -3
- package/dist/lead-tools.d.ts +20 -0
- package/dist/lead-tools.js +66 -3
- package/dist/lead-voice.js +1 -1
- package/dist/lead.js +18 -2
- package/dist/local-owner.js +2 -0
- package/dist/one-step-chat-schema.d.ts +22 -0
- package/dist/one-step-chat-schema.js +37 -0
- package/dist/operate.d.ts +27 -1
- package/dist/operate.js +206 -32
- package/dist/project-delete.js +9 -0
- package/dist/project-knowledge.d.ts +7 -7
- package/dist/publish.d.ts +2 -0
- package/dist/publish.js +1 -1
- package/dist/pull-request-flow.d.ts +4 -0
- package/dist/pull-request-flow.js +16 -1
- package/dist/release-adapters.js +30 -14
- package/dist/release-gate.d.ts +3 -3
- package/dist/release-gate.js +7 -4
- package/dist/release-next.d.ts +3 -3
- package/dist/release-next.js +13 -16
- package/dist/reply-voice.d.ts +23 -0
- package/dist/reply-voice.js +37 -0
- package/dist/scope.d.ts +6 -1
- package/dist/scope.js +3 -2
- package/dist/server/chat.js +4 -4
- package/dist/server/chrome.js +2 -2
- package/dist/server/home.d.ts +4 -3
- package/dist/server/home.js +33 -28
- package/dist/server/render-chat.js +11 -6
- package/dist/server/render-pages.js +2 -2
- package/dist/setup-guide.d.ts +2 -2
- package/dist/staged-runtimes.js +2 -1
- package/dist/store.d.ts +4 -2
- package/dist/store.js +35 -12
- package/dist/telegram-lead.js +69 -5
- package/dist/telegram-status.js +9 -4
- package/dist/telegram.d.ts +10 -0
- package/dist/telegram.js +92 -8
- package/dist/toolroll-update.js +3 -1
- package/dist/tui-contract.d.ts +2 -0
- package/dist/tui-source.js +3 -0
- package/dist/tui.js +41 -41
- package/dist/typed-start.d.ts +51 -0
- package/dist/typed-start.js +98 -0
- package/dist/wake-hub.d.ts +72 -0
- package/dist/wake-hub.js +151 -0
- package/package.json +1 -1
package/dist/project-delete.js
CHANGED
|
@@ -239,6 +239,15 @@ function deleteRows(store, repo, d, now) {
|
|
|
239
239
|
del("chat_decide_action", IN("task_id"), K);
|
|
240
240
|
del("chat_decide_prompt", IN("task_id"), K);
|
|
241
241
|
del("chat_message_ref", `${IN("decision")} OR ${IN("task_id")} OR ${IN("run")} OR project = ? OR ${IN("notification")} OR ${IN("task_ref")}`, decisions, K, R, repo, notifications, T);
|
|
242
|
+
// A typed yes to a Start card (one-step-chat.md C5). Its record goes with its message. Where the message stays (it
|
|
243
|
+
// named no task of this project) but the card's proposal goes, the record is settled instead and forgets the task: a
|
|
244
|
+
// yes still waiting is then answered by Toolroll as one that started nothing, never handed to the lead.
|
|
245
|
+
del("chat_typed_start", `provider || ':' || event IN (SELECT value FROM json_each(?))`, conversations);
|
|
246
|
+
if (tableExists(db, "chat_typed_start")) {
|
|
247
|
+
db.prepare(`UPDATE chat_typed_start SET outcome = 'not-counted', task_id = NULL, finished = COALESCE(finished, ?) WHERE ${IN("proposal")}`).run(now.toISOString(), proposals);
|
|
248
|
+
// A yes that started one of this project's tasks, said in a message that stays: the record forgets the task.
|
|
249
|
+
db.prepare(`UPDATE chat_typed_start SET task_id = NULL WHERE ${IN("task_id")}`).run(K);
|
|
250
|
+
}
|
|
242
251
|
del("chat_part", `provider || ':' || id IN (${parts})`, conversations, K, R);
|
|
243
252
|
del("chat_event", `provider || ':' || id IN (SELECT value FROM json_each(?))`, conversations);
|
|
244
253
|
del("chat_card_task", IN("task_ref"), T);
|
|
@@ -22,17 +22,20 @@ export type KnowledgeView = ReturnType<typeof knowledgeView>;
|
|
|
22
22
|
export declare function knowledgeVersion(store: Store, repo: string, actor: string, revision: number): Knowledge;
|
|
23
23
|
/** Chat reads only an admitted project. Keep the full source in the project UI. */
|
|
24
24
|
export declare function conversationKnowledge(store: Store, repo: string, actor: string, reference?: string, decision?: number): {
|
|
25
|
+
content?: never;
|
|
26
|
+
history?: never;
|
|
25
27
|
revision: number;
|
|
26
28
|
stale: boolean;
|
|
27
29
|
notice: string;
|
|
28
30
|
title?: never;
|
|
29
|
-
content?: never;
|
|
30
|
-
history?: never;
|
|
31
31
|
instructions?: never;
|
|
32
32
|
references?: never;
|
|
33
33
|
decisions?: never;
|
|
34
34
|
waiting?: never;
|
|
35
35
|
} | {
|
|
36
|
+
content?: never;
|
|
37
|
+
revision?: never;
|
|
38
|
+
history?: never;
|
|
36
39
|
stale?: never;
|
|
37
40
|
decision: {
|
|
38
41
|
repo: string;
|
|
@@ -51,28 +54,25 @@ export declare function conversationKnowledge(store: Store, repo: string, actor:
|
|
|
51
54
|
afterwards?: string;
|
|
52
55
|
notice: string;
|
|
53
56
|
title?: never;
|
|
54
|
-
content?: never;
|
|
55
|
-
revision?: never;
|
|
56
|
-
history?: never;
|
|
57
57
|
instructions?: never;
|
|
58
58
|
references?: never;
|
|
59
59
|
decisions?: never;
|
|
60
60
|
waiting?: never;
|
|
61
61
|
} | {
|
|
62
|
+
history?: never;
|
|
62
63
|
stale?: never;
|
|
63
64
|
revision: number;
|
|
64
65
|
title: string;
|
|
65
66
|
content: string;
|
|
66
67
|
notice: string;
|
|
67
|
-
history?: never;
|
|
68
68
|
instructions?: never;
|
|
69
69
|
references?: never;
|
|
70
70
|
decisions?: never;
|
|
71
71
|
waiting?: never;
|
|
72
72
|
} | {
|
|
73
|
+
content?: never;
|
|
73
74
|
stale?: never;
|
|
74
75
|
title?: never;
|
|
75
|
-
content?: never;
|
|
76
76
|
revision: number;
|
|
77
77
|
history: {
|
|
78
78
|
revision: number;
|
package/dist/publish.d.ts
CHANGED
|
@@ -107,6 +107,8 @@ export type ObservedPullRequest = {
|
|
|
107
107
|
export declare function observeChecks(store: Store, options: {
|
|
108
108
|
clock?: () => Date;
|
|
109
109
|
exec?: PublishExec;
|
|
110
|
+
/** Read only these (the fast pass's waiting pull requests, live-wakeups.md); every one is read exactly as below. */
|
|
111
|
+
publications?: readonly Publication[];
|
|
110
112
|
}): Promise<CheckReport>;
|
|
111
113
|
export declare const MERGE_LEASE_MS: number;
|
|
112
114
|
export declare const FIRING_GRACE_MS: number;
|
package/dist/publish.js
CHANGED
|
@@ -304,7 +304,7 @@ export async function observeChecks(store, options) {
|
|
|
304
304
|
const clock = options.clock ?? (() => new Date());
|
|
305
305
|
const exec = options.exec ?? execRun;
|
|
306
306
|
const report = { observed: 0, failing: 0, resolved: 0, problems: [], seen: [] };
|
|
307
|
-
for (const publication of store.openedPublications()) {
|
|
307
|
+
for (const publication of options.publications ?? store.openedPublications()) {
|
|
308
308
|
if (publication.prNumber === null)
|
|
309
309
|
continue;
|
|
310
310
|
// The observation generation is RESERVED before the network call
|
|
@@ -129,6 +129,8 @@ export declare function followPullRequests(store: Store, seen: readonly Observed
|
|
|
129
129
|
evidenceRoot: string;
|
|
130
130
|
exec?: PublishExec;
|
|
131
131
|
clock?: () => Date;
|
|
132
|
+
/** Asked again before each revision, ready mark and merge: false ends the pass there and writes nothing more. */
|
|
133
|
+
proceed?: () => boolean;
|
|
132
134
|
}): Promise<FollowReport>;
|
|
133
135
|
/** A person's merge: their password first, then the same checked merge. */
|
|
134
136
|
export declare function mergeAsPerson(store: Store, input: {
|
|
@@ -156,6 +158,8 @@ export declare function mergePullRequest(store: Store, input: {
|
|
|
156
158
|
method?: MergeMethod;
|
|
157
159
|
/** Only a person merging by hand may skip waiting for the full check. */
|
|
158
160
|
anyway?: boolean;
|
|
161
|
+
/** An automatic pass's standing, read in the write that reserves the merge: false refuses ("halted") and merges nothing. */
|
|
162
|
+
proceed?: () => boolean;
|
|
159
163
|
}): Promise<Result<{
|
|
160
164
|
commit: string | null;
|
|
161
165
|
}>>;
|
|
@@ -285,7 +285,10 @@ export async function followPullRequests(store, seen, options) {
|
|
|
285
285
|
const exec = options.exec ?? execRun;
|
|
286
286
|
const clock = options.clock ?? (() => new Date());
|
|
287
287
|
const report = { revisions: 0, asked: 0, ready: 0, merged: 0, problems: [] };
|
|
288
|
+
const halted = () => options.proceed?.() === false;
|
|
288
289
|
for (const one of seen) {
|
|
290
|
+
if (halted())
|
|
291
|
+
break;
|
|
289
292
|
const publication = one.publication;
|
|
290
293
|
const follow = followOf(store, publication.id);
|
|
291
294
|
// A flow's Pull request zone follows its own PR: green moves the card on, red takes its failure path.
|
|
@@ -313,6 +316,8 @@ export async function followPullRequests(store, seen, options) {
|
|
|
313
316
|
const check = failingChecks(one.rollup)[0] ?? { name: "a check", url: null };
|
|
314
317
|
const skeleton = ciRevisionNote(check.name, pr, one.headOid, "");
|
|
315
318
|
const excerpt = await logExcerpt(exec, publication.githubRepo, check.url, 490 - skeleton.length);
|
|
319
|
+
if (halted())
|
|
320
|
+
break;
|
|
316
321
|
const note = ciRevisionNote(check.name, pr, one.headOid, excerpt);
|
|
317
322
|
const request = createHash("sha256").update(`ci-revision ${publication.githubRepo} ${pr} ${one.headOid}`).digest("hex").slice(0, 32);
|
|
318
323
|
const scope = store.getScope(taskId);
|
|
@@ -348,11 +353,17 @@ export async function followPullRequests(store, seen, options) {
|
|
|
348
353
|
const grant = store.publicationGrantFor(store.refById(publication.taskRef)?.repo ?? "");
|
|
349
354
|
updateFollow(store, publication.id, { ready_head: one.headOid }, clock());
|
|
350
355
|
if (grant?.mergeWhenGreen === true && grant.publishOn === "complete") {
|
|
351
|
-
const merged = await mergePullRequest(store, { runId: publication.run, by: "merge when checks pass", exec, clock
|
|
356
|
+
const merged = await mergePullRequest(store, { runId: publication.run, by: "merge when checks pass", exec, clock,
|
|
357
|
+
...(options.proceed === undefined ? {} : { proceed: options.proceed }) });
|
|
352
358
|
if (merged.ok) {
|
|
353
359
|
report.merged++;
|
|
354
360
|
continue;
|
|
355
361
|
}
|
|
362
|
+
// Halted before the merge was reserved: the ready mark is taken back so the next pass merges it, and nobody is asked to.
|
|
363
|
+
if (merged.reason === "halted") {
|
|
364
|
+
updateFollow(store, publication.id, { ready_head: follow.readyHead }, clock());
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
356
367
|
report.problems.push(`PR #${pr}: ${merged.message}`);
|
|
357
368
|
}
|
|
358
369
|
store.enqueueNotification({
|
|
@@ -485,6 +496,8 @@ export async function mergePullRequest(store, input) {
|
|
|
485
496
|
// One merge at a time, each owning its reservation by its token: a second caller is refused, and each releases only its own.
|
|
486
497
|
const token = randomUUID();
|
|
487
498
|
const reserved = store.transact(() => {
|
|
499
|
+
if (input.proceed?.() === false)
|
|
500
|
+
return "halted";
|
|
488
501
|
if (refArchived(store, publication.taskRef))
|
|
489
502
|
return "archived";
|
|
490
503
|
if (store.handle.prepare("SELECT 1 FROM hold WHERE task_ref = ? AND owner_id LIKE 'project-delete:%' AND (until IS NULL OR until > ?)").get(publication.taskRef, clock().toISOString()) !== undefined)
|
|
@@ -494,6 +507,8 @@ export async function mergePullRequest(store, input) {
|
|
|
494
507
|
store.handle.prepare("INSERT OR REPLACE INTO merge_in_flight (publication, task_ref, token, by, at) VALUES (?, ?, ?, ?, ?)").run(publication.id, publication.taskRef, token, input.by, clock().toISOString());
|
|
495
508
|
return "ok";
|
|
496
509
|
});
|
|
510
|
+
if (reserved === "halted")
|
|
511
|
+
return refuse("halted", "The worker is stopping or paused, so this merge waits for its next pass.");
|
|
497
512
|
if (reserved === "archived")
|
|
498
513
|
return refuse("archived", "This task is put away. Restore it before merging.");
|
|
499
514
|
if (reserved === "deleting")
|
package/dist/release-adapters.js
CHANGED
|
@@ -18,7 +18,8 @@ import { callLabel, readOnlyCall, retryCall, retryingExec, writeWithRecheck } fr
|
|
|
18
18
|
const sqlite = () => createRequire(import.meta.url)("node:sqlite");
|
|
19
19
|
import { PROTECTION_MINIMUM_CHECKS, PROTECTION_REPORT_ONLY_CHECKS } from "./release.js";
|
|
20
20
|
import { activeUpdateWork } from "./desktop-update-gate.js";
|
|
21
|
-
import { BEFORE_SWAP,
|
|
21
|
+
import { BEFORE_SWAP, deployActionOf, stoppedWhereNothingChanged } from "./deploy-phase.js";
|
|
22
|
+
import { gateOwner } from "./release-gate.js";
|
|
22
23
|
import { findCliLinks, switchCliLinks } from "./release-links.js";
|
|
23
24
|
import { databasePath, finishReleaseOperation, requireFreshReleaseApproval, openStore, releaseAuthorityOfTask, releaseCarries, releaseOperations, renewReleaseLease, reserveRelease, runReleaseOperation, Store, recordReleaseStep, releaseIssuerGone, takeReleaseLease, } from "./store.js";
|
|
24
25
|
import { acquireDeploymentLock } from "./deploy-lock.js";
|
|
@@ -62,8 +63,6 @@ const ownCli = () => {
|
|
|
62
63
|
const bin = join(dirname(fileURLToPath(import.meta.url)), "bin.js");
|
|
63
64
|
return existsSync(bin) ? [process.execPath, bin] : ["toolroll"];
|
|
64
65
|
};
|
|
65
|
-
/** Journal phases the deployment's finish accepts (scripts/deploy-phases.mjs finish): the new service runs, unproven. */
|
|
66
|
-
const FINISHES_FORWARD = Object.freeze(["started", "healthy"]);
|
|
67
66
|
const cleanEnv = (extra = {}) => {
|
|
68
67
|
const env = { ...process.env, ...extra };
|
|
69
68
|
delete env["NODE_OPTIONS"];
|
|
@@ -580,23 +579,40 @@ export function releaseAdapters(options) {
|
|
|
580
579
|
const running = (options.processes ?? processes)().filter(one => one.command.includes("deploy-browser.mjs") && one.command.includes(stage));
|
|
581
580
|
if (running.length > 0)
|
|
582
581
|
return { ok: false, message: `the deployment is still running (process ${running.map(one => one.pid).join(", ")}); let it finish, then rerun` };
|
|
583
|
-
// The new service started (deploy-
|
|
584
|
-
//
|
|
582
|
+
// The new service was started (the phase table's "forward" rows, deploy-phase.ts): the release resumes it
|
|
583
|
+
// forward with the deployment's own finish (health, both consoles, leases, then the pause lifted).
|
|
585
584
|
// Finishing deploys, so the candidate is proven first, as for a deploy.
|
|
586
|
-
const
|
|
585
|
+
const journalPhase = () => { try {
|
|
587
586
|
return object(JSON.parse(readFileSync(join(stage, "deployment.json"), "utf8")))["phase"];
|
|
588
587
|
}
|
|
589
588
|
catch {
|
|
590
589
|
return undefined;
|
|
591
|
-
} }
|
|
592
|
-
|
|
590
|
+
} };
|
|
591
|
+
const phase = journalPhase();
|
|
592
|
+
if (deployActionOf(phase) === "forward") {
|
|
593
593
|
const proven = proveCandidate(runId);
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
594
|
+
let why;
|
|
595
|
+
if (proven.ok) {
|
|
596
|
+
const finished = await deployScript(worktree, stage, runId, proven, ["--yes", "--phase", "finish"]);
|
|
597
|
+
if (finished.code === 0)
|
|
598
|
+
return { ok: true };
|
|
599
|
+
why = `${tail(finished)} (the deployment journal is at ${String(phase)}; finishing it forward stopped; ${join(stage, "deployment.json")})`;
|
|
600
|
+
}
|
|
601
|
+
else
|
|
602
|
+
why = proven.message;
|
|
603
|
+
// Once the new service was recorded as started, a journal alone never rolls it back: the release stops here.
|
|
604
|
+
// Still at "starting" (killed while waiting for the new service; finish only looks, and it did not find it
|
|
605
|
+
// running): the deployment's own recovery puts the previous service back, and only when it proves the new
|
|
606
|
+
// one gone. That needs no approval of the candidate, exactly as below.
|
|
607
|
+
if (journalPhase() !== "starting")
|
|
608
|
+
return stopped(stage, why);
|
|
609
|
+
const recorded = recordedCommit(runId);
|
|
610
|
+
if (recorded === null)
|
|
611
|
+
return stopped(stage, why);
|
|
612
|
+
const back = await deployScript(worktree, stage, runId, recorded, ["--phase", "recover"]);
|
|
613
|
+
if (back.code === 0)
|
|
598
614
|
return { ok: true };
|
|
599
|
-
return stopped(stage, `${
|
|
615
|
+
return stopped(stage, `${why} The previous service was not put back either: ${tail(back)}`);
|
|
600
616
|
}
|
|
601
617
|
// Recovery needs no approval, but it runs only the recorded commit's own script, extracted by this runtime.
|
|
602
618
|
const recorded = recordedCommit(runId);
|
|
@@ -642,7 +658,7 @@ export function releaseAdapters(options) {
|
|
|
642
658
|
catch {
|
|
643
659
|
return false;
|
|
644
660
|
}
|
|
645
|
-
return phase
|
|
661
|
+
return stoppedWhereNothingChanged(phase);
|
|
646
662
|
},
|
|
647
663
|
async cliLinks() { return findCliLinks(); },
|
|
648
664
|
async switchLinks(links, bin, version) {
|
package/dist/release-gate.d.ts
CHANGED
|
@@ -18,9 +18,10 @@
|
|
|
18
18
|
* Anything missing, damaged or contradicting refuses and leaves the pause as it is. Without --yes it only shows the
|
|
19
19
|
* proof; with --yes it lifts that one pause (the owner is checked again inside the same write) and journals it.
|
|
20
20
|
*/
|
|
21
|
+
import { BEFORE_SWAP } from "./deploy-phase.js";
|
|
21
22
|
import type { Database } from "./store.js";
|
|
22
|
-
/** The journal phases a deployment passes before it stops the old service (deploy-
|
|
23
|
-
export
|
|
23
|
+
/** The journal phases a deployment passes before it stops the old service (deploy-phase.ts, the one table). */
|
|
24
|
+
export { BEFORE_SWAP };
|
|
24
25
|
export type GateReleaseWorld = {
|
|
25
26
|
/** The live database file. */
|
|
26
27
|
database: string;
|
|
@@ -81,4 +82,3 @@ export type GateReleaseOutcome = {
|
|
|
81
82
|
};
|
|
82
83
|
/** Prove, then (with `yes`) lift exactly that pause and journal the release in the deployment's own journal. */
|
|
83
84
|
export declare function releaseGate(id: string, world: GateReleaseWorld, yes: boolean): GateReleaseOutcome;
|
|
84
|
-
export {};
|
package/dist/release-gate.js
CHANGED
|
@@ -23,8 +23,9 @@ import { join } from "node:path";
|
|
|
23
23
|
import { durableJson } from "./desktop-update.js";
|
|
24
24
|
import { removeUpdateGate, updateGateOwned } from "./desktop-update-gate.js";
|
|
25
25
|
import { acquireDeploymentLock } from "./deploy-lock.js";
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
import { BEFORE_SWAP, RELEASED } from "./deploy-phase.js";
|
|
27
|
+
/** The journal phases a deployment passes before it stops the old service (deploy-phase.ts, the one table). */
|
|
28
|
+
export { BEFORE_SWAP };
|
|
28
29
|
/** Journal fields that only the swap and later phases write. */
|
|
29
30
|
const SWAP_FIELDS = ["stoppingService", "codingBackupBeforeSwap", "migration", "newService", "deployedAt"];
|
|
30
31
|
const GATE_ID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
|
|
@@ -121,7 +122,9 @@ export function proveBeforeSwap(id, world) {
|
|
|
121
122
|
if (staged.length > 0)
|
|
122
123
|
return refuse("after-swap", `Process ${staged.map(one => one.pid).join(", ")} runs from the staged runtime ${r.nextRuntime}.`);
|
|
123
124
|
const supervisor = processes.find(one => one.command.includes(`${r.priorRuntime}/controller-service.js`));
|
|
124
|
-
|
|
125
|
+
// The database as a whole argument: an `up` on a path that only begins with this database's is another service's.
|
|
126
|
+
const upHere = `${r.priorRuntime}/cli.js up --db ${world.database}`;
|
|
127
|
+
const worker = processes.find(one => one.command.split(upHere).slice(1).some(rest => rest === "" || /^\s/.test(rest)));
|
|
125
128
|
if (supervisor === undefined || worker === undefined)
|
|
126
129
|
return refuse("conflict", `The service is not running from ${r.priorRuntime}, so it can't be shown that the swap never began.`);
|
|
127
130
|
proof.push(`The service runs from ${r.priorRuntime} (process ${supervisor.pid}); nothing runs from the staged runtime, and no deployment is running.`);
|
|
@@ -163,7 +166,7 @@ export function releaseGate(id, world, yes) {
|
|
|
163
166
|
// The owner is checked again within removeUpdateGate's short write transaction.
|
|
164
167
|
removeUpdateGate(db, id);
|
|
165
168
|
durableJson(journalFile, {
|
|
166
|
-
...fresh.journal, phase:
|
|
169
|
+
...fresh.journal, phase: RELEASED, updatedAt: world.now().toISOString(),
|
|
167
170
|
releasedBeforeSwap: { at: world.now().toISOString(), by: "toolroll release --release-gate", from: fresh.journal.phase, proof: fresh.proof },
|
|
168
171
|
});
|
|
169
172
|
return { ok: true, released: true, id, stage: fresh.stage, proof: fresh.proof };
|
package/dist/release-next.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { deployActionOf, type DeployAction } from "./deploy-phase.js";
|
|
1
2
|
import { type RescueWorld } from "./deploy-rescue.js";
|
|
2
3
|
import { type GateReleaseWorld } from "./release-gate.js";
|
|
3
4
|
import { type ReleaseJournal } from "./release.js";
|
|
@@ -29,9 +30,8 @@ export type SettleWorld = {
|
|
|
29
30
|
rescue: RescueWorld;
|
|
30
31
|
gate: GateReleaseWorld;
|
|
31
32
|
};
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
export declare function deployActionOf(phase: unknown): DeployAction;
|
|
33
|
+
/** The one action a deployment's journal calls for: a row of the one phase table (deploy-phase.ts). */
|
|
34
|
+
export { deployActionOf, type DeployAction };
|
|
35
35
|
/** Settle what a stopped deployment left, saying what was done. A refusal stops the release before it does anything. */
|
|
36
36
|
export declare function settleDeployment(world: SettleWorld, say: (line: string) => void): Promise<{
|
|
37
37
|
ok: true;
|
package/dist/release-next.js
CHANGED
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
import { closeSync, mkdirSync, openSync, readdirSync, readFileSync, rmSync, writeSync } from "node:fs";
|
|
12
12
|
import { hostname } from "node:os";
|
|
13
13
|
import { dirname, join } from "node:path";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
14
|
+
import { deployActionOf } from "./deploy-phase.js";
|
|
15
|
+
import { rescueDeployment } from "./deploy-rescue.js";
|
|
16
|
+
import { releaseGate } from "./release-gate.js";
|
|
16
17
|
import { journalFile, readJournal } from "./release.js";
|
|
17
18
|
import { autoPublishHeld, PUBLISH_ALLOWED_ACTION } from "./release-auto.js";
|
|
18
19
|
import { sameMachineHost } from "./machine-identity.js";
|
|
@@ -78,18 +79,8 @@ export async function takeReleaseRun(stateDir, branch, say, seams = {}) {
|
|
|
78
79
|
await sleep(seams.pollMs ?? 5_000);
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
|
-
/** The one action a deployment's journal calls for. */
|
|
82
|
-
export
|
|
83
|
-
if (typeof phase !== "string")
|
|
84
|
-
return "refuse";
|
|
85
|
-
if (phase === "released" || phase === "deployed")
|
|
86
|
-
return "none";
|
|
87
|
-
if (BEFORE_SWAP.includes(phase))
|
|
88
|
-
return "lift-pause";
|
|
89
|
-
if (FORWARD_ONLY.includes(phase))
|
|
90
|
-
return "forward";
|
|
91
|
-
return "roll-back";
|
|
92
|
-
}
|
|
82
|
+
/** The one action a deployment's journal calls for: a row of the one phase table (deploy-phase.ts). */
|
|
83
|
+
export { deployActionOf };
|
|
93
84
|
/** Settle what a stopped deployment left, saying what was done. A refusal stops the release before it does anything. */
|
|
94
85
|
export async function settleDeployment(world, say) {
|
|
95
86
|
const open = world.rescue.journals().filter(one => deployActionOf(one.journal?.["phase"]) !== "none");
|
|
@@ -99,8 +90,14 @@ export async function settleDeployment(world, say) {
|
|
|
99
90
|
return { ok: false, message: `${open.length} deployments are unfinished: ${open.map(one => one.stage).join(", ")}. Nothing was changed.` };
|
|
100
91
|
const { stage, journal } = open[0];
|
|
101
92
|
const action = deployActionOf(journal?.["phase"]);
|
|
102
|
-
if (action === "refuse")
|
|
103
|
-
|
|
93
|
+
if (action === "refuse") {
|
|
94
|
+
// A word this version's table does not hold (a newer candidate's, or damage) is never acted on by itself: the
|
|
95
|
+
// deployment's own recovery knows its own phases, and the person starts it.
|
|
96
|
+
const phase = journal?.["phase"];
|
|
97
|
+
return typeof phase === "string"
|
|
98
|
+
? { ok: false, message: `The deployment in ${stage} stopped at a step this version doesn't know (${/^[a-z-]{1,40}$/.test(phase) ? phase : "not a plain word"}). Nothing was changed. Its own recovery can decide: \`toolroll release --recover-deploy --yes\`.` }
|
|
99
|
+
: { ok: false, message: `The deployment journal in ${stage} can't be read. Nothing was changed.` };
|
|
100
|
+
}
|
|
104
101
|
if (action === "forward") {
|
|
105
102
|
say("Finishing the deployment that was under way.");
|
|
106
103
|
return { ok: true };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type ReplyVoice = {
|
|
2
|
+
/** Characters of the shaped reply. */
|
|
3
|
+
characters: number;
|
|
4
|
+
sentences: number;
|
|
5
|
+
/** The `PLAIN_WORDS` rows said outside code, plus "owner" when the reply calls the person "the owner". */
|
|
6
|
+
internalWords: string[];
|
|
7
|
+
/** Opens with its own steps or a filler word ("I checked", "Let me", "Sure,") instead of the answer. */
|
|
8
|
+
narratedOpening: boolean;
|
|
9
|
+
};
|
|
10
|
+
/** A reply longer than this is counted as long in the eval's report. */
|
|
11
|
+
export declare const LONG_REPLY_CHARACTERS = 320;
|
|
12
|
+
export declare function replyVoice(reply: string): ReplyVoice;
|
|
13
|
+
export type VoiceSummary = {
|
|
14
|
+
answers: number;
|
|
15
|
+
medianCharacters: number;
|
|
16
|
+
/** Share of answers over `LONG_REPLY_CHARACTERS`, from 0 to 1. */
|
|
17
|
+
longShare: number;
|
|
18
|
+
/** Internal words said, summed over the answers. */
|
|
19
|
+
internalWords: number;
|
|
20
|
+
narratedOpenings: number;
|
|
21
|
+
};
|
|
22
|
+
/** One model's answers, summed up for the report. */
|
|
23
|
+
export declare function summarizeVoice(voices: readonly ReplyVoice[]): VoiceSummary;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a lead reply reads (docs/plans/lead-voice.md, V3): its length, its sentences, the internal words it says and
|
|
3
|
+
* whether it opens by describing its own steps. Measured on the shaped text a person sees, never the raw reply.
|
|
4
|
+
* A measure for the lead-answer eval's report; nothing gates on it and no reply is rewritten from it.
|
|
5
|
+
*/
|
|
6
|
+
import { PLAIN_WORDS } from "./plain-words.js";
|
|
7
|
+
import { shapeReply } from "./reply-shape.js";
|
|
8
|
+
/** A reply longer than this is counted as long in the eval's report. */
|
|
9
|
+
export const LONG_REPLY_CHARACTERS = 320;
|
|
10
|
+
const CODE = /```[\s\S]*?(?:```|$)|`[^`\n]+`/g;
|
|
11
|
+
const OWNER_AS_ADDRESS = /(?<![\w-])the\s+owner(?![\w-])/i;
|
|
12
|
+
const NARRATED_OPENING = /^(?:i\s+checked|i\s+looked\s+at|let\s+me|certainly|sure,)/i;
|
|
13
|
+
export function replyVoice(reply) {
|
|
14
|
+
const shaped = shapeReply(reply);
|
|
15
|
+
const prose = shaped.replace(CODE, " ");
|
|
16
|
+
const internalWords = PLAIN_WORDS.filter(one => one.pattern.test(prose)).map(one => one.never);
|
|
17
|
+
if (OWNER_AS_ADDRESS.test(prose))
|
|
18
|
+
internalWords.push("owner");
|
|
19
|
+
return {
|
|
20
|
+
characters: shaped.length,
|
|
21
|
+
sentences: prose.split(/(?<=[.!?])\s+|\n+/).filter(part => /[\p{L}\p{N}]/u.test(part)).length,
|
|
22
|
+
internalWords,
|
|
23
|
+
narratedOpening: NARRATED_OPENING.test(shaped.trimStart()),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** One model's answers, summed up for the report. */
|
|
27
|
+
export function summarizeVoice(voices) {
|
|
28
|
+
const sorted = voices.map(one => one.characters).sort((a, b) => a - b);
|
|
29
|
+
const mid = Math.floor(sorted.length / 2);
|
|
30
|
+
return {
|
|
31
|
+
answers: voices.length,
|
|
32
|
+
medianCharacters: sorted.length === 0 ? 0 : sorted.length % 2 === 1 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2,
|
|
33
|
+
longShare: voices.length === 0 ? 0 : voices.filter(one => one.characters > LONG_REPLY_CHARACTERS).length / voices.length,
|
|
34
|
+
internalWords: voices.reduce((sum, one) => sum + one.internalWords.length, 0),
|
|
35
|
+
narratedOpenings: voices.filter(one => one.narratedOpening).length,
|
|
36
|
+
};
|
|
37
|
+
}
|
package/dist/scope.d.ts
CHANGED
|
@@ -796,10 +796,15 @@ export declare function fileAndSealUnderMode(store: Store, input: ScopeInput & {
|
|
|
796
796
|
detail?: string;
|
|
797
797
|
};
|
|
798
798
|
export declare function approve(store: Store, taskId: string, by: string, now: Date, sawDigest: string, token: string, mutation?: Mutation,
|
|
799
|
-
/** A fast start (fast-start.ts): Build is this same yes, with its marker in the proof. Only that road passes it.
|
|
799
|
+
/** A fast start (fast-start.ts): Build is this same yes, with its marker in the proof. Only that road passes it.
|
|
800
|
+
* `typed`: a typed yes to a Start card (one-step-chat.md C5), the message that was the yes, recorded beside the marker. */
|
|
800
801
|
fastStart?: {
|
|
801
802
|
rule: string;
|
|
802
803
|
at: string;
|
|
804
|
+
typed?: {
|
|
805
|
+
provider: string;
|
|
806
|
+
event: string;
|
|
807
|
+
};
|
|
803
808
|
},
|
|
804
809
|
/** A person's own yes in chat (owner's decision, docs/plans/toolroll-cards.md): their tap or signed-in click stands in
|
|
805
810
|
* for a password, so `token` is unused. Every rule below still applies. The caller has proved the person is the one
|
package/dist/scope.js
CHANGED
|
@@ -1075,7 +1075,8 @@ export function fileAndSealUnderMode(store, input) {
|
|
|
1075
1075
|
});
|
|
1076
1076
|
}
|
|
1077
1077
|
export function approve(store, taskId, by, now, sawDigest, token, mutation = {},
|
|
1078
|
-
/** A fast start (fast-start.ts): Build is this same yes, with its marker in the proof. Only that road passes it.
|
|
1078
|
+
/** A fast start (fast-start.ts): Build is this same yes, with its marker in the proof. Only that road passes it.
|
|
1079
|
+
* `typed`: a typed yes to a Start card (one-step-chat.md C5), the message that was the yes, recorded beside the marker. */
|
|
1079
1080
|
fastStart,
|
|
1080
1081
|
/** A person's own yes in chat (owner's decision, docs/plans/toolroll-cards.md): their tap or signed-in click stands in
|
|
1081
1082
|
* for a password, so `token` is unused. Every rule below still applies. The caller has proved the person is the one
|
|
@@ -1216,7 +1217,7 @@ export const APPROVAL_PROOF = "approval proved";
|
|
|
1216
1217
|
export const approvalProofDetail = (at, generation, digest, local) => `${new Date(at).toISOString()} · generation ${generation}${local === undefined ? "" : ` · local ${local.generation} ${local.transport}`} · digest ${digest}`;
|
|
1217
1218
|
/** A fast start's proof: the v1 record proofDetailOf reads, with the marker. `local` keeps the legacy words
|
|
1218
1219
|
* localSealStandsSql reads, so a local owner's seal stands on the same rule. */
|
|
1219
|
-
const fastProofDetail = (fastStart) => (at, generation, digest, local) => JSON.stringify({ v: 1, at: new Date(at).toISOString(), generation, digest, fastStart, ...(local === undefined ? {} : { local: `proof · local ${local.generation} ${local.transport} · seal` }) });
|
|
1220
|
+
const fastProofDetail = ({ typed, ...fastStart }) => (at, generation, digest, local) => JSON.stringify({ v: 1, at: new Date(at).toISOString(), generation, digest, fastStart, ...(typed === undefined ? {} : { typed: { provider: typed.provider, event: typed.event } }), ...(local === undefined ? {} : { local: `proof · local ${local.generation} ${local.transport} · seal` }) });
|
|
1220
1221
|
/** The proof of THIS approval: the approver's own row for these exact bytes (never another approval's words). */
|
|
1221
1222
|
export function approvalProofOf(store, taskId, by, digest) {
|
|
1222
1223
|
const rows = store.handle.prepare("SELECT outcome, detail FROM action_ledger WHERE task_id = ? AND action = ? AND actor = ? ORDER BY id DESC LIMIT 50").all(taskId, APPROVAL_PROOF, by);
|
package/dist/server/chat.js
CHANGED
|
@@ -780,7 +780,7 @@ export function createChatHandlers(runtime) {
|
|
|
780
780
|
const cardJson = String(request.headers["accept"] ?? "").includes("application/json") && !body.has("nonce");
|
|
781
781
|
const cardAnswer = (status, ok, said, taskId = null) => respond(response, status, "application/json", JSON.stringify({ ok, said, taskId }));
|
|
782
782
|
if (principal === null)
|
|
783
|
-
return cardJson ? cardAnswer(403, false, "Your
|
|
783
|
+
return cardJson ? cardAnswer(403, false, "Your sign-in changed. Sign in again.") : refuse(response, who, 403, "Your sign-in changed. Sign in again.", back);
|
|
784
784
|
if (op === "dismiss") {
|
|
785
785
|
const dismissed = dismissLeadProposal(store, principal, id, now);
|
|
786
786
|
if (cardJson)
|
|
@@ -849,7 +849,7 @@ export function createChatHandlers(runtime) {
|
|
|
849
849
|
return refuse(response, who, 403, "proposals are confirmed from the browser or the CLI");
|
|
850
850
|
const principal = leadPrincipal(who);
|
|
851
851
|
if (principal === null)
|
|
852
|
-
return refuse(response, who, 403, "
|
|
852
|
+
return refuse(response, who, 403, "Your sign-in changed. Sign in again.", "/chat");
|
|
853
853
|
const id = Number(url.pathname.split("/")[2]);
|
|
854
854
|
store.sweepCoordinatorProposals(now);
|
|
855
855
|
const back = body.get("return") === null ? "/chat" : safeReturn(body.get("return"));
|
|
@@ -919,7 +919,7 @@ export function createChatHandlers(runtime) {
|
|
|
919
919
|
if (who.role === "approver" && enabled.billing === "subscription" && unbound) {
|
|
920
920
|
const principal = leadPrincipal(who);
|
|
921
921
|
if (principal === null)
|
|
922
|
-
return wantsJson ? said(403, "
|
|
922
|
+
return wantsJson ? said(403, "Your sign-in changed. Sign in again.") : refuse(response, who, 403, "Your sign-in changed. Sign in again.", back);
|
|
923
923
|
const sentRequest = body.get("request");
|
|
924
924
|
if (sentRequest !== null && (body.getAll("request").length !== 1 || !/^[a-f0-9]{32}$/.test(sentRequest)))
|
|
925
925
|
return said(409, "This conversation changed. Reload it before sending your message.");
|
|
@@ -961,7 +961,7 @@ export function createChatHandlers(runtime) {
|
|
|
961
961
|
const requestId = sentRequest ?? randomBytes(16).toString("hex");
|
|
962
962
|
const principal = leadPrincipal(who);
|
|
963
963
|
if (principal === null)
|
|
964
|
-
return wantsJson ? said(403, "
|
|
964
|
+
return wantsJson ? said(403, "Your sign-in changed. Sign in again.") : refuse(response, who, 403, "Your sign-in changed. Sign in again.", back);
|
|
965
965
|
const message = (body.get("message") ?? "").trim();
|
|
966
966
|
if (message === "" || message.length > LEAD_MESSAGE_MAX_CHARS) {
|
|
967
967
|
return said(400, `a message is 1 to ${LEAD_MESSAGE_MAX_CHARS} characters`, leadSession.id);
|
package/dist/server/chrome.js
CHANGED
|
@@ -726,7 +726,7 @@ export function workToolRows(scoped = false, _offersChat = false, offersCode = f
|
|
|
726
726
|
{ key: "tasks", href: "/tasks", label: "Task list", hint: "everything, filterable by state" },
|
|
727
727
|
{ key: "recipes", href: "/recipes", label: "Recipes", hint: "choose, customize, and reuse a workflow" },
|
|
728
728
|
{ key: "workbench", href: "/tasks?scope=all", label: "Every project", hint: "every project's tasks in one list" },
|
|
729
|
-
{ key: "ledger", href: "/ledger", label: "
|
|
729
|
+
{ key: "ledger", href: "/ledger", label: "Audit log", hint: "who acted, what happened, and the result" },
|
|
730
730
|
{ key: "spend", href: "/spend", label: "Spend", hint: "what agent work cost, and monthly budgets" },
|
|
731
731
|
];
|
|
732
732
|
return scoped ? rows.filter(row => row.key !== "workbench" && row.key !== "spend") : rows;
|
|
@@ -737,7 +737,7 @@ export function settingsRows(scoped = false, offersSettings = false) {
|
|
|
737
737
|
{ key: "fleet", href: "/fleet", label: "Fleet", hint: "who is working, and on what" },
|
|
738
738
|
{ key: "caps", href: "/caps", label: "Requirements", hint: "tools and credentials builds need" },
|
|
739
739
|
{ key: "people", href: "/people", label: "People", hint: "who can sign in, and what they have done" },
|
|
740
|
-
{ key: "mode", href: "/mode", label: "Operating mode", hint: "
|
|
740
|
+
{ key: "mode", href: "/mode", label: "Operating mode", hint: "how much this project may do without asking" },
|
|
741
741
|
{ key: "system", href: "/system", label: "System", hint: "workers, providers, and grants" },
|
|
742
742
|
];
|
|
743
743
|
return scoped ? rows.filter(row => row.key === "people" || row.key === "settings") : rows;
|
package/dist/server/home.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Home ("/", UI revamp wave B): one sentence that answers "what needs me?", four counts read from the same work index
|
|
3
|
-
* the Tasks list filters (so a count and the list it opens always agree),
|
|
4
|
-
*
|
|
3
|
+
* the Tasks list filters (so a count and the list it opens always agree), the work in three groups (Waiting on you,
|
|
4
|
+
* Working, Done; home-groups.ts), and the lead's message box. Reads only: every act keeps its own route and rules (an agent's
|
|
5
5
|
* question is answered through POST /d/<id>/answer, the same one-click act as its own page).
|
|
6
6
|
*/
|
|
7
7
|
import type { BrowserHomeView } from "../browser-workspace.js";
|
|
@@ -37,5 +37,6 @@ export declare function homeSentence(needs: number, perProject: readonly {
|
|
|
37
37
|
name: string;
|
|
38
38
|
needs: number;
|
|
39
39
|
}[], scope: string | null, any: boolean): string;
|
|
40
|
-
/** Home's view for one person and scope, from
|
|
40
|
+
/** Home's view for one person and scope, from four reads of the work index (what waits on you, what runs, what waits on
|
|
41
|
+
* Toolroll, what was done), grouped by the one rule every surface shares (home-groups.ts). */
|
|
41
42
|
export declare function homeView(facts: HomeFacts): BrowserHomeView;
|