toolroll 0.9.85 → 0.9.87
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 +83 -0
- package/dist/agent-fence.d.ts +6 -2
- package/dist/agent-fence.js +3 -2
- package/dist/agents-board.d.ts +12 -0
- package/dist/agents-board.js +26 -0
- package/dist/approval-rules-ui.d.ts +4 -1
- package/dist/approval-rules-ui.js +31 -15
- package/dist/assignment-ui.js +1 -1
- package/dist/assignment.js +4 -3
- package/dist/backup-ui.d.ts +5 -4
- package/dist/backup-ui.js +65 -32
- package/dist/browser/terminal.css +1 -1
- package/dist/browser/terminal.js +57 -54
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +35 -68
- package/dist/browser-crew.js +1 -1
- package/dist/browser-workspace.d.ts +363 -177
- package/dist/browser-workspace.js +10 -9
- package/dist/chat-actions.d.ts +11 -1
- package/dist/chat-actions.js +53 -1
- package/dist/chat-channel.js +1 -0
- package/dist/chat-controls.d.ts +1 -1
- package/dist/chat-controls.js +1 -1
- package/dist/chat-quiet.js +3 -2
- package/dist/chat-settings-page.d.ts +29 -9
- package/dist/chat-settings-page.js +59 -28
- package/dist/check-levels-ui.d.ts +2 -1
- package/dist/check-levels-ui.js +67 -58
- package/dist/claim.d.ts +3 -0
- package/dist/claim.js +4 -1
- package/dist/contracts/chat-actions.d.ts +9 -0
- package/dist/contracts/chat-actions.js +3 -0
- package/dist/contracts/cli.d.ts +43 -0
- package/dist/contracts/cli.js +4 -0
- package/dist/contracts/console-api.d.ts +4 -1
- package/dist/contracts/console-api.js +4 -1
- package/dist/contracts/flow-propose.d.ts +4 -1
- package/dist/contracts/flow-propose.js +1 -1
- package/dist/contracts/flow.d.ts +41 -0
- package/dist/contracts/flow.js +4 -2
- package/dist/contracts/lead-tools.d.ts +21 -0
- package/dist/contracts/lead-tools.js +2 -0
- package/dist/contracts/store-json.d.ts +2 -0
- package/dist/contracts/trigger-payloads.d.ts +20 -0
- package/dist/contracts/trigger-payloads.js +4 -0
- package/dist/coordinator.js +1 -0
- package/dist/credentials-ui.d.ts +5 -3
- package/dist/credentials-ui.js +58 -31
- package/dist/deploy-gate.js +4 -0
- package/dist/design/kit.css +685 -0
- package/dist/design/kit.d.ts +208 -0
- package/dist/design/kit.js +177 -0
- package/dist/design/tokens.css +19 -1
- package/dist/discord-settings.js +8 -4
- package/dist/dispatch.d.ts +3 -0
- package/dist/dispatch.js +4 -1
- package/dist/export-ui.d.ts +6 -4
- package/dist/export-ui.js +18 -8
- package/dist/flow-browser.d.ts +103 -0
- package/dist/flow-browser.js +204 -0
- package/dist/flow-changed.d.ts +14 -0
- package/dist/flow-changed.js +53 -0
- package/dist/flow-code.d.ts +5 -1
- package/dist/flow-code.js +4 -1
- package/dist/flow-engine.js +16 -0
- package/dist/flow-gallery-ui.d.ts +41 -4
- package/dist/flow-gallery-ui.js +95 -63
- package/dist/flow-live.js +9 -1
- package/dist/flow-pull-request.js +15 -7
- package/dist/flow-share.js +3 -1
- package/dist/flow-starters-ui.d.ts +3 -9
- package/dist/flow-starters-ui.js +12 -23
- package/dist/flow-steps.d.ts +9 -1
- package/dist/flow-steps.js +55 -5
- package/dist/flow-triggers.js +55 -0
- package/dist/flow-unattended-words.d.ts +20 -0
- package/dist/flow-unattended-words.js +24 -0
- package/dist/flow-unattended.d.ts +9 -5
- package/dist/flow-unattended.js +45 -23
- package/dist/flows-cli.d.ts +19 -0
- package/dist/flows-cli.js +54 -5
- package/dist/flows-ui.d.ts +12 -6
- package/dist/flows-ui.js +152 -32
- package/dist/flows.js +5 -2
- package/dist/integrations-ui.d.ts +6 -4
- package/dist/integrations-ui.js +34 -40
- package/dist/integrations.d.ts +2 -2
- package/dist/integrations.js +4 -4
- package/dist/kits-ui.d.ts +8 -10
- package/dist/kits-ui.js +29 -33
- package/dist/kits.js +1 -1
- package/dist/knowledge-ui.js +76 -19
- package/dist/lead-tools.js +35 -6
- package/dist/lead-voice.js +16 -5
- package/dist/ledger-view.d.ts +2 -0
- package/dist/ledger-view.js +41 -38
- package/dist/models-ui.d.ts +8 -3
- package/dist/models-ui.js +82 -59
- package/dist/monitoring-ui.d.ts +4 -2
- package/dist/monitoring-ui.js +44 -13
- package/dist/needs-you.d.ts +1 -1
- package/dist/needs-you.js +8 -0
- package/dist/operate-remote-arguments.js +1 -0
- package/dist/operate-remote.js +1 -1
- package/dist/operate.d.ts +1 -1
- package/dist/operate.js +249 -6
- package/dist/passwordless.d.ts +1 -1
- package/dist/passwordless.js +1 -1
- package/dist/people-audit-ui.d.ts +2 -1
- package/dist/people-audit-ui.js +25 -23
- package/dist/policy-ui.d.ts +3 -2
- package/dist/policy-ui.js +27 -21
- package/dist/policy.d.ts +3 -0
- package/dist/policy.js +14 -1
- package/dist/project-delete-ui.d.ts +4 -2
- package/dist/project-delete-ui.js +135 -126
- package/dist/project-delete.js +7 -3
- package/dist/project-knowledge.d.ts +10 -10
- package/dist/publish.d.ts +11 -0
- package/dist/publish.js +16 -3
- package/dist/pull-request-flow.js +35 -4
- package/dist/pull-request-ui.d.ts +2 -1
- package/dist/pull-request-ui.js +39 -28
- package/dist/recipe-ui.d.ts +4 -2
- package/dist/recipe-ui.js +14 -4
- package/dist/release-adapters.d.ts +3 -0
- package/dist/release-adapters.js +22 -2
- package/dist/release-cli.js +81 -9
- package/dist/release.d.ts +21 -0
- package/dist/release.js +40 -4
- package/dist/releases.js +1 -1
- package/dist/request-budget-ui.d.ts +4 -2
- package/dist/request-budget-ui.js +30 -19
- package/dist/result-follow-ups.js +4 -0
- package/dist/retention-ui.d.ts +4 -3
- package/dist/retention-ui.js +15 -12
- package/dist/scope.d.ts +8 -0
- package/dist/scope.js +13 -0
- package/dist/serve.d.ts +0 -1
- package/dist/serve.js +51 -7
- package/dist/server/agents-computers.d.ts +9 -0
- package/dist/server/agents-computers.js +67 -0
- package/dist/server/chat.js +20 -18
- package/dist/server/chrome.d.ts +1 -1
- package/dist/server/chrome.js +19 -9
- package/dist/server/console.css +31 -2
- package/dist/server/flows.js +79 -36
- package/dist/server/home.d.ts +35 -0
- package/dist/server/home.js +104 -0
- package/dist/server/http.d.ts +3 -0
- package/dist/server/http.js +5 -0
- package/dist/server/kit-standalone.d.ts +22 -0
- package/dist/server/kit-standalone.js +69 -0
- package/dist/server/kit.d.ts +405 -0
- package/dist/server/kit.js +361 -0
- package/dist/server/pages.d.ts +2 -1
- package/dist/server/pages.js +213 -174
- package/dist/server/people-tokens.js +26 -85
- package/dist/server/render-history.d.ts +51 -0
- package/dist/server/render-history.js +228 -0
- package/dist/server/render-new-task.d.ts +29 -0
- package/dist/server/render-new-task.js +96 -0
- package/dist/server/render-next.d.ts +63 -0
- package/dist/server/render-next.js +109 -0
- package/dist/server/render-pages.d.ts +35 -190
- package/dist/server/render-pages.js +154 -563
- package/dist/server/render-people.d.ts +24 -3
- package/dist/server/render-people.js +93 -72
- package/dist/server/render-projects.d.ts +103 -0
- package/dist/server/render-projects.js +247 -0
- package/dist/server/render-results.d.ts +5 -24
- package/dist/server/render-results.js +48 -185
- package/dist/server/render-settings.d.ts +5 -74
- package/dist/server/render-settings.js +102 -493
- package/dist/server/render-tasks.d.ts +43 -5
- package/dist/server/render-tasks.js +131 -91
- package/dist/server/route-table.js +31 -5
- package/dist/server/settings.js +276 -255
- package/dist/server/task-views.d.ts +8 -0
- package/dist/server/task-views.js +90 -2
- package/dist/server/tasks.d.ts +18 -0
- package/dist/server/tasks.js +148 -41
- package/dist/server/terminals.js +1 -1
- package/dist/server/welcome.d.ts +22 -0
- package/dist/server/welcome.js +69 -0
- package/dist/settings-groups-ui.d.ts +279 -0
- package/dist/settings-groups-ui.js +615 -0
- package/dist/settings-kit.d.ts +42 -6
- package/dist/settings-kit.js +69 -7
- package/dist/settings-proofs.js +1 -0
- package/dist/setup-guide.d.ts +2 -2
- package/dist/skills-ui.js +59 -7
- package/dist/slack-settings.js +12 -5
- package/dist/spend-ui.d.ts +45 -26
- package/dist/spend-ui.js +124 -56
- package/dist/sso-ui.d.ts +5 -2
- package/dist/sso-ui.js +63 -31
- package/dist/storage-ui.d.ts +3 -2
- package/dist/storage-ui.js +56 -50
- package/dist/store.d.ts +182 -5
- package/dist/store.js +512 -38
- package/dist/surface.js +23 -3
- package/dist/task-archive.d.ts +10 -0
- package/dist/task-archive.js +16 -0
- package/dist/task-control.d.ts +1 -1
- package/dist/teams-settings.d.ts +2 -1
- package/dist/teams-settings.js +24 -41
- package/dist/telegram-settings.d.ts +0 -4
- package/dist/telegram-settings.js +52 -25
- package/dist/tidy.d.ts +130 -0
- package/dist/tidy.js +503 -0
- package/dist/toolroll-update-ui.d.ts +8 -4
- package/dist/toolroll-update-ui.js +55 -45
- package/dist/tools-ui.d.ts +5 -3
- package/dist/tools-ui.js +107 -42
- package/dist/work-index.d.ts +24 -0
- package/dist/work-index.js +56 -30
- package/dist/workspace-ui.d.ts +1 -1
- package/dist/workspace-ui.js +8 -4
- package/package.json +1 -1
package/dist/server/pages.js
CHANGED
|
@@ -4,6 +4,7 @@ import { canReveal, cdCommandOf, localCheckoutOf } from "../checkout-proof.js";
|
|
|
4
4
|
import { instanceHttpAnswer } from "../console-instance.js";
|
|
5
5
|
import { handlersOf } from './handler-registry.js';
|
|
6
6
|
import { html, joinHtml, postForm, reauth } from "../html.js";
|
|
7
|
+
import * as kit from "./kit.js";
|
|
7
8
|
/** pages handlers, moved without changing their route bodies. */
|
|
8
9
|
import { createHmac, randomBytes } from "node:crypto";
|
|
9
10
|
import { realpathSync } from "node:fs";
|
|
@@ -15,7 +16,6 @@ import { listFolder, localOrigins } from "../folder-look.js";
|
|
|
15
16
|
import { Readable } from "node:stream";
|
|
16
17
|
import { pipeline } from "node:stream/promises";
|
|
17
18
|
import { setImmediate as yieldEventLoop } from "node:timers/promises";
|
|
18
|
-
import { INSTALLATION_SCOPE, resolvePhaseAgent } from "../agentconfig.js";
|
|
19
19
|
import { parseApiToken, tokenLive } from "../api-tokens.js";
|
|
20
20
|
import { classify } from "../board.js";
|
|
21
21
|
import { serveBrowserAsset } from '../browser-shell.js';
|
|
@@ -27,7 +27,8 @@ import { run as execRun } from "../exec.js";
|
|
|
27
27
|
import { computeGaps } from "../gaps.js";
|
|
28
28
|
import { LEDGER_CSV_HEADER, ledgerCsvRows } from "../ledger-csv.js";
|
|
29
29
|
import { ledgerBody } from "../ledger-view.js";
|
|
30
|
-
import {
|
|
30
|
+
import { agentRowOf, agentsListOf, agentFactsFor, sealedDiffOf } from "../agents-board.js";
|
|
31
|
+
import { computersOf, phasesOf, systemOf } from "./agents-computers.js";
|
|
31
32
|
import { markAgentSeen } from "../agent-board.js";
|
|
32
33
|
import { agentsLiveOf } from "../agents-live.js";
|
|
33
34
|
import { readChildWindow } from "../live-children-read.js";
|
|
@@ -48,17 +49,24 @@ import { serveInstallAsset } from "./pages-assets.js";
|
|
|
48
49
|
import { healthz } from "./remote-hooks.js";
|
|
49
50
|
import { slowRequestLines, SLOW_REQUEST_MS } from "../latency-timeline.js";
|
|
50
51
|
import { leadsStillAdmitted, leadTilesFor } from './agents-live-lead.js';
|
|
51
|
-
import { refuse, regionScript, screen, settingsRows, WORKSPACE_STYLE, workToolRows } from "./chrome.js";
|
|
52
|
+
import { refuse, regionScript, relativeAge as relativeAgeOf, screen, settingsRows, WORKSPACE_STYLE, workToolRows } from "./chrome.js";
|
|
52
53
|
import { redirect, respond, safeReturn, SAFETY, taskHref } from "./http.js";
|
|
53
54
|
import { assignmentOf } from "../assignment.js";
|
|
54
55
|
import { assignmentTaskStatusOf } from "../assignment-presentation.js";
|
|
55
|
-
import {
|
|
56
|
+
import { githubIdentityOf, homePage, portfolioOverview, startBuilderHtml, workbenchRail } from "./render-pages.js";
|
|
57
|
+
import { browsePage, githubReposPage, projectHref, projectPage, projectsPage, unknownProject } from "./render-projects.js";
|
|
58
|
+
import { notFound as kitNotFound } from "./kit.js";
|
|
59
|
+
import {} from "./render-pages.js";
|
|
60
|
+
import {} from "../project-outcomes.js";
|
|
61
|
+
import { CHECK_LEVEL_WORDS } from "../check-levels.js";
|
|
56
62
|
import { capsPage } from "./render-settings.js";
|
|
57
63
|
import { decisionAnswerScript, taskBody } from "./render-tasks.js";
|
|
58
64
|
import {} from "./session.js";
|
|
59
65
|
import { rereadBrowser } from "./live.js";
|
|
66
|
+
import { homeView } from "./home.js";
|
|
67
|
+
import { workIndexPage } from "../work-index.js";
|
|
60
68
|
export function createPagesHandlers(runtime) {
|
|
61
|
-
const { identities, clock, lastChecks, store, visible, consoleProjects, restricted, ceiling, options, projectFamilyPeek, sendScreen, chromeFor, unscopedMode, admissionList, taskRepoOf, taskViewData, runVisible, evidenceRoot, workAccess, identify, liveCeiling, sessions, deletedRepos, authenticateApprover, authenticateCredential, projectOf, lookupSession } = runtime;
|
|
69
|
+
const { identities, clock, lastChecks, store, visible, consoleProjects, restricted, ceiling, options, projectFamilyPeek, sendScreen, chromeFor, unscopedMode, admissionList, taskRepoOf, taskViewData, runVisible, evidenceRoot, workAccess, identify, liveCeiling, sessions, deletedRepos, authenticateApprover, authenticateCredential, projectOf, lookupSession, firstRunStepsNow } = runtime;
|
|
62
70
|
// The filesystem browser (operator request): pick a project folder by
|
|
63
71
|
// looking, not typing. CONFINED to exactly what opening allows —
|
|
64
72
|
// explicit --repo mode has an enumerable list and gets no browser at
|
|
@@ -98,14 +106,14 @@ export function createPagesHandlers(runtime) {
|
|
|
98
106
|
.slice(0, 200);
|
|
99
107
|
const root = browseRoots.find(one => canonical === one || canonical.startsWith(`${one}/`));
|
|
100
108
|
const parent = canonical === root ? null : canonical.slice(0, canonical.lastIndexOf("/")) || root;
|
|
101
|
-
const
|
|
109
|
+
const registered = new Set(store.listProjects().filter(one => visible(one.path)).map(one => one.path));
|
|
102
110
|
return sendScreen(response, 200, browsePage(chromeFor(who.session.project, "projects"), {
|
|
103
111
|
at: canonical,
|
|
104
112
|
root,
|
|
105
113
|
roots: browseRoots,
|
|
106
114
|
parent,
|
|
107
115
|
entries,
|
|
108
|
-
|
|
116
|
+
registered,
|
|
109
117
|
}));
|
|
110
118
|
}
|
|
111
119
|
// The signed-in gh account's repositories, OFFERED instead of typed
|
|
@@ -155,10 +163,10 @@ export function createPagesHandlers(runtime) {
|
|
|
155
163
|
listed,
|
|
156
164
|
local: localByIdentity,
|
|
157
165
|
registered,
|
|
158
|
-
csrf: who.via === "cookie" ? who.session.csrf : "",
|
|
159
166
|
cloneReady: ceiling.roots.length > 0,
|
|
160
167
|
openProject: who.via === "cookie" ? who.session.project ?? null : null,
|
|
161
168
|
localUnanswered,
|
|
169
|
+
now: clock(),
|
|
162
170
|
}));
|
|
163
171
|
}
|
|
164
172
|
// List repositories / Refresh list: ask the GitHub CLI signed in on this machine, then show the page.
|
|
@@ -171,17 +179,50 @@ export function createPagesHandlers(runtime) {
|
|
|
171
179
|
}
|
|
172
180
|
async function projectsIndex(ctx) {
|
|
173
181
|
const { url, who, response } = ctx;
|
|
174
|
-
return void projectsScreen(response, who, url.searchParams.get("said"), 200, safeReturn(url.searchParams.get("return")));
|
|
182
|
+
return void projectsScreen(response, who, url.searchParams.get("said"), 200, safeReturn(url.searchParams.get("return")), "said");
|
|
175
183
|
}
|
|
184
|
+
// Home (UI revamp wave B): what needs this person, four counts that agree with the Tasks filters, and the lead's box.
|
|
185
|
+
// A token or a restricted session keeps landing on Tasks; a first run with nothing filed yet keeps its checklist in the chat.
|
|
176
186
|
async function homeRedirect(ctx) {
|
|
177
|
-
const { who, response } = ctx;
|
|
178
|
-
|
|
187
|
+
const { who, response, now, project } = ctx;
|
|
188
|
+
if (who.via !== "cookie" || restricted())
|
|
189
|
+
return redirect(response, "/work");
|
|
190
|
+
if (firstRunStepsNow(now) !== null && !store.hasAnyWork())
|
|
191
|
+
return redirect(response, "/chat");
|
|
192
|
+
// One scope for the page and the chrome around it: the project its address names (proved by the router), else all.
|
|
193
|
+
const chrome = chromeFor(project, "none");
|
|
194
|
+
const projects = chrome.projects ?? [];
|
|
195
|
+
const view = homeView({ store, now, access: workAccess(), evidenceRoot, project, projects, chat: chrome.chat === true, canAnswer: who.role === "approver" });
|
|
196
|
+
return sendScreen(response, 200, screen("Home", html `<div data-kit-page><h1>Home</h1><p>${view.say}</p><p><a href="/work">Tasks</a></p></div>`, { chrome, workspace: { view } }));
|
|
197
|
+
}
|
|
198
|
+
// Search (UI revamp wave B): the ⌘K menu's "See all results". The places come from the page; the tasks here are the
|
|
199
|
+
// most recent ones in view (live and archived), matched on their title and id. Reads only.
|
|
200
|
+
async function searchPage(ctx) {
|
|
201
|
+
const { url, response, now } = ctx;
|
|
202
|
+
const q = (url.searchParams.get("q") ?? "").replace(/[\p{Cc}\p{Cf}]/gu, " ").trim().slice(0, 200);
|
|
203
|
+
const words = q.toLowerCase().split(/\s+/).filter(Boolean);
|
|
204
|
+
const access = workAccess();
|
|
205
|
+
const live = workIndexPage(store, now, access, { view: "all", limit: 100, root: evidenceRoot });
|
|
206
|
+
const archived = workIndexPage(store, now, access, { view: "archived", limit: 40, root: evidenceRoot });
|
|
207
|
+
const multi = new Set(live.items.map(one => one.repo ?? "")).size > 1;
|
|
208
|
+
const tasks = words.length === 0 ? [] : [...live.items, ...archived.items]
|
|
209
|
+
.filter(one => words.every(word => `${one.title} ${one.rootId}`.toLowerCase().includes(word)))
|
|
210
|
+
.slice(0, 60)
|
|
211
|
+
.map(one => {
|
|
212
|
+
const waits = one.status.views.includes("needs-you");
|
|
213
|
+
const tone = one.archived !== undefined ? "" : waits ? "needs"
|
|
214
|
+
: { attention: "needs", ready: "needs", live: "live", done: "done", problem: "failed", muted: "", neutral: "" }[one.status.tone];
|
|
215
|
+
return { id: one.rootId, title: one.title, href: taskHref(one.rootId), project: multi || one.repo === null ? (one.repo === null ? "Unplaced" : projectName(one.repo)) : null,
|
|
216
|
+
age: relativeAgeOf(one.archived?.at ?? one.updatedAt, now), chip: { word: one.archived !== undefined ? "Archived" : waits ? "Needs you" : one.status.label, tone }, archived: one.archived !== undefined };
|
|
217
|
+
});
|
|
218
|
+
const view = { kind: "search", q, tasks, truncated: live.nextCursor !== null || archived.nextCursor !== null };
|
|
219
|
+
return sendScreen(response, 200, screen("Search", html `<div data-kit-page><h1>Search</h1><form action="/search"><input type="search" name="q" value="${q}" aria-label="Search"></form></div>`, { chrome: chromeFor(null, "none"), workspace: { view } }));
|
|
179
220
|
}
|
|
180
221
|
// The board's old name; bookmarks keep working. A GET-only alias, so a
|
|
181
222
|
// plain 302 — never the shared 303 helper, which belongs to POST landings.
|
|
182
223
|
async function morningAlias(ctx) {
|
|
183
224
|
const { response } = ctx;
|
|
184
|
-
response.writeHead(302, { ...SAFETY, Location: "/
|
|
225
|
+
response.writeHead(302, { ...SAFETY, Location: "/" });
|
|
185
226
|
response.end();
|
|
186
227
|
return;
|
|
187
228
|
}
|
|
@@ -309,70 +350,24 @@ export function createPagesHandlers(runtime) {
|
|
|
309
350
|
const rows = store.actionLedger({ ...query, ...(before === null ? {} : { before: Number(before) }), limit: 51 });
|
|
310
351
|
if (url.searchParams.get("format") === "json")
|
|
311
352
|
return respond(response, 200, "application/json; charset=utf-8", JSON.stringify(checkResponse("ledgerPage", { chain: { ok: report.ok, entries: report.entries, through: report.through, head: report.head, problem: report.problem }, entries: rows.slice(0, 50), nextBefore: rows.length > 50 ? rows[49].id : null })));
|
|
312
|
-
return sendScreen(response, 200, screen("
|
|
353
|
+
return sendScreen(response, 200, screen("Audit log", ledgerBody(rows, projects, url.searchParams, chainView), { chrome: chromeFor(chosen === "" ? null : chosen, "ledger", undefined, chosen === "" ? "all" : "project") }));
|
|
313
354
|
}
|
|
355
|
+
// The activity summary lives on Home now (UI revamp wave B); the old address keeps answering, as a GET alias.
|
|
314
356
|
async function activityPage(ctx) {
|
|
315
|
-
const {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
taskCount: store.listTasksScoped(project, undefined, 1, null).length,
|
|
320
|
-
repo: project,
|
|
321
|
-
summary: tally(store.runsSinceScoped(since, project)),
|
|
322
|
-
decisions: store.listDecisionsScoped(project),
|
|
323
|
-
incidents: store.openIncidents(project),
|
|
324
|
-
stranded: store.strandedTasks(project),
|
|
325
|
-
gaps: project === null ? null : computeGaps(store, project, now),
|
|
326
|
-
// Rows that want a person, plus task updates the live pairing failed to send (never quiet progress).
|
|
327
|
-
outboxPending: store.pendingForAttention().length,
|
|
328
|
-
settings: options.telegramTokenFile !== undefined,
|
|
329
|
-
building: store.liveClaims(project, now),
|
|
330
|
-
runners: store.listRunners(),
|
|
331
|
-
worktrees: store
|
|
332
|
-
.listWorktrees()
|
|
333
|
-
.filter(one => project === null || one.repo === project || identities.hot(one.repo) === identities.hot(project)),
|
|
334
|
-
episode: project === null ? null : store.latestWatchEpisode(project),
|
|
335
|
-
now,
|
|
336
|
-
}));
|
|
337
|
-
}
|
|
338
|
-
async function systemScreen(ctx) {
|
|
339
|
-
const { response, now, project } = ctx;
|
|
340
|
-
const since = new Date(now.getTime() - 24 * 60 * 60_000).toISOString();
|
|
341
|
-
void since;
|
|
342
|
-
return sendScreen(response, 200, systemPage(chromeFor(project, "system"), {
|
|
343
|
-
agents: ["plan", "build", "repair", "review"].map(phase => {
|
|
344
|
-
const answer = resolvePhaseAgent(store, phase, project, {});
|
|
345
|
-
const row = (project === null ? null : store.phaseConfig(project, phase)) ??
|
|
346
|
-
store.phaseConfig(INSTALLATION_SCOPE, phase);
|
|
347
|
-
return {
|
|
348
|
-
phase,
|
|
349
|
-
...(answer.ok
|
|
350
|
-
? { provider: answer.spec.provider, model: answer.spec.model, source: answer.source }
|
|
351
|
-
: { problem: answer.problem }),
|
|
352
|
-
setBy: row?.updatedBy ?? null,
|
|
353
|
-
};
|
|
354
|
-
}),
|
|
355
|
-
building: store.liveClaims(project, now),
|
|
356
|
-
runners: store.listRunners(),
|
|
357
|
-
worktrees: store.listWorktrees().filter(one => project === null || one.repo === project || identities.hot(one.repo) === identities.hot(project)),
|
|
358
|
-
episode: project === null ? null : store.latestWatchEpisode(project),
|
|
359
|
-
outboxPending: store.pendingForAttention().length,
|
|
360
|
-
// External work at a glance (arc 3 finding 24): every dispatch
|
|
361
|
-
// grant with its blocked state, plus any open sync episode — the
|
|
362
|
-
// page a sync-failed push deep-links to now shows the fact.
|
|
363
|
-
externalWork: store
|
|
364
|
-
.listGrants()
|
|
365
|
-
.filter(one => one.dispatch === true && one.remoteRepo != null && visible(one.repo))
|
|
366
|
-
.map(one => ({
|
|
367
|
-
remoteRepo: String(one.remoteRepo),
|
|
368
|
-
blocked: one.dispatchBlocked ?? null,
|
|
369
|
-
openEpisode: store
|
|
370
|
-
.listNotifications("all")
|
|
371
|
-
.find(n => n.kind === "sync-failed" && n.resolvedAt === null && n.dedupeKey.startsWith(`sync:${one.remoteRepo}:`))?.subject ?? null,
|
|
372
|
-
})),
|
|
373
|
-
now,
|
|
374
|
-
}));
|
|
357
|
+
const { response } = ctx;
|
|
358
|
+
response.writeHead(302, { ...SAFETY, Location: "/" });
|
|
359
|
+
response.end();
|
|
360
|
+
return;
|
|
375
361
|
}
|
|
362
|
+
// A GET-only alias, a plain 302 (like morningAlias): an old address lands on the part of Agents that took it over.
|
|
363
|
+
const aliasTo = (location) => async (ctx) => {
|
|
364
|
+
ctx.response.writeHead(302, { ...SAFETY, Location: location });
|
|
365
|
+
ctx.response.end();
|
|
366
|
+
};
|
|
367
|
+
// System became part of Agents (UI revamp wave B): the model per phase and the computers that build.
|
|
368
|
+
const systemScreen = aliasTo("/agents#models");
|
|
369
|
+
// Terminals live on their own page (/term, its own bundle); the Agents area's address for them leads there.
|
|
370
|
+
const agentsTerminals = aliasTo("/term");
|
|
376
371
|
// Peek became the Agents board: one place to check on every running agent (agents-board.ts).
|
|
377
372
|
async function peekPage(ctx) {
|
|
378
373
|
return redirect(ctx.response, "/agents");
|
|
@@ -431,6 +426,13 @@ export function createPagesHandlers(runtime) {
|
|
|
431
426
|
}
|
|
432
427
|
return { task, open };
|
|
433
428
|
}
|
|
429
|
+
/** After a retire: fixed words naming a computer the store knows as retired (never text from the address). */
|
|
430
|
+
function retiredSaid(name) {
|
|
431
|
+
if (name === null)
|
|
432
|
+
return null;
|
|
433
|
+
const known = store.listRunners().find(one => one.name === name && one.retiredAt !== null);
|
|
434
|
+
return known === undefined ? null : { tone: "info", text: `${known.name} is retired. It takes no more work; tasks reserved for it wait until you move them.` };
|
|
435
|
+
}
|
|
434
436
|
// The Agents board (one place to check on running agents): rows by state, and the opened agent's live view.
|
|
435
437
|
async function agentsPage(ctx) {
|
|
436
438
|
const { url, who, response, project } = ctx;
|
|
@@ -489,11 +491,21 @@ export function createPagesHandlers(runtime) {
|
|
|
489
491
|
const view = { kind: "agents", groups: [], selected: null, lanes: null, empty: null, live: true };
|
|
490
492
|
return sendScreen(response, 200, screen("Agents", html `<h1>Agents</h1><p class="meta">Live needs JavaScript.</p>`, { chrome: chromeFor(project, "runs"), workspace: { view } }));
|
|
491
493
|
}
|
|
492
|
-
const groups =
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
494
|
+
const groups = agentsListOf(store, now, access, who.name, evidenceRoot);
|
|
495
|
+
// The installation's parts (what /fleet and /system showed) are for a person with access to all of it, as those
|
|
496
|
+
// pages were; a project-limited person sees their own agents only.
|
|
497
|
+
const installation = !restricted();
|
|
498
|
+
const windowAsked = url.searchParams.get("window");
|
|
499
|
+
const view = { kind: "agents", groups, selected, lanes: null,
|
|
500
|
+
empty: groups.length === 0 ? "No agents in the last day. They show here the moment one starts." : null,
|
|
501
|
+
window: windowAsked === "30m" || windowAsked === "2h" ? windowAsked : "24h",
|
|
502
|
+
...(selected !== null || !installation ? {} : {
|
|
503
|
+
computers: computersOf(store, now, visible, who.via === "cookie" && who.role === "approver" && !store.isDemo()),
|
|
504
|
+
phases: phasesOf(store, project),
|
|
505
|
+
system: systemOf(store, project, one => project === null || one.repo === project || identities.hot(one.repo) === identities.hot(project), visible),
|
|
506
|
+
computerSaid: retiredSaid(url.searchParams.get("retired")),
|
|
507
|
+
}),
|
|
508
|
+
terminals: who.via === "cookie" && who.role === "approver" && !store.isDemo() && store.isInstanceOperator(who.name) };
|
|
497
509
|
const fallback = joinHtml([
|
|
498
510
|
html `<h1>Agents</h1>`,
|
|
499
511
|
groups.length === 0 ? html `<p class="meta">${view.empty ?? ""}</p>` : joinHtml(groups.map(group => joinHtml([
|
|
@@ -501,39 +513,13 @@ export function createPagesHandlers(runtime) {
|
|
|
501
513
|
...group.rows.map(row => html `<p class="row"><a href="${row.href}"><strong>${row.title}</strong></a> <span class="meta">${[row.label, row.line].filter(Boolean).join(" \u00b7 ")}</span></p>`),
|
|
502
514
|
], "\n")), "\n"),
|
|
503
515
|
], "\n");
|
|
504
|
-
|
|
516
|
+
// The list reads itself while something builds, as /system did (forms being edited are kept); an opened agent
|
|
517
|
+
// follows its own room instead.
|
|
518
|
+
const refreshSeconds = selected !== null ? undefined : store.liveClaims(null, now).length > 0 ? 10 : installation ? 60 : undefined;
|
|
519
|
+
return sendScreen(response, 200, screen("Agents", fallback, { chrome: chromeFor(project, "runs"), workspace: { view }, ...(refreshSeconds === undefined ? {} : { refreshSeconds }) }));
|
|
505
520
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
// Cross-project by design: which agent is on which project is the
|
|
509
|
-
// question, so this screen is a survey — reads are still filtered
|
|
510
|
-
// through the ceiling before a card renders. No project needed.
|
|
511
|
-
const queued = store.fleetQueue(clock());
|
|
512
|
-
const building = store.liveClaims(null, clock());
|
|
513
|
-
const owned = new Set(queued.map(one => one.assignedRunner).filter((one) => one !== null));
|
|
514
|
-
const runners = store.listRunners().filter(one => one.retiredAt === null || owned.has(one.name));
|
|
515
|
-
const body = fleetBody(queued, building, runners, store.queueRevision(), visible);
|
|
516
|
-
if (url.searchParams.get("fragment") === "1") {
|
|
517
|
-
return respond(response, 200, "text/html; charset=utf-8", body);
|
|
518
|
-
}
|
|
519
|
-
const said = url.searchParams.get("said");
|
|
520
|
-
const fleetScreen = screen("fleet", joinHtml([
|
|
521
|
-
html `<h1>Fleet</h1>`,
|
|
522
|
-
html `<p class="hint">one lane per worker — drag a queued card onto another worker to re-reserve it</p>`,
|
|
523
|
-
...(said === null ? [] : [html `<p class="meta">${said}</p>`]),
|
|
524
|
-
html `<div id="fleet-region">${body}</div>`,
|
|
525
|
-
html `<p class="meta" id="fleet-region-stamp"></p>`,
|
|
526
|
-
html `<h2>Register a worker</h2>`,
|
|
527
|
-
postForm("/fleet/runner/register", html `<label>Name<input type="text" name="name" placeholder="builder-2" maxlength="60" required></label><label>Capacity<input type="text" name="capacity" inputmode="numeric" value="1" aria-label="capacity"></label><label>Your password, typed again<input type="password" name="token" autocomplete="current-password" required></label><button type="submit">Register — its token is shown once</button>`, { attrs: { class: "card" } }),
|
|
528
|
-
html `<details class="arm-danger"><summary>Retire a worker</summary>${postForm("/fleet/runner/retire", html `<label>Which worker<input type="text" name="name" placeholder="builder-1" required></label>${reauth(html `<label>Your password, typed again<input type="password" name="token" autocomplete="current-password" required></label>`)}<button type="submit" class="danger">Retire it</button>`, { attrs: { class: "card" } })}</details>`,
|
|
529
|
-
], "\n"),
|
|
530
|
-
// The password fields make this screen sensitive: sendScreen strips
|
|
531
|
-
// the chrome additions and keeps the reorder poller — the named
|
|
532
|
-
// functional exception (it never reads the fields).
|
|
533
|
-
{ chrome: chromeFor(project, "fleet", undefined, "all"), functional: { script: fleetScript(), fetches: true } });
|
|
534
|
-
return sendScreen(response, 200, fleetScreen);
|
|
535
|
-
}
|
|
536
|
-
// v103: every ledger entry in a date range, sealed, with an evidence pack for each task it names.
|
|
521
|
+
// Fleet became the computers on Agents (UI revamp wave B): register, retire, reserved work and notes are there.
|
|
522
|
+
const fleetPage = aliasTo("/agents#computers");
|
|
537
523
|
async function ledgerExport(ctx) {
|
|
538
524
|
const { url, who, request, response, now } = ctx;
|
|
539
525
|
const from = exportDay(url.searchParams.get("from")), last = exportDay(url.searchParams.get("to"));
|
|
@@ -689,29 +675,29 @@ export function createPagesHandlers(runtime) {
|
|
|
689
675
|
const decidedOn = identities.generation();
|
|
690
676
|
const resolved = asked === "" ? null : await identities.resolveNow(asked);
|
|
691
677
|
if (resolved === "no-answer") {
|
|
692
|
-
return void projectsScreen(response, who, "that folder didn't answer — it may be on a drive that's disconnected, or waiting on macOS; try again in a moment", 503, safeReturn(body.get("return")));
|
|
678
|
+
return void projectsScreen(response, who, "that folder didn't answer — it may be on a drive that's disconnected, or waiting on macOS; try again in a moment", 503, safeReturn(body.get("return")), "open");
|
|
693
679
|
}
|
|
694
680
|
const canonical = resolved;
|
|
695
681
|
if (canonical === null) {
|
|
696
|
-
return void projectsScreen(response, who, "that path does not exist on this server", 400, safeReturn(body.get("return")));
|
|
682
|
+
return void projectsScreen(response, who, "that path does not exist on this server", 400, safeReturn(body.get("return")), "open");
|
|
697
683
|
}
|
|
698
684
|
// Authorization is the ceiling, then the path must actually be a git
|
|
699
685
|
// repository — validation with direct argv and a bound, never a shell.
|
|
700
686
|
// Both checks apply even to configured repos: naming a directory in
|
|
701
687
|
// config authorizes it; only being a repository makes it openable.
|
|
702
688
|
if (!(await authorizedProject(liveCeiling(), canonical))) {
|
|
703
|
-
return void projectsScreen(response, who, "that path is outside what this server was configured to serve", 403, safeReturn(body.get("return")));
|
|
689
|
+
return void projectsScreen(response, who, "that path is outside what this server was configured to serve", 403, safeReturn(body.get("return")), "open");
|
|
704
690
|
}
|
|
705
691
|
if (!(await isGitRepo(canonical))) {
|
|
706
|
-
return void projectsScreen(response, who, "that path is not a git repository", 400, safeReturn(body.get("return")));
|
|
692
|
+
return void projectsScreen(response, who, "that path is not a git repository", 400, safeReturn(body.get("return")), "open");
|
|
707
693
|
}
|
|
708
694
|
// An account limited to listed projects never widens its own list.
|
|
709
695
|
if (!visible(canonical)) {
|
|
710
|
-
return void projectsScreen(response, who, "That project is outside your access.", 403, safeReturn(body.get("return")));
|
|
696
|
+
return void projectsScreen(response, who, "That project is outside your access.", 403, safeReturn(body.get("return")), "open");
|
|
711
697
|
}
|
|
712
698
|
// An identity that changed during the awaits above undoes what they proved.
|
|
713
699
|
if (identities.generation() !== decidedOn)
|
|
714
|
-
return void projectsScreen(response, who, "a project folder changed while this was being checked — try again", 409, safeReturn(body.get("return")));
|
|
700
|
+
return void projectsScreen(response, who, "a project folder changed while this was being checked — try again", 409, safeReturn(body.get("return")), "open");
|
|
715
701
|
// Opening an allowed repository admits it the same way `repos add` does
|
|
716
702
|
// (project-admission.ts). A co-located `up` notices the durable list and
|
|
717
703
|
// connects its builder; there is no separate restart or binding step.
|
|
@@ -719,11 +705,11 @@ export function createPagesHandlers(runtime) {
|
|
|
719
705
|
const stillAllowed = (repo) => identities.generation() === decidedOn && visible(repo);
|
|
720
706
|
const admitted = await admitProject(store, { registryFile: options.registryPath ?? null, path: canonical, actor: { label: who.name, origin: "console" }, now, stillAllowed });
|
|
721
707
|
if (!admitted.ok) {
|
|
722
|
-
return void projectsScreen(response, who, admitted.reason === "not-git" ? "that path is not a git repository" : `that project is valid, but it could not be added — ${admitted.message}`, admitted.reason === "moved" ? 409 : 400, safeReturn(body.get("return")));
|
|
708
|
+
return void projectsScreen(response, who, admitted.reason === "not-git" ? "that path is not a git repository" : `that project is valid, but it could not be added — ${admitted.message}`, admitted.reason === "moved" ? 409 : 400, safeReturn(body.get("return")), "open");
|
|
723
709
|
}
|
|
724
710
|
// And once more before the session follows it.
|
|
725
711
|
if (!stillAllowed(admitted.repo))
|
|
726
|
-
return void projectsScreen(response, who, "a project folder changed while this was being checked — try again", 409, safeReturn(body.get("return")));
|
|
712
|
+
return void projectsScreen(response, who, "a project folder changed while this was being checked — try again", 409, safeReturn(body.get("return")), "open");
|
|
727
713
|
deletedRepos.delete(admitted.repo);
|
|
728
714
|
who.session.project = admitted.repo;
|
|
729
715
|
who.session.projectRevision++;
|
|
@@ -744,26 +730,25 @@ export function createPagesHandlers(runtime) {
|
|
|
744
730
|
const token = body.get("token") ?? "";
|
|
745
731
|
// A worker credential is minted here: a credential act, a password typed now even with Passwordless on.
|
|
746
732
|
if (!authenticateCredential(who, token).ok) {
|
|
747
|
-
return refuse(response, who, 403, "registering a worker takes your password, typed again", "/
|
|
733
|
+
return refuse(response, who, 403, "registering a worker takes your password, typed again", "/agents#computers");
|
|
748
734
|
}
|
|
749
735
|
const name = (body.get("name") ?? "").trim();
|
|
750
736
|
if (name === "" || name.length > 60 || /[\r\n\t]/.test(name) || hasForbiddenControls(name)) {
|
|
751
|
-
return refuse(response, who, 400, "a worker's name is one short line, no control characters", "/
|
|
737
|
+
return refuse(response, who, 400, "a worker's name is one short line, no control characters", "/agents#computers");
|
|
752
738
|
}
|
|
753
739
|
const capacity = Number((body.get("capacity") ?? "1").trim() || "1");
|
|
754
740
|
if (!Number.isInteger(capacity) || capacity < 1 || capacity > 64) {
|
|
755
|
-
return refuse(response, who, 400, "capacity is a whole number of tasks, 1 to 64", "/
|
|
741
|
+
return refuse(response, who, 400, "capacity is a whole number of tasks, 1 to 64", "/agents#computers");
|
|
756
742
|
}
|
|
757
743
|
const { token: minted } = registerRunner(store, { name, host: hostname(), capacity, now });
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
html `<
|
|
762
|
-
|
|
763
|
-
html `<
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
], "\n"),
|
|
744
|
+
// A kit reading page (UI revamp wave B), back to the computers on Agents. The token is text to select: a Copy
|
|
745
|
+
// button would need a script, and this page carries none.
|
|
746
|
+
const tokenScreen = screen("fleet", html `<div data-kit-page>${kit.phoneHeader("Computer registered", { back: "/agents#computers" })}${kit.doc(kit.docHeader({ title: `${name} is registered`, back: ["/agents#computers", "Agents"], same: true, meta: html `Takes up to ${capacity} ${capacity === 1 ? "task" : "tasks"} at a time` }), kit.slots({
|
|
747
|
+
status: html `<h2>Its token, shown once</h2>`,
|
|
748
|
+
text: "Only a hash is kept, so copy it now.",
|
|
749
|
+
acts: html `<code class="k-code" data-runner-token>${minted}</code>`,
|
|
750
|
+
terms: "Keep it beside the computer, in a file only you can read (0600) or a secrets manager. If it's lost, register the name again; its old claims are taken back.",
|
|
751
|
+
}), kit.acts(kit.btn("Back to Agents", { href: "/agents#computers" })))}</div>`,
|
|
767
752
|
// A one-time secret on screen: no script of any kind rides along.
|
|
768
753
|
{ chrome: chromeFor(projectOf(who, request) ?? null, "fleet"), forceSensitive: true });
|
|
769
754
|
return sendScreen(response, 200, tokenScreen);
|
|
@@ -775,13 +760,14 @@ export function createPagesHandlers(runtime) {
|
|
|
775
760
|
return refuse(response, who, 403, "runner retirement is a browser surface");
|
|
776
761
|
const token = body.get("token") ?? "";
|
|
777
762
|
if (!authenticateApprover(who, token).ok) {
|
|
778
|
-
return refuse(response, who, 403, "retiring a worker takes your password, typed again", "/
|
|
763
|
+
return refuse(response, who, 403, "retiring a worker takes your password, typed again", "/agents#computers");
|
|
779
764
|
}
|
|
780
765
|
const name = (body.get("name") ?? "").trim();
|
|
781
766
|
const retired = store.retireRunner(name, now);
|
|
782
767
|
if (!retired)
|
|
783
|
-
return refuse(response, who, 404, `no worker \`${name}\``, "/
|
|
784
|
-
|
|
768
|
+
return refuse(response, who, 404, `no worker \`${name}\``, "/agents#computers");
|
|
769
|
+
// Agents says it in fixed words, naming the computer only once the store knows it as retired (retiredSaid).
|
|
770
|
+
return redirect(response, `/agents?retired=${encodeURIComponent(name)}#computers`);
|
|
785
771
|
}
|
|
786
772
|
async function onboardPreview(ctx) {
|
|
787
773
|
const { who, response, posted } = ctx;
|
|
@@ -795,24 +781,24 @@ export function createPagesHandlers(runtime) {
|
|
|
795
781
|
if (process.platform === "win32")
|
|
796
782
|
return refuse(response, who, 403, "adding from GitHub is not supported on Windows yet");
|
|
797
783
|
if (ceiling.roots.length === 0)
|
|
798
|
-
return projectsScreen(response, who, "naming where repositories live takes --project-root", 400);
|
|
784
|
+
return projectsScreen(response, who, "naming where repositories live takes --project-root", 400, "/", "link");
|
|
799
785
|
const shape = parseGithubRepo(body.get("repo") ?? "");
|
|
800
786
|
if (!shape.ok)
|
|
801
|
-
return projectsScreen(response, who, shape.problem, 400);
|
|
787
|
+
return projectsScreen(response, who, shape.problem, 400, "/", "link");
|
|
802
788
|
// Roots by INDEX (finding 13) — a posted path is never accepted.
|
|
803
789
|
const rootIndex = Number(body.get("root") ?? "0");
|
|
804
790
|
const rootRaw = Number.isInteger(rootIndex) ? ceiling.roots[rootIndex] : undefined;
|
|
805
791
|
if (rootRaw === undefined)
|
|
806
|
-
return projectsScreen(response, who, "pick one of the configured roots", 400);
|
|
792
|
+
return projectsScreen(response, who, "pick one of the configured roots", 400, "/", "link");
|
|
807
793
|
const root = await identities.resolveNow(rootRaw);
|
|
808
794
|
if (root === null || root === "no-answer")
|
|
809
|
-
return projectsScreen(response, who, "that projects root does not resolve right now", 400);
|
|
795
|
+
return projectsScreen(response, who, "that projects root does not resolve right now", 400, "/", "link");
|
|
810
796
|
const previewed = await (options.ghPreview ?? previewGithubRepo)(shape.owner, shape.name);
|
|
811
797
|
if (!previewed.ok)
|
|
812
|
-
return projectsScreen(response, who, previewed.message, 400);
|
|
798
|
+
return projectsScreen(response, who, previewed.message, 400, "/", "link");
|
|
813
799
|
const reparsed = parseGithubRepo(previewed.preview.nameWithOwner);
|
|
814
800
|
if (!reparsed.ok)
|
|
815
|
-
return projectsScreen(response, who, "GitHub named a repository shape this console refuses", 400);
|
|
801
|
+
return projectsScreen(response, who, "GitHub named a repository shape this console refuses", 400, "/", "link");
|
|
816
802
|
// The hook boundary is NORMALIZED like the real preview would be
|
|
817
803
|
// (verification finding 2): a size that is not a nonnegative finite
|
|
818
804
|
// number is unknown, and unknown is LARGE.
|
|
@@ -821,7 +807,7 @@ export function createPagesHandlers(runtime) {
|
|
|
821
807
|
: null;
|
|
822
808
|
const target = join(root, reparsed.name);
|
|
823
809
|
if (dirname(target) !== root)
|
|
824
|
-
return projectsScreen(response, who, "the target escaped its root — refused", 400);
|
|
810
|
+
return projectsScreen(response, who, "the target escaped its root — refused", 400, "/", "link");
|
|
825
811
|
// The session-held record (finding 14): swept at mint, capped 3.
|
|
826
812
|
const session = who.session;
|
|
827
813
|
session.onboard ??= new Map();
|
|
@@ -830,7 +816,7 @@ export function createPagesHandlers(runtime) {
|
|
|
830
816
|
if (record.mintedAt < cutoff)
|
|
831
817
|
session.onboard.delete(key);
|
|
832
818
|
if (session.onboard.size >= 3)
|
|
833
|
-
return projectsScreen(response, who, "three previews are already waiting — confirm or let one expire", 400);
|
|
819
|
+
return projectsScreen(response, who, "three previews are already waiting — confirm or let one expire", 400, "/", "link");
|
|
834
820
|
const nonce = randomBytes(16).toString("hex");
|
|
835
821
|
session.onboard.set(nonce, {
|
|
836
822
|
nameWithOwner: previewed.preview.nameWithOwner,
|
|
@@ -840,7 +826,7 @@ export function createPagesHandlers(runtime) {
|
|
|
840
826
|
large: isLargeRepo({ ...previewed.preview, diskUsageKib }),
|
|
841
827
|
mintedAt: Date.now(),
|
|
842
828
|
});
|
|
843
|
-
return projectsScreen(response, who, null, 200);
|
|
829
|
+
return projectsScreen(response, who, null, 200, "/", "link");
|
|
844
830
|
}
|
|
845
831
|
async function onboardConfirm(ctx) {
|
|
846
832
|
const { who, request, response, posted, now } = ctx;
|
|
@@ -854,13 +840,13 @@ export function createPagesHandlers(runtime) {
|
|
|
854
840
|
if (process.platform === "win32")
|
|
855
841
|
return refuse(response, who, 403, "adding from GitHub is not supported on Windows yet");
|
|
856
842
|
if (ceiling.roots.length === 0)
|
|
857
|
-
return projectsScreen(response, who, "naming where repositories live takes --project-root", 400);
|
|
843
|
+
return projectsScreen(response, who, "naming where repositories live takes --project-root", 400, "/", "clone");
|
|
858
844
|
// CONFIRM: password, then session liveness AGAIN via lookupSession
|
|
859
845
|
// (finding 22/33) — the record is consumed from the RETURNED live
|
|
860
846
|
// session, synchronously, before the first await.
|
|
861
847
|
const password = body.get("token") ?? "";
|
|
862
848
|
if (!authenticateApprover(who, password).ok) {
|
|
863
|
-
return projectsScreen(response, who, "cloning takes your password, typed again", 403);
|
|
849
|
+
return projectsScreen(response, who, "cloning takes your password, typed again", 403, "/", "clone");
|
|
864
850
|
}
|
|
865
851
|
const cookieId = /(?:^|;\s*)standing-orders_session=([0-9a-f]{64})/.exec(request.headers.cookie ?? "")?.[1];
|
|
866
852
|
const live = cookieId === undefined ? null : lookupSession(cookieId, false);
|
|
@@ -870,53 +856,53 @@ export function createPagesHandlers(runtime) {
|
|
|
870
856
|
const record = live.onboard?.get(nonce);
|
|
871
857
|
if (record === undefined || record.mintedAt < Date.now() - 10 * 60_000) {
|
|
872
858
|
live.onboard?.delete(nonce);
|
|
873
|
-
return projectsScreen(response, who, "that preview expired or was already used — preview again", 400);
|
|
859
|
+
return projectsScreen(response, who, "that preview expired or was already used — preview again", 400, "/", "clone");
|
|
874
860
|
}
|
|
875
861
|
live.onboard?.delete(nonce); // consumed BEFORE the first await
|
|
876
862
|
// Identities as this confirmation found them: the admission below re-checks against them (project-identity.ts).
|
|
877
863
|
const decidedOn = identities.generation();
|
|
878
864
|
if (record.large && body.get("big-ok") !== "1") {
|
|
879
|
-
return projectsScreen(response, who, "that repository is large (or its size is unknown) — tick the box to clone it anyway", 400);
|
|
865
|
+
return projectsScreen(response, who, "that repository is large (or its size is unknown) — tick the box to clone it anyway", 400, "/", "clone");
|
|
880
866
|
}
|
|
881
867
|
const rootRaw = ceiling.roots[record.rootIndex];
|
|
882
868
|
if (rootRaw === undefined)
|
|
883
|
-
return projectsScreen(response, who, "the configured roots changed — preview again", 400);
|
|
869
|
+
return projectsScreen(response, who, "the configured roots changed — preview again", 400, "/", "clone");
|
|
884
870
|
const root = await identities.resolveNow(rootRaw);
|
|
885
871
|
if (root === null || root === "no-answer")
|
|
886
|
-
return projectsScreen(response, who, "that projects root does not resolve right now", 400);
|
|
872
|
+
return projectsScreen(response, who, "that projects root does not resolve right now", 400, "/", "clone");
|
|
887
873
|
if (dirname(record.target) !== root)
|
|
888
|
-
return projectsScreen(response, who, "the configured roots changed — preview again", 400);
|
|
874
|
+
return projectsScreen(response, who, "the configured roots changed — preview again", 400, "/", "clone");
|
|
889
875
|
const cloned = await (options.ghClone ?? cloneGithubRepo)(record.nameWithOwner, root, async (cwd) => {
|
|
890
876
|
const answer = await execRun("git", ["rev-parse", "--show-toplevel"], { cwd, timeoutMs: 15_000 });
|
|
891
877
|
return { code: answer.code, stdout: answer.stdout };
|
|
892
878
|
});
|
|
893
879
|
if (!cloned.ok)
|
|
894
|
-
return projectsScreen(response, who, cloned.message, 400);
|
|
880
|
+
return projectsScreen(response, who, cloned.message, 400, "/", "clone");
|
|
895
881
|
// The clone answer is BOUND to the record's exact target (verification
|
|
896
882
|
// finding 2): whatever produced it — the real primitive or a test
|
|
897
883
|
// hook — an answer naming any other path never enrolls or opens.
|
|
898
884
|
if (cloned.target !== record.target) {
|
|
899
|
-
return projectsScreen(response, who, "the clone answered with a different path than the preview promised — refused; nothing was enrolled", 400);
|
|
885
|
+
return projectsScreen(response, who, "the clone answered with a different path than the preview promised — refused; nothing was enrolled", 400, "/", "clone");
|
|
900
886
|
}
|
|
901
887
|
// authorizedProject AFTER the repository exists (finding 20).
|
|
902
888
|
const clonedAt = await identities.resolveNow(cloned.target);
|
|
903
889
|
const admitted = typeof clonedAt === "string" && clonedAt !== "no-answer" && (await authorizedProject(liveCeiling(), clonedAt)) ? clonedAt : null;
|
|
904
890
|
if (admitted === null) {
|
|
905
|
-
return projectsScreen(response, who, `the clone landed at ${cloned.target} but did not prove under the ceiling — it was left in place; enroll it by hand`, 400);
|
|
891
|
+
return projectsScreen(response, who, `the clone landed at ${cloned.target} but did not prove under the ceiling — it was left in place; enroll it by hand`, 400, "/", "clone");
|
|
906
892
|
}
|
|
907
893
|
const stillAllowed = (repo) => identities.generation() === decidedOn && visible(repo);
|
|
908
894
|
const joined = await admitProject(store, { registryFile: options.registryPath ?? null, path: admitted, actor: { label: who.name, origin: "console" }, now, stillAllowed });
|
|
909
895
|
if (!joined.ok) {
|
|
910
|
-
return projectsScreen(response, who, `${cloned.target} is cloned but not added — ${joined.message}`, joined.reason === "moved" ? 409 : 400);
|
|
896
|
+
return projectsScreen(response, who, `${cloned.target} is cloned but not added — ${joined.message}`, joined.reason === "moved" ? 409 : 400, "/", "clone");
|
|
911
897
|
}
|
|
912
898
|
if (!stillAllowed(joined.repo))
|
|
913
|
-
return projectsScreen(response, who, `${cloned.target} is cloned but wasn't opened — a project folder changed while it was being checked; try again`, 409);
|
|
899
|
+
return projectsScreen(response, who, `${cloned.target} is cloned but wasn't opened — a project folder changed while it was being checked; try again`, 409, "/", "clone");
|
|
914
900
|
deletedRepos.delete(admitted);
|
|
915
901
|
who.session.project = admitted;
|
|
916
902
|
who.session.projectRevision += 1;
|
|
917
903
|
return projectsScreen(response, who, options.upConsole === true
|
|
918
904
|
? `${projectName(admitted)} is ready — the builder is connecting automatically`
|
|
919
|
-
: `${admitted} is ready and open`, 200);
|
|
905
|
+
: `${admitted} is ready and open`, 200, "/", "said");
|
|
920
906
|
}
|
|
921
907
|
/**
|
|
922
908
|
* The opener: every project inside the ceiling, most recently opened
|
|
@@ -925,8 +911,9 @@ export function createPagesHandlers(runtime) {
|
|
|
925
911
|
* registry row never confers access; this page only offers what the
|
|
926
912
|
* server was configured to allow.
|
|
927
913
|
*/
|
|
928
|
-
|
|
929
|
-
|
|
914
|
+
/** The projects this person may open: the registered ones they can see, most recently opened first, then the
|
|
915
|
+
* configured ones the queue has seen. A registry row never confers access. */
|
|
916
|
+
function visibleProjects() {
|
|
930
917
|
const recent = store.listProjects().filter(one => visible(one.path));
|
|
931
918
|
const recentPaths = new Set(recent.map(one => one.path));
|
|
932
919
|
const candidates = new Set();
|
|
@@ -935,6 +922,20 @@ export function createPagesHandlers(runtime) {
|
|
|
935
922
|
if (!recentPaths.has(canonical) && visible(canonical))
|
|
936
923
|
candidates.add(canonical);
|
|
937
924
|
}
|
|
925
|
+
return { recent, candidates: [...candidates] };
|
|
926
|
+
}
|
|
927
|
+
/** A cheap peek per project: one small set of COUNTs, scoped to the exact repo. */
|
|
928
|
+
function peekOf(path, now) {
|
|
929
|
+
try {
|
|
930
|
+
return projectFamilyPeek(path, now);
|
|
931
|
+
}
|
|
932
|
+
catch {
|
|
933
|
+
return null;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
async function projectsScreen(response, who, problem, status, returnTo = "/", problemAt = null) {
|
|
937
|
+
const now = clock();
|
|
938
|
+
const { recent, candidates } = visibleProjects();
|
|
938
939
|
const open = who.via === "cookie" ? who.session.project : null;
|
|
939
940
|
// Onboarding availability (findings 2/28/32/39): a cookie session on a
|
|
940
941
|
// root-configured, non-demo, POSIX serve gets the live card; everybody
|
|
@@ -953,31 +954,66 @@ export function createPagesHandlers(runtime) {
|
|
|
953
954
|
: "choose which folder this server may use with --project-root <dir>",
|
|
954
955
|
}
|
|
955
956
|
: { enabled: true, roots: ceiling.roots, record: [...(who.session.onboard?.entries() ?? [])][0] ?? null };
|
|
956
|
-
// A cheap peek per project for the switcher cards — one small set of
|
|
957
|
-
// COUNTs each, scoped to the exact repo.
|
|
958
|
-
const peekOf = (path) => {
|
|
959
|
-
try {
|
|
960
|
-
return projectFamilyPeek(path, now);
|
|
961
|
-
}
|
|
962
|
-
catch {
|
|
963
|
-
return null;
|
|
964
|
-
}
|
|
965
|
-
};
|
|
966
957
|
const peeks = {};
|
|
967
958
|
for (const one of recent)
|
|
968
|
-
peeks[one.path] = peekOf(one.path);
|
|
959
|
+
peeks[one.path] = peekOf(one.path, now);
|
|
969
960
|
for (const path of candidates)
|
|
970
|
-
peeks[path] = peekOf(path);
|
|
971
|
-
|
|
972
|
-
|
|
961
|
+
peeks[path] = peekOf(path, now);
|
|
962
|
+
return sendScreen(response, status, projectsPage(chromeFor(open, "projects"), {
|
|
963
|
+
recent, candidates, open, problem, problemAt, onboard: onboardState, peeks, returnTo,
|
|
964
|
+
browsable: !restricted() && (ceiling.roots.length > 0 || unscopedMode),
|
|
965
|
+
}));
|
|
966
|
+
}
|
|
967
|
+
/** One project's reading page (/projects/<name>). A GET only reads (docs/plans/get-side-effects.md): it never
|
|
968
|
+
* switches the session's project; its Open project button posts to /projects/select, which proves it again. The
|
|
969
|
+
* name is found among the projects this person may open (projectsSelect's rule: visible here and inside the
|
|
970
|
+
* ceiling); a name it can't see answers exactly like one that doesn't exist. */
|
|
971
|
+
async function projectPageHandler(ctx) {
|
|
972
|
+
const { url, who, response, now } = ctx;
|
|
973
|
+
let name = "";
|
|
974
|
+
try {
|
|
975
|
+
name = decodeURIComponent(url.pathname.slice("/projects/".length));
|
|
976
|
+
}
|
|
977
|
+
catch {
|
|
978
|
+
name = "";
|
|
979
|
+
}
|
|
980
|
+
const { recent, candidates } = visibleProjects();
|
|
981
|
+
const known = [...recent.map(one => ({ path: one.path, name: one.name, openedAt: one.lastOpenedAt })), ...candidates.map(path => ({ path, name: projectName(path), openedAt: null }))];
|
|
982
|
+
const named = known.filter(one => one.name === name);
|
|
983
|
+
const askedPath = url.searchParams.get("path");
|
|
984
|
+
const found = askedPath !== null ? named.find(one => one.path === askedPath) ?? null : named.length === 1 ? named[0] : null;
|
|
985
|
+
const open = who.via === "cookie" ? who.session.project : null;
|
|
986
|
+
if (found === null || !(await authorizedProject(liveCeiling(), found.path))) {
|
|
987
|
+
const words = unknownProject(name === "" ? "by that name" : name);
|
|
988
|
+
return sendScreen(response, 404, screen("Not found", kitNotFound(url.pathname, words), { chrome: chromeFor(open, "projects") }));
|
|
989
|
+
}
|
|
990
|
+
const path = found.path;
|
|
991
|
+
// The tasks that wait on this person or run, from the board's own classification, scoped to this one project.
|
|
992
|
+
const snapshot = store.boardScoped(path, now, 200, null);
|
|
993
|
+
const cards = snapshot.tasks
|
|
994
|
+
.filter(facts => facts.repo === path)
|
|
995
|
+
.map(facts => classify(facts.blockerRepo !== null && !visible(facts.blockerRepo) ? { ...facts, blockerState: null } : facts, now))
|
|
996
|
+
.filter(card => card.lane === "attention" || card.lane === "building");
|
|
997
|
+
const tasks = cards.slice(0, 5).map(card => ({ href: card.href, title: card.title, reason: card.reason, needs: card.lane === "attention", since: card.stalledSince }));
|
|
998
|
+
const cookie = who.via === "cookie";
|
|
999
|
+
let activity = null;
|
|
1000
|
+
if (store.accountCanAccess(who.name, path)) {
|
|
973
1001
|
try {
|
|
974
|
-
activity = {
|
|
1002
|
+
activity = { report: projectOutcomes(store, path, now, { principal: 'operator', repos: [path] }) };
|
|
975
1003
|
}
|
|
976
1004
|
catch {
|
|
977
|
-
activity = {
|
|
1005
|
+
activity = { report: null };
|
|
978
1006
|
}
|
|
979
1007
|
}
|
|
980
|
-
return sendScreen(response,
|
|
1008
|
+
return sendScreen(response, 200, projectPage(chromeFor(open, "projects"), {
|
|
1009
|
+
name: found.name, path, selfHref: projectHref(found.name, path, named.length > 1), open: open === path, openedAt: found.openedAt,
|
|
1010
|
+
peek: peekOf(path, now), tasks, moreTasks: Math.max(0, cards.length - tasks.length),
|
|
1011
|
+
checks: cookie ? { href: `/settings/checks?repo=${encodeURIComponent(path)}`, level: CHECK_LEVEL_WORDS[projectCheckLevel(store, path).level] } : null,
|
|
1012
|
+
pullRequests: cookie ? { href: `/settings/pull-requests?repo=${encodeURIComponent(path)}`, on: publishingOf(store, path).on } : null,
|
|
1013
|
+
knowledgeHref: `/settings/knowledge?repo=${encodeURIComponent(path)}`,
|
|
1014
|
+
settingsHref: restricted() ? null : `/settings/project?repo=${encodeURIComponent(path)}`,
|
|
1015
|
+
activity, now,
|
|
1016
|
+
}));
|
|
981
1017
|
}
|
|
982
1018
|
async function instanceEdge(ctx) {
|
|
983
1019
|
const { request, response, url } = ctx;
|
|
@@ -1108,7 +1144,9 @@ export function createPagesHandlers(runtime) {
|
|
|
1108
1144
|
"projects.github": projectsGithub,
|
|
1109
1145
|
"projects.github-list": projectsGithubList,
|
|
1110
1146
|
"projects.page": projectsIndex,
|
|
1147
|
+
"projects.project": projectPageHandler,
|
|
1111
1148
|
"home": homeRedirect,
|
|
1149
|
+
"search": searchPage,
|
|
1112
1150
|
"morning": morningAlias,
|
|
1113
1151
|
"workbench": workbench,
|
|
1114
1152
|
"ledger": ledgerPage,
|
|
@@ -1119,6 +1157,7 @@ export function createPagesHandlers(runtime) {
|
|
|
1119
1157
|
"agents": agentsPage,
|
|
1120
1158
|
"agents.live": agentsLive,
|
|
1121
1159
|
"agents.live.read": agentsLiveRead,
|
|
1160
|
+
"agents.terminals": agentsTerminals,
|
|
1122
1161
|
"fleet": fleetPage,
|
|
1123
1162
|
"ledger.export": ledgerExport,
|
|
1124
1163
|
"menu": menuPage,
|