toolroll 0.9.65 → 0.9.67
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 +57 -0
- package/README.md +20 -2
- package/dist/agent-fence.d.ts +69 -2
- package/dist/agent-fence.js +291 -11
- package/dist/agentconfig.js +4 -1
- package/dist/batch-checks.js +3 -2
- package/dist/browser/THIRD_PARTY_NOTICES.txt +46 -0
- package/dist/browser/terminal.js +134 -23
- package/dist/browser/workspace.css +1 -1
- package/dist/builder.d.ts +4 -0
- package/dist/builder.js +54 -12
- package/dist/contracts/console-api.d.ts +1 -0
- package/dist/contracts/console-api.js +1 -0
- package/dist/invoke.d.ts +18 -0
- package/dist/invoke.js +137 -14
- package/dist/live-terminal.d.ts +24 -13
- package/dist/live-terminal.js +79 -42
- package/dist/operate.d.ts +2 -0
- package/dist/operate.js +60 -2
- package/dist/project-delete-ui.d.ts +6 -0
- package/dist/project-delete-ui.js +20 -1
- package/dist/release-adapters.d.ts +6 -0
- package/dist/release-adapters.js +70 -3
- package/dist/release.d.ts +7 -1
- package/dist/release.js +20 -7
- package/dist/result-follow-ups.js +3 -2
- package/dist/scope.d.ts +13 -0
- package/dist/scope.js +10 -5
- package/dist/serve.js +1 -0
- package/dist/server/chrome.d.ts +10 -1
- package/dist/server/chrome.js +45 -4
- package/dist/server/console.css +55 -4
- package/dist/server/render-pages.d.ts +28 -3
- package/dist/server/render-pages.js +78 -9
- package/dist/server/render-tasks.js +2 -1
- package/dist/server/route-table.js +1 -0
- package/dist/server/settings.js +21 -0
- package/dist/server/tasks.js +7 -4
- package/dist/store.d.ts +11 -1
- package/dist/store.js +33 -2
- package/dist/teardown-git.d.ts +22 -0
- package/dist/teardown-git.js +136 -2
- package/dist/tool-broker.d.ts +12 -0
- package/dist/tool-broker.js +66 -2
- package/dist/unfenced-test-support.d.ts +2 -0
- package/dist/unfenced-test-support.js +11 -0
- package/dist/writes-mode.d.ts +9 -0
- package/dist/writes-mode.js +8 -0
- package/package.json +5 -1
package/dist/scope.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
3
3
|
import { isAcpProfile, type AcpProviderId } from "./acp-agents.js";
|
|
4
4
|
export { isAcpProfile };
|
|
5
5
|
import { type NetworkProfile } from "./egress.js";
|
|
6
|
+
import { type WritesMode } from "./writes-mode.js";
|
|
6
7
|
import { type EvidenceKind } from "./contracts/acceptance-terms.js";
|
|
7
8
|
import { type AcceptanceCriterion, type AcceptanceProblem, type ScopeTerms } from "./contracts/scope.js";
|
|
8
9
|
import type { Store, Mutation } from "./store.js";
|
|
@@ -74,6 +75,9 @@ export type ClaudeProfile = {
|
|
|
74
75
|
/** What the build may reach (egress.ts), resolved when the scope was written; absent on approvals sealed before
|
|
75
76
|
* network policies, which run narrowed (egress.ts legacyNetwork). */
|
|
76
77
|
network?: NetworkProfile;
|
|
78
|
+
/** Where the build may write (writes-mode.ts), resolved when the scope was written; absent on approvals sealed before
|
|
79
|
+
* write allowlists, which keep writing anywhere their fence allows. */
|
|
80
|
+
writes?: WritesMode;
|
|
77
81
|
};
|
|
78
82
|
export type CodexShapedProfile = {
|
|
79
83
|
provider: "codex" | "openrouter";
|
|
@@ -91,6 +95,9 @@ export type CodexShapedProfile = {
|
|
|
91
95
|
repairTimeoutSeconds: number;
|
|
92
96
|
repairModel: string;
|
|
93
97
|
network?: NetworkProfile;
|
|
98
|
+
/** Where the build may write (writes-mode.ts), resolved when the scope was written; absent on approvals sealed before
|
|
99
|
+
* write allowlists, which keep writing anywhere their fence allows. */
|
|
100
|
+
writes?: WritesMode;
|
|
94
101
|
};
|
|
95
102
|
export type GeminiProfile = {
|
|
96
103
|
provider: "gemini";
|
|
@@ -110,6 +117,9 @@ export type GeminiProfile = {
|
|
|
110
117
|
repairTimeoutSeconds: number;
|
|
111
118
|
repairModel: string;
|
|
112
119
|
network?: NetworkProfile;
|
|
120
|
+
/** Where the build may write (writes-mode.ts), resolved when the scope was written; absent on approvals sealed before
|
|
121
|
+
* write allowlists, which keep writing anywhere their fence allows. */
|
|
122
|
+
writes?: WritesMode;
|
|
113
123
|
};
|
|
114
124
|
/**
|
|
115
125
|
* An agent over the Agent Client Protocol (acp-agents.ts, docs/plans/acp-adapter.md).
|
|
@@ -130,6 +140,9 @@ export type AcpProfile = {
|
|
|
130
140
|
repairIdleTimeoutSeconds: number;
|
|
131
141
|
repairModel: string;
|
|
132
142
|
network?: NetworkProfile;
|
|
143
|
+
/** Where the build may write (writes-mode.ts), resolved when the scope was written; absent on approvals sealed before
|
|
144
|
+
* write allowlists, which keep writing anywhere their fence allows. */
|
|
145
|
+
writes?: WritesMode;
|
|
133
146
|
};
|
|
134
147
|
export type ExecutionProfile = ClaudeProfile | CodexShapedProfile | GeminiProfile | AcpProfile;
|
|
135
148
|
/** Whether a profile's clock is a no-progress window (an ACP profile has both clocks; its overall one is a deadline). */
|
package/dist/scope.js
CHANGED
|
@@ -39,6 +39,7 @@ export { isAcpProfile };
|
|
|
39
39
|
*/
|
|
40
40
|
import { createHash, randomBytes, scryptSync, timingSafeEqual } from "node:crypto";
|
|
41
41
|
import { networkFromValue, networkWords } from "./egress.js";
|
|
42
|
+
import { isWritesMode, writesWords } from "./writes-mode.js";
|
|
42
43
|
import { z } from "zod";
|
|
43
44
|
import {} from "./contracts/acceptance-terms.js";
|
|
44
45
|
import { parseContract } from "./contracts/contract.js";
|
|
@@ -362,9 +363,12 @@ export function profileFromJson(json) {
|
|
|
362
363
|
const network = p["network"] === undefined ? undefined : networkFromValue(p["network"]);
|
|
363
364
|
if (network === null)
|
|
364
365
|
return null;
|
|
365
|
-
const
|
|
366
|
+
const writes = p["writes"];
|
|
367
|
+
if (writes !== undefined && !isWritesMode(writes))
|
|
368
|
+
return null;
|
|
369
|
+
const withNetwork = { ...(network === undefined ? {} : { network }), ...(writes === undefined ? {} : { writes }) };
|
|
366
370
|
if (p["provider"] === "claude") {
|
|
367
|
-
if (exactKeys(p, [...PROVIDER_KEYS, "permissionArgv"], ["timeoutKind", "network"]) &&
|
|
371
|
+
if (exactKeys(p, [...PROVIDER_KEYS, "permissionArgv"], ["timeoutKind", "network", "writes"]) &&
|
|
368
372
|
str(p["model"]) &&
|
|
369
373
|
(p["permissionArgv"] === "auto" || p["permissionArgv"] === "acceptEdits" || p["permissionArgv"] === "bypassPermissions") &&
|
|
370
374
|
num(p["maxTurns"]) && num(p["repairMaxTurns"]) &&
|
|
@@ -386,7 +390,7 @@ export function profileFromJson(json) {
|
|
|
386
390
|
return null;
|
|
387
391
|
}
|
|
388
392
|
if (p["provider"] === "codex" || p["provider"] === "openrouter") {
|
|
389
|
-
if (exactKeys(p, [...PROVIDER_KEYS, "sandboxMode"], ["timeoutKind", "network"]) &&
|
|
393
|
+
if (exactKeys(p, [...PROVIDER_KEYS, "sandboxMode"], ["timeoutKind", "network", "writes"]) &&
|
|
390
394
|
str(p["model"]) &&
|
|
391
395
|
(p["sandboxMode"] === "workspace-write" || p["sandboxMode"] === "danger-full-access") &&
|
|
392
396
|
p["maxTurns"] === "unsupported" && p["repairMaxTurns"] === "unsupported" &&
|
|
@@ -408,7 +412,7 @@ export function profileFromJson(json) {
|
|
|
408
412
|
return null;
|
|
409
413
|
}
|
|
410
414
|
if (p["provider"] === "gemini") {
|
|
411
|
-
if (exactKeys(p, [...PROVIDER_KEYS, "approvalArgv"], ["timeoutKind", "network"]) &&
|
|
415
|
+
if (exactKeys(p, [...PROVIDER_KEYS, "approvalArgv"], ["timeoutKind", "network", "writes"]) &&
|
|
412
416
|
str(p["model"]) &&
|
|
413
417
|
(p["approvalArgv"] === "auto_edit" || p["approvalArgv"] === "yolo") &&
|
|
414
418
|
p["maxTurns"] === "unsupported" && p["repairMaxTurns"] === "unsupported" &&
|
|
@@ -430,7 +434,7 @@ export function profileFromJson(json) {
|
|
|
430
434
|
return null;
|
|
431
435
|
}
|
|
432
436
|
if (typeof p["provider"] === "string" && isAcpProvider(p["provider"])) {
|
|
433
|
-
if (exactKeys(p, [...PROVIDER_KEYS, "permission", "idleTimeoutSeconds", "repairIdleTimeoutSeconds"], ["network"]) &&
|
|
437
|
+
if (exactKeys(p, [...PROVIDER_KEYS, "permission", "idleTimeoutSeconds", "repairIdleTimeoutSeconds"], ["network", "writes"]) &&
|
|
434
438
|
str(p["model"]) &&
|
|
435
439
|
p["permission"] === "full" &&
|
|
436
440
|
p["maxTurns"] === "unsupported" && p["repairMaxTurns"] === "unsupported" &&
|
|
@@ -1278,6 +1282,7 @@ export function describeScope(scope, readiness = NO_READINESS) {
|
|
|
1278
1282
|
...scopeRouteWords(scope, readiness),
|
|
1279
1283
|
// What the build may reach (egress.ts), part of what the yes agrees to.
|
|
1280
1284
|
...(scope.profile === null || scope.profile === undefined ? [] : [` network ${networkWords(scope.profile.network ?? null).replace(/^Network: /, "")}`]),
|
|
1285
|
+
...(scope.profile === null || scope.profile === undefined ? [] : [` writes ${writesWords(scope.profile.writes)}`]),
|
|
1281
1286
|
` reference ${scope.digest}`,
|
|
1282
1287
|
` approved ${describeApproval(approval)}`,
|
|
1283
1288
|
];
|
package/dist/serve.js
CHANGED
|
@@ -447,6 +447,7 @@ export function createDecisionServer(options) {
|
|
|
447
447
|
builds: projectBuildsOf(repo, who),
|
|
448
448
|
access: projectAccessOf(repo),
|
|
449
449
|
network: { setting: store.projectNetwork(repo), canChange: who.via === "cookie" && who.role === "approver" && !store.isDemo() && store.accountCanAccess(who.name, repo) },
|
|
450
|
+
writes: { setting: store.projectWrites(repo), canChange: who.via === "cookie" && who.role === "approver" && !store.isDemo() && store.accountCanAccess(who.name, repo) },
|
|
450
451
|
liveView: (() => {
|
|
451
452
|
const commands = liveCommandsShown(store, repo);
|
|
452
453
|
const canChange = who.via === "cookie" && who.role === "approver" && !store.isDemo() && store.accountCanAccess(who.name, repo);
|
package/dist/server/chrome.d.ts
CHANGED
|
@@ -128,7 +128,9 @@ export type Chrome = {
|
|
|
128
128
|
* at the sink like every page, fetched same-origin — and the nonce'd CSP
|
|
129
129
|
* refuses to execute anything the region could smuggle. Nothing reads on a timer.
|
|
130
130
|
*/
|
|
131
|
-
export declare function regionScript(regionId: string, fragmentName: string, path?: string
|
|
131
|
+
export declare function regionScript(regionId: string, fragmentName: string, path?: string, options?: {
|
|
132
|
+
animate?: boolean;
|
|
133
|
+
}): string;
|
|
132
134
|
/**
|
|
133
135
|
* The live transcript's DEDICATED poller (arc 1 §4): a JSON byte-offset
|
|
134
136
|
* protocol, not an HTML fragment — the response is raw sanitized text and
|
|
@@ -297,3 +299,10 @@ export declare function safePrUrl(url: string | null): string | null;
|
|
|
297
299
|
export declare function shortDigest(digest: string): Html;
|
|
298
300
|
/** One display line, bounded — turn text is data, never layout. */
|
|
299
301
|
export declare function oneLineOf(text: string, cap: number): string;
|
|
302
|
+
/**
|
|
303
|
+
* The board's clocks tick in the page: each <time data-tick> counts up from
|
|
304
|
+
* its datetime, measured against the server's own now (the board's data-now)
|
|
305
|
+
* so a skewed device clock cannot run them backwards. The server-rendered
|
|
306
|
+
* minutes are the answer without script.
|
|
307
|
+
*/
|
|
308
|
+
export declare function boardTickScript(regionId?: string): string;
|
package/dist/server/chrome.js
CHANGED
|
@@ -161,7 +161,7 @@ export const WORKSPACE_STYLE = styleAsset(STYLE + BRAND_MARK_CSS + INBOX_TABS_CS
|
|
|
161
161
|
* at the sink like every page, fetched same-origin — and the nonce'd CSP
|
|
162
162
|
* refuses to execute anything the region could smuggle. Nothing reads on a timer.
|
|
163
163
|
*/
|
|
164
|
-
export function regionScript(regionId, fragmentName, path) {
|
|
164
|
+
export function regionScript(regionId, fragmentName, path, options = {}) {
|
|
165
165
|
// Where the fragment lives: the page's own URL by default; an explicit
|
|
166
166
|
// path when a page embeds another entity's region (the task page embeds
|
|
167
167
|
// the live run's peek, slice 1c — no proxy route exists for it).
|
|
@@ -184,7 +184,7 @@ export function regionScript(regionId, fragmentName, path) {
|
|
|
184
184
|
`function keep(){var data={lanes:[],focus:null,pager:-1,fold:{}};` +
|
|
185
185
|
`region.querySelectorAll("details.lane").forEach(function(d){var k=laneKey(d);if(k)data.fold[k]=d.open;});` +
|
|
186
186
|
`var act=document.activeElement;` +
|
|
187
|
-
`if(act&®ion.contains(act)&&act.matches&&act.matches("a.row, a.lane-card"))data.focus=act.getAttribute("href")
|
|
187
|
+
`if(act&®ion.contains(act)&&act.matches&&act.matches("a.row, a.lane-card, a.worker-job")){data.focus=act.getAttribute("href");data.focusKind=act.matches("a.worker-job")?"a.worker-job":"a.row, a.lane-card";}` +
|
|
188
188
|
`var board=region.querySelector(".board");` +
|
|
189
189
|
`if(board&&board.scrollLeft>0){var lanes=board.querySelectorAll(".lane");` +
|
|
190
190
|
`var bc=board.getBoundingClientRect();var mid=bc.left+bc.width/2;var best=-1,bd=1e9;` +
|
|
@@ -208,17 +208,41 @@ export function regionScript(regionId, fragmentName, path) {
|
|
|
208
208
|
`if(board){var lanes=board.querySelectorAll(".lane");var at=Math.min(data.pager,lanes.length-1);` +
|
|
209
209
|
`if(at>=0){var br=board.getBoundingClientRect();var lr=lanes[at].getBoundingClientRect();` +
|
|
210
210
|
`board.scrollLeft=board.scrollLeft+(lr.left+lr.width/2)-(br.left+br.width/2);}}}` +
|
|
211
|
-
`if(data.focus!==null){var links=region.querySelectorAll(
|
|
211
|
+
`if(data.focus!==null){var links=region.querySelectorAll(data.focusKind);` +
|
|
212
212
|
`for(var i=0;i<links.length;i++){if(links[i].getAttribute("href")===data.focus){links[i].focus({preventScroll:true});break;}}}}` +
|
|
213
213
|
`function tell(){if(!stamp||stopped)return;var s=Math.round((Date.now()-last)/1000);` +
|
|
214
214
|
`stamp.textContent=failed?"stale — retrying ("+s+"s old)":"updated "+s+"s ago";}` +
|
|
215
215
|
`setInterval(tell,1000);` +
|
|
216
|
+
// The animated swap (the live board): a card that changed lane slides there and a new one drops in, through one
|
|
217
|
+
// same-document view transition keyed by each card's data-vt; a worker whose builds changed flashes once
|
|
218
|
+
// (data-flash). Never under reduced motion, in a hidden tab, or where the browser lacks view transitions — there
|
|
219
|
+
// the swap is the plain instant one. Names are set only for the swap and cleared after it.
|
|
220
|
+
`var animate=${options.animate === true ? "true" : "false"};` +
|
|
221
|
+
// A name spells the whole key in hex — two tasks can never share one, so a card's identity never jumps to another.
|
|
222
|
+
`function vtName(v){var n="so-c";for(var i=0;i<v.length;i++){n+=("000"+v.charCodeAt(i).toString(16)).slice(-4);}return n;}` +
|
|
223
|
+
`function nameAll(){var seen={};region.querySelectorAll("[data-vt]").forEach(function(e){var n=vtName(e.getAttribute("data-vt"));if(seen[n])return;seen[n]=1;e.style.setProperty("view-transition-name",n);});}` +
|
|
224
|
+
`function flashes(){var m={};region.querySelectorAll("[data-flash]").forEach(function(e){m[e.getAttribute("data-flash")]=e.getAttribute("data-flash-value");});return m;}` +
|
|
225
|
+
`function apply(t){var before=animate?flashes():{};var kept=keep();region.innerHTML=t;restore(kept);` +
|
|
226
|
+
`if(animate)region.querySelectorAll("[data-flash]").forEach(function(e){var k=e.getAttribute("data-flash"),v=e.getAttribute("data-flash-value");if(k in before&&before[k]!==v&&v!=="")e.classList.add("flash");});}` +
|
|
227
|
+
// The swap resolves when its transition has finished, and the next fetch waits for it (refresh holds busy), so
|
|
228
|
+
// one transition runs at a time; each cleans up only its own (the generation), never a later one's.
|
|
229
|
+
`var generation=0;` +
|
|
230
|
+
`function swap(t){var calm=!window.matchMedia||matchMedia("(prefers-reduced-motion: reduce)").matches;` +
|
|
231
|
+
`if(!animate||calm||document.hidden||typeof document.startViewTransition!=="function"){apply(t);return Promise.resolve();}` +
|
|
232
|
+
// Bounded: a transition that never settles (or never runs its update) is let go after 1.5 s — the fragment is
|
|
233
|
+
// applied exactly once either way, so the board can never stop refreshing behind an animation.
|
|
234
|
+
`var root=document.documentElement;var mine=++generation;var applied=false;var settled=false;nameAll();root.classList.add("so-swap");` +
|
|
235
|
+
`var once=function(){if(applied)return false;applied=true;apply(t);return true;};` +
|
|
236
|
+
`var end=function(){if(settled)return;settled=true;once();if(mine!==generation)return;root.classList.remove("so-swap");region.querySelectorAll("[data-vt]").forEach(function(e){e.style.removeProperty("view-transition-name");});};` +
|
|
237
|
+
`var vt;try{vt=document.startViewTransition(function(){if(once())nameAll();});}catch(e){end();return Promise.resolve();}` +
|
|
238
|
+
`return new Promise(function(done){var timer=setTimeout(function(){end();done();},1500);` +
|
|
239
|
+
`var fin=function(){clearTimeout(timer);end();done();};try{Promise.resolve(vt&&vt.finished).then(fin,fin);}catch(e){fin();}});}` +
|
|
216
240
|
`function refresh(){if(stopped)return;if(busy){again=true;return;}busy=true;again=false;` +
|
|
217
241
|
`var q=location.search?location.search+"&fragment="+${JSON.stringify(fragmentName)}:"?fragment="+${JSON.stringify(fragmentName)};` +
|
|
218
242
|
`fetch(${target},{redirect:"manual",cache:"no-store"})` +
|
|
219
243
|
`.then(function(r){if(r.type==="opaqueredirect"||r.status===401||r.status===403){location.href="/login";return null;}` +
|
|
220
244
|
`return r.ok?r.text():null;})` +
|
|
221
|
-
`.then(function(t){if(t){
|
|
245
|
+
`.then(function(t){if(t){return swap(t).then(function(){last=Date.now();failed=false;});}failed=true;})` +
|
|
222
246
|
`.catch(function(){failed=true;})` +
|
|
223
247
|
// A fragment that marks itself final stops: a finished or abandoned build is not fetched again.
|
|
224
248
|
`.then(function(){busy=false;tell();if(region.querySelector("[data-region-stop]")){stopped=true;if(stamp)stamp.textContent="";return;}if(again)refresh();});}` +
|
|
@@ -760,3 +784,20 @@ export function oneLineOf(text, cap) {
|
|
|
760
784
|
const flat = text.replace(/\s+/g, " ").trim();
|
|
761
785
|
return flat.length <= cap ? flat : `${flat.slice(0, cap - 1)}…`;
|
|
762
786
|
}
|
|
787
|
+
/**
|
|
788
|
+
* The board's clocks tick in the page: each <time data-tick> counts up from
|
|
789
|
+
* its datetime, measured against the server's own now (the board's data-now)
|
|
790
|
+
* so a skewed device clock cannot run them backwards. The server-rendered
|
|
791
|
+
* minutes are the answer without script.
|
|
792
|
+
*/
|
|
793
|
+
export function boardTickScript(regionId = "board-region") {
|
|
794
|
+
return (`(function(){var seen=null,offset=0;var region=document.getElementById(${JSON.stringify(regionId)});` +
|
|
795
|
+
`function fmt(s){if(s<0)s=0;var h=Math.floor(s/3600),m=Math.floor(s%3600/60),x=s%60;return h>0?h+"h "+(m<10?"0":"")+m+"m":m+":"+(x<10?"0":"")+x;}` +
|
|
796
|
+
`function run(){var scope=region||document;var anchor=scope.querySelector("[data-now]");var v=anchor?anchor.getAttribute("data-now"):null;` +
|
|
797
|
+
`if(v!==null&&v!==seen){seen=v;var n=Number(v);offset=isFinite(n)?n-Date.now():0;}` +
|
|
798
|
+
`var now=Date.now()+offset;scope.querySelectorAll("time[data-tick]").forEach(function(t){var at=Date.parse(t.getAttribute("datetime")||"");if(isNaN(at))return;var text=fmt(Math.floor((now-at)/1000));if(t.textContent!==text)t.textContent=text;});}` +
|
|
799
|
+
// A swapped region brings back the server's minutes, so a swap (the region's children replaced — never a clock's
|
|
800
|
+
// own text) ticks at once and no clock flickers back to them. Hidden tabs do not tick.
|
|
801
|
+
`run();setInterval(function(){if(!document.hidden)run();},1000);document.addEventListener("visibilitychange",function(){if(!document.hidden)run();});` +
|
|
802
|
+
`if(region&&typeof MutationObserver==="function")new MutationObserver(run).observe(region,{childList:true});})();`);
|
|
803
|
+
}
|
package/dist/server/console.css
CHANGED
|
@@ -1825,7 +1825,7 @@
|
|
|
1825
1825
|
building, then the rest. A header is a 2.75rem tap; a drawn chevron
|
|
1826
1826
|
says which way it folds. */
|
|
1827
1827
|
@media (max-width: 760px) {
|
|
1828
|
-
.board { display: flex; flex-direction: column; gap: .5rem; padding-bottom: 0; }
|
|
1828
|
+
.board { display: flex; flex-direction: column; align-items: stretch; gap: .5rem; padding-bottom: 0; }
|
|
1829
1829
|
.board .lane { min-height: 0; padding: 0 .625rem .125rem; }
|
|
1830
1830
|
.board .lane > summary {
|
|
1831
1831
|
cursor: pointer; display: flex; align-items: center; min-height: 2.75rem; margin: 0;
|
|
@@ -1864,7 +1864,7 @@
|
|
|
1864
1864
|
}
|
|
1865
1865
|
.lane-attention h2::before { background: var(--brand); opacity: 1; }
|
|
1866
1866
|
.lane-building h2::before { background: var(--running); opacity: 1; }
|
|
1867
|
-
.lane-done h2::before { background: var(--
|
|
1867
|
+
.lane-done h2::before { background: var(--muted-foreground); opacity: 1; }
|
|
1868
1868
|
.lane-card {
|
|
1869
1869
|
display: block; text-decoration: none; color: inherit;
|
|
1870
1870
|
background: var(--card); border: 1px solid var(--border);
|
|
@@ -1891,6 +1891,37 @@
|
|
|
1891
1891
|
font-family: var(--font-mono); font-size: .6875rem; color: var(--running); font-variant-numeric: tabular-nums;
|
|
1892
1892
|
}
|
|
1893
1893
|
.lane-card .live-line .clock { color: var(--muted-foreground); }
|
|
1894
|
+
/* The build's steps — set up, writing, checking and committing: three quiet
|
|
1895
|
+
ticks, the current one in the running hue. A stage, never a percent. */
|
|
1896
|
+
.lane-card .steps { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .1875rem; margin-top: .5rem; }
|
|
1897
|
+
.lane-card .steps i { height: .1875rem; border-radius: 999px; background: var(--border); }
|
|
1898
|
+
.lane-card .steps i.on { background: color-mix(in srgb, var(--running) 45%, var(--border)); }
|
|
1899
|
+
.lane-card .steps i.now { background: var(--running); }
|
|
1900
|
+
|
|
1901
|
+
/* The worker strip: one quiet tile per worker above the lanes — what it is
|
|
1902
|
+
building here, the stage, the clock. Ink and lines only; the running dot
|
|
1903
|
+
is the one hue. */
|
|
1904
|
+
.crew { display: grid; grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr)); gap: .625rem; margin: .25rem 0 .875rem; }
|
|
1905
|
+
.crew-empty { margin: .25rem 0 .875rem; }
|
|
1906
|
+
.worker {
|
|
1907
|
+
border: 1px solid var(--border); border-radius: var(--radius); background: var(--card);
|
|
1908
|
+
padding: .625rem .75rem; min-width: 0; display: flex; flex-direction: column; gap: .375rem;
|
|
1909
|
+
}
|
|
1910
|
+
.worker-busy { border-color: color-mix(in srgb, var(--running) 30%, var(--border)); }
|
|
1911
|
+
.worker-offline { opacity: .6; }
|
|
1912
|
+
.worker-head { display: flex; align-items: center; gap: .5rem; min-width: 0; }
|
|
1913
|
+
.worker-head .name { font-family: var(--font-mono); font-size: .75rem; font-weight: 500; }
|
|
1914
|
+
.worker-head .kind { font-size: .6875rem; color: var(--muted-foreground); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; min-width: 0; }
|
|
1915
|
+
.worker-head .slots { font-family: var(--font-mono); font-size: .6875rem; color: var(--muted-foreground); font-variant-numeric: tabular-nums; }
|
|
1916
|
+
.worker-job { display: flex; flex-direction: column; gap: .0625rem; text-decoration: none; color: inherit; min-width: 0; margin: 0; }
|
|
1917
|
+
.worker-job .t { font-size: .8125rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
1918
|
+
.worker-job .sub { display: flex; justify-content: space-between; gap: .5rem; font-family: var(--font-mono); font-size: .6875rem; color: var(--muted-foreground); font-variant-numeric: tabular-nums; }
|
|
1919
|
+
.worker-job.idle { font-size: .75rem; color: var(--muted-foreground); }
|
|
1920
|
+
@media (hover: hover) and (pointer: fine) { a.worker-job:hover .t { text-decoration: underline; } }
|
|
1921
|
+
@media (max-width: 760px) {
|
|
1922
|
+
.crew { grid-template-columns: minmax(0, 1fr); gap: .375rem; }
|
|
1923
|
+
.worker { padding: .5rem .625rem; }
|
|
1924
|
+
}
|
|
1894
1925
|
.lane-empty { margin: .75rem 0 .25rem; }
|
|
1895
1926
|
.plan-doc { white-space: pre-wrap; overflow-wrap: anywhere; font-size: .8125rem; max-height: 24rem; overflow-y: auto; }
|
|
1896
1927
|
.lane-more { display: block; margin-top: .5rem; font-size: .75rem; }
|
|
@@ -2674,8 +2705,10 @@ button { min-height: 44px; }
|
|
|
2674
2705
|
@media (hover: hover) and (pointer: fine) { .next-pager .skip:hover { border-color: color-mix(in srgb, var(--border) 60%, var(--muted-foreground)); } }
|
|
2675
2706
|
|
|
2676
2707
|
/* Motion: only where a human caused the change — navigation, presses,
|
|
2677
|
-
overlays
|
|
2678
|
-
|
|
2708
|
+
overlays — and one exception, the live board: a card that changes lane
|
|
2709
|
+
slides there and a worker that picks up work flashes once (the board's
|
|
2710
|
+
swap, html.so-swap, below). Every other liveness swap stays instant; the
|
|
2711
|
+
pulse dot is the one "alive" signal. Everything dies under prefers-reduced-motion; auto-refresh pages
|
|
2679
2712
|
opt out of the navigation cross-fade separately (see shell()). */
|
|
2680
2713
|
@view-transition { navigation: auto; }
|
|
2681
2714
|
/* A page change fades THROUGH, not across: the old page leaves before the new
|
|
@@ -2776,3 +2809,21 @@ button.pick-file { min-height: 1.75rem; padding: 0 .55rem; font-size: .75rem; }
|
|
|
2776
2809
|
.meta, .hint { line-height: 1.35; }
|
|
2777
2810
|
details { margin: var(--so-phone-row) 0; }
|
|
2778
2811
|
}
|
|
2812
|
+
|
|
2813
|
+
/* The live board's swap (regionScript animate): only cards move — the page
|
|
2814
|
+
itself does not fade — a short slide for a card that changed lane, a drop
|
|
2815
|
+
for a new one, a fade for one that left. Transform and opacity only, and
|
|
2816
|
+
the reduce block kills it with everything else (the script also skips the
|
|
2817
|
+
transition under reduced motion). */
|
|
2818
|
+
html.so-swap::view-transition-old(root), html.so-swap::view-transition-new(root) { animation: none; }
|
|
2819
|
+
html.so-swap::view-transition-group(*) { animation-duration: 320ms; animation-timing-function: cubic-bezier(.2, .8, .2, 1); }
|
|
2820
|
+
html.so-swap::view-transition-new(*):only-child { animation: so-card-in 240ms ease-out both; }
|
|
2821
|
+
html.so-swap::view-transition-old(*):only-child { animation: so-card-out 160ms ease-in both; }
|
|
2822
|
+
@keyframes so-card-in { from { opacity: 0; transform: translateY(-.375rem); } }
|
|
2823
|
+
@keyframes so-card-out { to { opacity: 0; } }
|
|
2824
|
+
.worker.flash { animation: so-worker-claim 900ms ease-out; }
|
|
2825
|
+
@keyframes so-worker-claim { from { background: color-mix(in srgb, var(--running) 14%, var(--card)); } }
|
|
2826
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2827
|
+
html.so-swap::view-transition-group(*), html.so-swap::view-transition-new(*), html.so-swap::view-transition-old(*) { animation: none !important; }
|
|
2828
|
+
.worker.flash { animation: none; }
|
|
2829
|
+
}
|
|
@@ -146,14 +146,39 @@ export declare function systemPage(chrome: Chrome, data: {
|
|
|
146
146
|
}[];
|
|
147
147
|
now: Date;
|
|
148
148
|
}): Screen;
|
|
149
|
+
/**
|
|
150
|
+
* A build's stage as one of three steps — set up, writing, checking and
|
|
151
|
+
* committing — read from the machine's own phase, never a percent. The
|
|
152
|
+
* steps only move forward: evidence and proof checks run both before and
|
|
153
|
+
* after the commit (builder.ts), so everything after the agent's turn is
|
|
154
|
+
* one step, and the stage words say which part. Null for runs that are not
|
|
155
|
+
* builds (planning, review, investigation): those have no honest steps.
|
|
156
|
+
*/
|
|
157
|
+
export declare const BUILD_STEPS: readonly ["setting up", "writing", "checking and committing"];
|
|
158
|
+
export declare function buildStepOf(claim: NonNullable<BoardCard["claim"]>): number | null;
|
|
159
|
+
/**
|
|
160
|
+
* The workers a board's strip may show: not retired, bound to at least one
|
|
161
|
+
* project the server's ceiling admits (never a runner serving only projects
|
|
162
|
+
* this server must not speak about), and — on a one-project board — bound to
|
|
163
|
+
* that project.
|
|
164
|
+
*/
|
|
165
|
+
export declare function boardWorkers(runners: readonly Runner[], scope: {
|
|
166
|
+
all: boolean;
|
|
167
|
+
project: string | null;
|
|
168
|
+
visible: (repo: string) => boolean;
|
|
169
|
+
}): Runner[];
|
|
149
170
|
/**
|
|
150
171
|
* The board: the pipeline as lanes, position as meaning — attention, then
|
|
151
|
-
* queued, then waiting, then building, then recently done
|
|
152
|
-
* construction (every card is a link,
|
|
153
|
-
* makes it safe to re-render itself
|
|
172
|
+
* queued, then waiting, then building, then recently done — under a strip
|
|
173
|
+
* of the workers doing it. Read-only by construction (every card is a link,
|
|
174
|
+
* no forms, no nonces), which is what makes it safe to re-render itself
|
|
175
|
+
* while somebody watches; a card that changes lane slides there
|
|
176
|
+
* (regionScript's animate swap, keyed by data-vt).
|
|
154
177
|
*/
|
|
155
178
|
export declare function boardBody(data: {
|
|
156
179
|
cards: BoardCard[];
|
|
180
|
+
/** The workers this board's work can go to (retired ones left out). */
|
|
181
|
+
workers: readonly Runner[];
|
|
157
182
|
done: ReturnType<Store["listCompletedWorkScoped"]>;
|
|
158
183
|
saturated: boolean;
|
|
159
184
|
now: Date;
|
|
@@ -207,11 +207,74 @@ export function systemPage(chrome, data) {
|
|
|
207
207
|
: html `<span class="meta">syncing normally</span>`}</p>`), "\n")}`,
|
|
208
208
|
], "\n"), { chrome, refreshSeconds: data.building.length > 0 ? 10 : 60 });
|
|
209
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* A build's stage as one of three steps — set up, writing, checking and
|
|
212
|
+
* committing — read from the machine's own phase, never a percent. The
|
|
213
|
+
* steps only move forward: evidence and proof checks run both before and
|
|
214
|
+
* after the commit (builder.ts), so everything after the agent's turn is
|
|
215
|
+
* one step, and the stage words say which part. Null for runs that are not
|
|
216
|
+
* builds (planning, review, investigation): those have no honest steps.
|
|
217
|
+
*/
|
|
218
|
+
export const BUILD_STEPS = ["setting up", "writing", "checking and committing"];
|
|
219
|
+
export function buildStepOf(claim) {
|
|
220
|
+
if (claim.role === "planner" || claim.role === "reviewer" || claim.role === "scout")
|
|
221
|
+
return null;
|
|
222
|
+
if (claim.model === null)
|
|
223
|
+
return 0;
|
|
224
|
+
if (claim.phase === null || claim.phase === "agent-running")
|
|
225
|
+
return 1;
|
|
226
|
+
return 2;
|
|
227
|
+
}
|
|
228
|
+
/** A clock that ticks in the page (boardTickScript) from the server's own start time; the text is the no-script answer. */
|
|
229
|
+
function tickingClock(startedIso, now) {
|
|
230
|
+
const minutes = Math.max(0, Math.round((now.getTime() - new Date(startedIso).getTime()) / 60_000));
|
|
231
|
+
return html `<time class="clock" datetime="${startedIso}" data-tick>${minutes}m</time>`;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* The workers a board's strip may show: not retired, bound to at least one
|
|
235
|
+
* project the server's ceiling admits (never a runner serving only projects
|
|
236
|
+
* this server must not speak about), and — on a one-project board — bound to
|
|
237
|
+
* that project.
|
|
238
|
+
*/
|
|
239
|
+
export function boardWorkers(runners, scope) {
|
|
240
|
+
return runners.filter(runner => runner.retiredAt === null &&
|
|
241
|
+
runner.repos.some(repo => scope.visible(repo)) &&
|
|
242
|
+
(scope.all || scope.project === null || runner.repos.includes(scope.project)));
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* The worker strip across the top of the board: every worker this board's
|
|
246
|
+
* work can go to, what it is building here, and its stage and clock. Only
|
|
247
|
+
* builds the board already shows are named — a worker busy in a project
|
|
248
|
+
* this reader cannot see reads "no build on this board", never "free".
|
|
249
|
+
*/
|
|
250
|
+
function workerStrip(runners, cards, now) {
|
|
251
|
+
if (runners.length === 0)
|
|
252
|
+
return html `<p class="meta crew-empty">No workers yet — work waits in the queue until one joins.</p>`;
|
|
253
|
+
const tiles = runners.map(runner => {
|
|
254
|
+
const builds = cards
|
|
255
|
+
.filter(card => card.lane === "building" && card.claim !== null && card.claim.runner === runner.name)
|
|
256
|
+
.sort((a, b) => a.claim.claimedAt.localeCompare(b.claim.claimedAt));
|
|
257
|
+
const online = runnerAlive(runner, now);
|
|
258
|
+
const state = !online ? "offline" : builds.length > 0 ? "busy" : "idle";
|
|
259
|
+
const dot = state === "busy" ? html `<span class="dot dot-ok pulse"></span>` : html `<span class="dot dot-off"></span>`;
|
|
260
|
+
const jobs = builds.length > 0
|
|
261
|
+
? builds.map(card => {
|
|
262
|
+
const claim = card.claim;
|
|
263
|
+
const words = claim.model === null ? "preparing workspace" : claim.phase === null ? "the agent is working" : phaseWords(claim.phase);
|
|
264
|
+
return html `<a class="worker-job" href="${card.href}"><span class="t">${card.title}</span><span class="sub"><span>${words}</span>${tickingClock(claim.claimedAt, now)}</span></a>`;
|
|
265
|
+
})
|
|
266
|
+
: html `<p class="worker-job idle">${online ? "no build on this board" : html `offline since ${whenTime(runner.heartbeatAt)}`}</p>`;
|
|
267
|
+
return html `<div class="worker worker-${state}" data-flash="${runner.name}" data-flash-value="${builds.map(card => card.taskId).join(" ")}"><div class="worker-head">${dot}<span class="name">${runner.name}</span><span class="kind">${runner.agents.join(" · ")}</span>${runner.capacity > 1 ? html `<span class="slots">${builds.length}/${runner.capacity}</span>` : null}</div>${jobs}</div>`;
|
|
268
|
+
});
|
|
269
|
+
return html `<section class="crew" aria-label="Workers">${tiles}</section>`;
|
|
270
|
+
}
|
|
210
271
|
/**
|
|
211
272
|
* The board: the pipeline as lanes, position as meaning — attention, then
|
|
212
|
-
* queued, then waiting, then building, then recently done
|
|
213
|
-
* construction (every card is a link,
|
|
214
|
-
* makes it safe to re-render itself
|
|
273
|
+
* queued, then waiting, then building, then recently done — under a strip
|
|
274
|
+
* of the workers doing it. Read-only by construction (every card is a link,
|
|
275
|
+
* no forms, no nonces), which is what makes it safe to re-render itself
|
|
276
|
+
* while somebody watches; a card that changes lane slides there
|
|
277
|
+
* (regionScript's animate swap, keyed by data-vt).
|
|
215
278
|
*/
|
|
216
279
|
export function boardBody(data, ciRed) {
|
|
217
280
|
const chip = (repo) => !data.all || repo === null ? null : html `<span class="badge">${projectName(repo)}</span>`;
|
|
@@ -261,7 +324,7 @@ export function boardBody(data, ciRed) {
|
|
|
261
324
|
const kept = parts.filter((one) => one !== null);
|
|
262
325
|
return kept.length === 0 ? null : html `<span class="chips">${joinHtml(kept, " ")}</span>`;
|
|
263
326
|
};
|
|
264
|
-
const plain = (card) => html `<a class="lane-card" href="${card.href}"><span class="id">${card.taskId}</span><span class="t">${card.title}</span><span class="why">${card.reason}</span>${facts([
|
|
327
|
+
const plain = (card) => html `<a class="lane-card" href="${card.href}" data-vt="${card.taskId}"><span class="id">${card.taskId}</span><span class="t">${card.title}</span><span class="why">${card.reason}</span>${facts([
|
|
265
328
|
...(card.stalledSince === null ? [] : [["waiting", age(card.stalledSince)]]),
|
|
266
329
|
])}${chips([
|
|
267
330
|
chip(card.repo),
|
|
@@ -279,7 +342,7 @@ export function boardBody(data, ciRed) {
|
|
|
279
342
|
}
|
|
280
343
|
return heads;
|
|
281
344
|
})();
|
|
282
|
-
const queuedCard = (card) => html `<a class="lane-card" href="${card.href}"><span class="id">${card.taskId}</span><span class="t">${card.title}</span><span class="why">${card.reason}</span>${facts([
|
|
345
|
+
const queuedCard = (card) => html `<a class="lane-card" href="${card.href}" data-vt="${card.taskId}"><span class="id">${card.taskId}</span><span class="t">${card.title}</span><span class="why">${card.reason}</span>${facts([
|
|
283
346
|
["worker", card.assignedRunner === null ? "any free worker" : card.assignedRunner],
|
|
284
347
|
])}${chips([
|
|
285
348
|
queuedHeads.get(`${card.assignedRunner ?? ""}|${card.repo ?? ""}`) === card.taskId
|
|
@@ -292,14 +355,19 @@ export function boardBody(data, ciRed) {
|
|
|
292
355
|
const claim = card.claim;
|
|
293
356
|
if (claim === null)
|
|
294
357
|
return plain(card);
|
|
295
|
-
const minutes = Math.max(1, Math.round((data.now.getTime() - new Date(claim.claimedAt).getTime()) / 60_000));
|
|
296
358
|
const workspace = claim.worktree === null ? null : (claim.worktree.split("/").pop() ?? claim.worktree);
|
|
297
359
|
// Chip copy: unknown phases show nothing rather than raw tokens.
|
|
298
360
|
const phase = claim.phase === null ? undefined : PHASE_WORDS[claim.phase];
|
|
299
361
|
// The live strip is the run's own phase and clock — never a percent:
|
|
300
362
|
// a build has no honest progress figure, only a stage and an elapsed.
|
|
301
363
|
const live = claim.model === null ? "preparing workspace" : (phase ?? "the agent is working");
|
|
302
|
-
|
|
364
|
+
// The steps: where the build is among set up → writing → checking →
|
|
365
|
+
// committing, from its phase — a stage, never a percent.
|
|
366
|
+
const step = buildStepOf(claim);
|
|
367
|
+
const steps = step === null
|
|
368
|
+
? null
|
|
369
|
+
: html `<span class="steps" role="img" aria-label="${`step ${step + 1} of ${BUILD_STEPS.length}: ${BUILD_STEPS[step]}`}">${BUILD_STEPS.map((_, index) => html `<i class="${index < step ? "on" : index === step ? "now" : ""}"></i>`)}</span>`;
|
|
370
|
+
return html `<a class="lane-card building" href="${card.href}" data-vt="${card.taskId}"><span class="id">${card.taskId}</span><span class="t"><span class="dot dot-ok pulse"></span>${card.title}</span>${steps}<span class="live-line"><span class="stage">${live}</span>${tickingClock(claim.claimedAt, data.now)}</span>${facts([
|
|
303
371
|
["worker", claim.runner],
|
|
304
372
|
...(claim.model === null
|
|
305
373
|
? []
|
|
@@ -316,7 +384,7 @@ export function boardBody(data, ciRed) {
|
|
|
316
384
|
const pr = row.prNumber === null
|
|
317
385
|
? null
|
|
318
386
|
: html `<span class="badge badge-open">PR #${row.prNumber}</span>${ciRed(row.prNumber) ? html ` <span class="badge badge-failed">CI failing</span>` : null}`;
|
|
319
|
-
return html `<a class="lane-card" href="${taskHref(row.taskId)}"><span class="id">${row.taskId}</span><span class="t">${row.title}</span>${row.handoff === null ? null : html `<span class="why">${row.handoff.length > 120 ? row.handoff.slice(0, 120) + "…" : row.handoff}</span>`}${facts([
|
|
387
|
+
return html `<a class="lane-card" href="${taskHref(row.taskId)}" data-vt="${row.taskId}"><span class="id">${row.taskId}</span><span class="t">${row.title}</span>${row.handoff === null ? null : html `<span class="why">${row.handoff.length > 120 ? row.handoff.slice(0, 120) + "…" : row.handoff}</span>`}${facts([
|
|
320
388
|
...(row.ranMinutes === null ? [] : [["ran", `${row.ranMinutes}m`]]),
|
|
321
389
|
["usage", runCostWords({ authMode: row.authMode, costUsd: row.costUsd, tokensIn: null, tokensOut: null })],
|
|
322
390
|
])}${chips([
|
|
@@ -347,7 +415,8 @@ export function boardBody(data, ciRed) {
|
|
|
347
415
|
data.all ? null : html `<p class="meta board-view"><strong>State</strong> · <a href="/board?view=order">order →</a> <span class="meta">drag to reorder, or to reserve a task for one worker</span></p>`,
|
|
348
416
|
deltaLine,
|
|
349
417
|
toggle,
|
|
350
|
-
|
|
418
|
+
workerStrip(data.workers, data.cards, data.now),
|
|
419
|
+
html `<div class="board" data-now="${String(data.now.getTime())}">`,
|
|
351
420
|
lane("attention", "needs you", "these wait for a person", plain),
|
|
352
421
|
lane("queued", "queued", "starts when a worker is free", queuedCard),
|
|
353
422
|
lane("waiting", "waiting", "paused until a time, another task, or a requirement is ready", plain),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Task pages: approval, agents, plans, controls, decisions and the task body. */
|
|
2
2
|
import { networkWords } from "../egress.js";
|
|
3
|
+
import { writesWords } from "../writes-mode.js";
|
|
3
4
|
import { ACP_AGENTS, isAcpProfile } from "../acp-agents.js";
|
|
4
5
|
import { html, htmlString, joinHtml, postForm, replaceMarkup, reauth } from "../html.js";
|
|
5
6
|
import { closedQuestionReason, closedQuestionWords } from "../decision.js";
|
|
@@ -541,7 +542,7 @@ export function profileWords(scope) {
|
|
|
541
542
|
? html `<p class="meta">Runs on <span class="mono">${ACP_AGENTS[profile.provider].name} (via ACP) · ${profile.model}</span> — full access inside the OS fence; the agent runs any tool it chooses and nothing asks. No turn limit: stops after ${Math.round(profile.timeoutSeconds / 60)} min, or ${Math.round(profile.idleTimeoutSeconds / 60)} min silent; repairs on ${repair}, ${Math.round(profile.repairTimeoutSeconds / 60)} min, or ${Math.round(profile.repairIdleTimeoutSeconds / 60)} min silent.</p>`
|
|
542
543
|
: html `<p class="meta">Runs on <span class="mono">${profile.provider} · ${profile.model}</span> — ${profile.sandboxMode === "danger-full-access" ? "FULL permissions via --dangerously-bypass-approvals-and-sandbox; nothing asks" : "workspace-write sandbox"}, no turn limit (${Math.round(profile.timeoutSeconds / 60)} min ${profile.timeoutKind === "idle" ? "without progress" : "per attempt"}); repairs on ${repair}, ${Math.round(profile.repairTimeoutSeconds / 60)} min</p>`;
|
|
543
544
|
// What the build may reach is part of what is approved (egress.ts): said on every approval form.
|
|
544
|
-
return html `${base}<p class="meta" data-scope-network>${networkWords(profile.network ?? null)}</p>`;
|
|
545
|
+
return html `${base}<p class="meta" data-scope-network>${networkWords(profile.network ?? null)}</p><p class="meta" data-scope-writes>Writes: ${writesWords(profile.writes)}.</p>`;
|
|
545
546
|
}
|
|
546
547
|
export const ROLE_NOUN = { plan: "Planner", build: "Builder", repair: "Repair", review: "Reviewer" };
|
|
547
548
|
export function agentsStandingWords(view) {
|
|
@@ -283,6 +283,7 @@ post("settings", "spend.budget", "/spend/budget", { scope: "step-up", ...operato
|
|
|
283
283
|
post("settings", "settings.project-concurrency", "/settings/project/concurrency");
|
|
284
284
|
post("settings", "settings.project-live-view", "/settings/project/live-view", { scope: "step-up", callers: ["cookie"] });
|
|
285
285
|
post("settings", "settings.project-network", "/settings/project/network", { scope: "step-up", callers: ["cookie"] });
|
|
286
|
+
post("settings", "settings.project-writes", "/settings/project/writes", { scope: "step-up", callers: ["cookie"] });
|
|
286
287
|
post("settings", "settings.project-delete", "/settings/project/delete", { scope: "step-up", ...operator("An instance operator deletes projects.", "/settings/project") });
|
|
287
288
|
// Lead sweeps (docs/plans/lead-fan-out.md): the table, and cancelling what isn't ready. The handler shows a sweep only in
|
|
288
289
|
// a project the reader can see, and acts only for an approver.
|
package/dist/server/settings.js
CHANGED
|
@@ -80,6 +80,7 @@ import { policyHtml } from "../policy-ui.js";
|
|
|
80
80
|
import { checkPolicy, parseList, policyParts } from "../policy.js";
|
|
81
81
|
import { parseProjectConcurrency, saveProjectConcurrency } from "../project-concurrency.js";
|
|
82
82
|
import { MAX_NETWORK_HOSTS, normalizeHost } from "../egress.js";
|
|
83
|
+
import { isWritesMode } from "../writes-mode.js";
|
|
83
84
|
import { projectDeleteConfirmHtml, projectSettingsHtml } from "../project-delete-ui.js";
|
|
84
85
|
import { deleteProject, holdingsWords } from "../project-delete.js";
|
|
85
86
|
import { applySavedKnowledge, changeKnowledge, knowledgeVersion, knowledgeView } from "../project-knowledge.js";
|
|
@@ -2483,6 +2484,7 @@ ${modeWords(terms)
|
|
|
2483
2484
|
"settings.project-concurrency": projectConcurrency,
|
|
2484
2485
|
"settings.project-live-view": projectLiveView,
|
|
2485
2486
|
"settings.project-network": projectNetwork,
|
|
2487
|
+
"settings.project-writes": projectWrites,
|
|
2486
2488
|
"settings.project-delete": projectDelete,
|
|
2487
2489
|
"settings.policy-send": policySave,
|
|
2488
2490
|
"settings.approval-send": approvalSave,
|
|
@@ -2566,5 +2568,24 @@ ${modeWords(terms)
|
|
|
2566
2568
|
"edge.google-callback": googleCallback,
|
|
2567
2569
|
"edge.connect-callback": connectCallback,
|
|
2568
2570
|
});
|
|
2571
|
+
// Settings → Project → Where builds write (writes-mode.ts). An approver for the project changes it with their password
|
|
2572
|
+
// typed now; the ledger keeps before → after. New scopes use it; approved ones keep theirs.
|
|
2573
|
+
async function projectWrites(ctx) {
|
|
2574
|
+
const { who, response, now, posted } = ctx;
|
|
2575
|
+
const body = readForm(posted, CONSOLE_FORMS.projectWrites);
|
|
2576
|
+
const repo = body.get("repo") ?? "";
|
|
2577
|
+
if (!consoleProjects().includes(repo) || !visible(repo))
|
|
2578
|
+
return refuse(response, who, 403, "That project is outside your access.", "/settings/project");
|
|
2579
|
+
if (who.via !== "cookie" || who.role !== "approver" || store.isDemo() || !store.accountCanAccess(who.name, repo))
|
|
2580
|
+
return refuse(response, who, 403, "An approver for this project changes where its builds may write.", "/settings/project");
|
|
2581
|
+
const back = (key, words) => redirect(response, `/settings/project?repo=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}#writes`);
|
|
2582
|
+
const wanted = body.get("writes");
|
|
2583
|
+
if (!isWritesMode(wanted))
|
|
2584
|
+
return back("problem", "Choose where builds may write. Nothing changed.");
|
|
2585
|
+
if (!authenticateCredential(who, body.get("password") ?? "").ok)
|
|
2586
|
+
return back("problem", "Enter your Toolroll password to change where builds may write.");
|
|
2587
|
+
const changed = store.setProjectWrites(repo, wanted, who.name, now);
|
|
2588
|
+
return back("said", changed.before === changed.after ? "Nothing changed." : "Saved. Tasks scoped from now on use it; tasks already approved keep what they were approved with.");
|
|
2589
|
+
}
|
|
2569
2590
|
return { registrations, modelsView };
|
|
2570
2591
|
}
|
package/dist/server/tasks.js
CHANGED
|
@@ -61,9 +61,9 @@ import { agentQuestion, answerAgentQuestion, markAgentSeen } from "../agent-boar
|
|
|
61
61
|
import { WorkIndexCursorError, workIndexPage } from "../work-index.js";
|
|
62
62
|
import { buildProgressOf, parseWorkView } from "../workspace-ui.js";
|
|
63
63
|
import { requestContext } from "./request-context.js";
|
|
64
|
-
import { oneLineOf, QUEUE_VIEW, refuse, regionScript, screen, transcriptScript, whenTime } from "./chrome.js";
|
|
64
|
+
import { boardTickScript, oneLineOf, QUEUE_VIEW, refuse, regionScript, screen, transcriptScript, whenTime } from "./chrome.js";
|
|
65
65
|
import { chatReturnWithSaid, matchTaskPath, redirect, respond, reviewHref, safeChatReturn, safeReturn, SAFETY, taskChatHref, taskHref } from "./http.js";
|
|
66
|
-
import { boardBody, donePage, inboxFingerprints, inboxPage, newTaskPage, nextPage, parseInboxTab, QUEUE_FRONT, queueBody, queueScript, TASK_STATES, tasksPage, workPage } from "./render-pages.js";
|
|
66
|
+
import { boardBody, boardWorkers, donePage, inboxFingerprints, inboxPage, newTaskPage, nextPage, parseInboxTab, QUEUE_FRONT, queueBody, queueScript, TASK_STATES, tasksPage, workPage } from "./render-pages.js";
|
|
67
67
|
import { runEfficiencyOf } from "../run-efficiency.js";
|
|
68
68
|
import { ACCEPT_ANYWAY_NEEDS_REASON, editorFileHref, proofBundleView, rankReviewQueue, RESULT_REFUSALS, REVIEW_QUEUE_CAP, reviewCockpitDetailParts, reviewCockpitPage, reviewPriorityOf, runFactsFragment, runOutcomeBadge, runPage, RUNS_PAGE, runsPage, withRefusal } from "./render-results.js";
|
|
69
69
|
import { approvalFormDigest, approveRefusalWords, checkProgressHtml, consentDoorOf, decisionPage, nonceHashOf, PermissionsWouldChange, requirementsFromEditor, resumeDigestOf, taskOf } from "./render-tasks.js";
|
|
@@ -399,7 +399,10 @@ export function createTasksHandlers(runtime) {
|
|
|
399
399
|
});
|
|
400
400
|
});
|
|
401
401
|
const laneCards = [...cards, ...unverifiedCards];
|
|
402
|
-
|
|
402
|
+
// The worker strip: the workers this board's work can go to — bound to
|
|
403
|
+
// this project, or (rolled up) to any project the ceiling admits.
|
|
404
|
+
const workers = boardWorkers(store.listRunners(), { all, project, visible });
|
|
405
|
+
const body = boardBody({ cards: laneCards, workers, done: verifiedDone, saturated: snapshot.saturated, now, all, project, delta }, pr => store.ciFailureObserved(pr));
|
|
403
406
|
if (url.searchParams.get("fragment") === "1") {
|
|
404
407
|
// The live region alone — the in-page swapper's diet. Same auth,
|
|
405
408
|
// same ceiling, no shell, no scripts (finding 2).
|
|
@@ -423,7 +426,7 @@ export function createTasksHandlers(runtime) {
|
|
|
423
426
|
const regionBody = html `<div id="board-region">${body}</div><p class="meta" id="board-region-stamp"></p>`;
|
|
424
427
|
return sendScreen(response, 200, screen("board", regionBody, {
|
|
425
428
|
chrome: chromeFor(project, "board", undefined, all ? "board-all" : undefined),
|
|
426
|
-
functional: { script: regionScript("board-region", "1"), fetches: true },
|
|
429
|
+
functional: { script: regionScript("board-region", "1", undefined, { animate: true }) + boardTickScript(), fetches: true },
|
|
427
430
|
}));
|
|
428
431
|
}
|
|
429
432
|
/** A completed (or attempted) row as the review list ranks it: its proof reasons, CI standing, and the assignment
|