toolroll 0.9.33 → 0.9.36
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 +7 -0
- package/dist/assignment.d.ts +1 -0
- package/dist/assignment.js +2 -2
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +17 -17
- package/dist/browser-crew.js +1 -0
- package/dist/browser-workspace.d.ts +5 -3
- package/dist/builder.js +4 -7
- package/dist/chat-acceptance.d.ts +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +124 -63
- package/dist/contracts/contract-test.d.ts +45 -0
- package/dist/contracts/contract-test.js +69 -0
- package/dist/contracts/contract.d.ts +87 -0
- package/dist/contracts/contract.js +308 -0
- package/dist/contracts/flow-propose.d.ts +325 -0
- package/dist/contracts/flow-propose.js +41 -0
- package/dist/contracts/flow.d.ts +19400 -0
- package/dist/contracts/flow.js +359 -0
- package/dist/contracts/handoff.d.ts +91 -0
- package/dist/contracts/handoff.js +82 -0
- package/dist/contracts/plan.d.ts +58 -0
- package/dist/contracts/plan.js +67 -0
- package/dist/contracts/proof.d.ts +136 -0
- package/dist/contracts/proof.js +110 -0
- package/dist/contracts/verification-receipt.d.ts +148 -0
- package/dist/contracts/verification-receipt.js +87 -0
- package/dist/demo.js +28 -11
- package/dist/desktop-update.d.ts +1 -1
- package/dist/evidence.d.ts +3 -40
- package/dist/evidence.js +7 -3
- package/dist/flow-gallery.d.ts +3 -2
- package/dist/flow-share.d.ts +10 -32
- package/dist/flow-share.js +65 -83
- package/dist/flow-starters.d.ts +2 -1
- package/dist/flow-triggers.d.ts +15 -72
- package/dist/flow-triggers.js +94 -83
- package/dist/flows.d.ts +37 -218
- package/dist/flows.js +350 -385
- package/dist/mate-tools.js +14 -44
- package/dist/mcp-connect.d.ts +3 -0
- package/dist/mcp-connect.js +16 -4
- package/dist/operate.js +83 -12
- package/dist/plan.d.ts +12 -12
- package/dist/plan.js +109 -72
- package/dist/planner-source.js +2 -1
- package/dist/planner.js +55 -25
- package/dist/process-recovery-eligibility.d.ts +2 -2
- package/dist/process-recovery-eligibility.js +13 -8
- package/dist/project-admission.d.ts +93 -0
- package/dist/project-admission.js +134 -0
- package/dist/project-delete-ui.js +5 -2
- package/dist/proof.d.ts +15 -91
- package/dist/proof.js +90 -249
- package/dist/result-evidence-readers.d.ts +42 -0
- package/dist/result-evidence-readers.js +91 -0
- package/dist/runner.d.ts +10 -0
- package/dist/runner.js +16 -0
- package/dist/scope.js +4 -3
- package/dist/serve.d.ts +4 -0
- package/dist/serve.js +112 -160
- package/dist/setup-guide.d.ts +2 -2
- package/dist/store.d.ts +2 -0
- package/dist/store.js +4 -0
- package/dist/surface.js +16 -3
- package/dist/task-review-brief.d.ts +75 -0
- package/dist/task-review-brief.js +179 -0
- package/dist/text-limits.d.ts +87 -0
- package/dist/text-limits.js +86 -0
- package/dist/verification-evidence.js +24 -12
- package/dist/when-html.d.ts +2 -0
- package/dist/when-html.js +13 -0
- package/package.json +3 -2
package/dist/serve.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SCREENSHOT_CAPTURE, structuredHandoffView, terminalDiffView } from "./result-evidence-readers.js";
|
|
1
2
|
import { checkLeadIdentity, leadIdentityOf, leadNameOf, LEAD_NAME_MAX, LEAD_PERSONA_MAX } from "./lead-identity.js";
|
|
2
3
|
import { aboutYouOf, checkAboutYou, saveAboutYou, ABOUT_YOU_LINE_MAX, ABOUT_YOU_MAX_LINES } from "./lead-about.js";
|
|
3
4
|
import { withActor } from "./actor.js";
|
|
@@ -53,7 +54,7 @@ import { FORM_PATH, flowFormPage, receiveFlowForm, shareFlowButton, stopSharingF
|
|
|
53
54
|
import { addFlowTriggerTo, checkFlowTriggerNow, HOOK_PATH, pressFlowButton, receiveFlowHook, removeFlowTrigger, renewFlowHook, removeLinearKey, saveHooksBase, saveLinearKey, saveLinearSigningSecret } from "./flow-triggers.js";
|
|
54
55
|
import { addCardToFlow, advanceFlows, cancelFlowCard, crossProjectProblem, decideFlowCard, FLOW_HREF, flowDefinitionOf, moveCardInFlow } from "./flow-engine.js";
|
|
55
56
|
import { chooseFlowCard } from "./flow-send.js";
|
|
56
|
-
import { FLOW_TEMPLATES, validateFlowDefinition } from "./flows.js";
|
|
57
|
+
import { FlowContractError, FLOW_TEMPLATES, validateFlowDefinition, withZoneNames } from "./flows.js";
|
|
57
58
|
import { TOOL_CATALOG, addToolTo, catalogTool, discoverTools, localAppOf, projectToolsOf, removeToolFrom, secretsSetFor, setToolSecret, splitCommandLine, testToolOf } from "./project-tools.js";
|
|
58
59
|
import { changeLearning, learningView } from "./project-learning.js";
|
|
59
60
|
import { applySavedKnowledge, changeKnowledge, knowledgeView, knowledgeVersion, readKnowledgeSnapshot } from "./project-knowledge.js";
|
|
@@ -178,12 +179,13 @@ import { parseReport } from "./scout-report.js";
|
|
|
178
179
|
import { buildDataDocument, composeRequest, credentialKeyOf, isDirectChatProvider, isSubscriptionChatProvider, parseAssistantEnvelope, performChatRequest, priceOf, priceForConfig, worstCaseForPrice, settleForPrice, subscriptionCredentialKey, fetchOpenRouterCatalog, plausibleChatKey, PRICED_MODELS, settleMicrousd, worstCaseMicrousd, CHAT_KEY_ENV, TURN_WALL_CLOCK_MS, } from "./converse.js";
|
|
179
180
|
import { fileTaskProposal, fileRoutineProposal } from "./proposal.js";
|
|
180
181
|
import { TASK_TEXT_LIMITS, validateTaskText } from "./task-text.js";
|
|
182
|
+
import { TEXT_LIMITS } from "./text-limits.js";
|
|
181
183
|
import {} from "./store.js";
|
|
182
184
|
import { attendedLivenessState } from "./liveness.js";
|
|
183
185
|
import { approvalOf, approve as approveScope, attendedDigestOf, attendedTermsJson, authenticateApprover as checkApproverPassword, canonicalProfileJson, chainFromJson, profileDigestOf, proposeGuarded, scopeAuthorityOf, acceptanceLinesToInput, acceptanceToLines, acceptanceWords, parseAcceptanceCriteria, } from "./scope.js";
|
|
184
186
|
import { isQualityMode, qualityModeTitle } from "./quality.js";
|
|
185
187
|
import { LIMITS, hasForbiddenControls, validateNote } from "./decision.js";
|
|
186
|
-
import { parseExecutionPlanDocument,
|
|
188
|
+
import { parseExecutionPlanDocument, milestonesOf } from "./plan.js";
|
|
187
189
|
import { contractChangesOf, decodePlanContractRecord, describeContractChanges } from "./planner-source.js";
|
|
188
190
|
import { observeWorktree, parseBaseTreeSnapshot, aggregateNewNames, PEEK_LIMITS } from "./peek.js";
|
|
189
191
|
import { readLiveWindow } from "./live.js";
|
|
@@ -194,7 +196,8 @@ import { parseGithubRepo, previewGithubRepo, cloneGithubRepo, listGithubRepos, i
|
|
|
194
196
|
import { verifiedAuthor, LEAD_THREAD, isDigestTime, RESULT_SCREENSHOTS, MATE_ASK_OTHER } from "./store.js";
|
|
195
197
|
import { RESULT_SHOT_CHOICES } from "./result-shots.js";
|
|
196
198
|
import { digestTimes } from "./digest-times.js";
|
|
197
|
-
import { updateRepos,
|
|
199
|
+
import { updateRepos, removeRepos } from "./repos.js";
|
|
200
|
+
import { admitProject, releaseProject } from "./project-admission.js";
|
|
198
201
|
import { run as execRun } from "./exec.js";
|
|
199
202
|
import { findFirstTasks, firstResultWords, firstRunSteps, firstTaskJourney, firstTaskSuggestions, HOW_IT_WORKS, SANDBOX_COMMAND, signInCommandFor, START_COMMAND } from "./first-run.js";
|
|
200
203
|
/** A user agent, reduced to safe display words — never echoed raw. */
|
|
@@ -276,6 +279,8 @@ import { PACKAGE_VERSION } from "./version.js";
|
|
|
276
279
|
import { updateNoticeWords } from "./update-notice.js";
|
|
277
280
|
import { whenHtml } from "./when-html.js";
|
|
278
281
|
const SESSION_COOKIE = "standing-orders_session";
|
|
282
|
+
/** Stands in for "no project" in a ceiling: no folder resolves to it, so it admits nothing. */
|
|
283
|
+
const NO_PROJECT = "/\0no-project";
|
|
279
284
|
/** Where `up`'s one-time sign-in link points, and how long it works. */
|
|
280
285
|
export const SIGN_IN_LINK_PATH = "/login/once/";
|
|
281
286
|
export const SIGN_IN_LINK_MS = 10 * 60_000;
|
|
@@ -632,7 +637,13 @@ export function createDecisionServer(options) {
|
|
|
632
637
|
/** No ceiling configured at all: the legacy trust-everything mode, named. */
|
|
633
638
|
const unscopedMode = ceiling.repos.length === 0 && ceiling.roots.length === 0;
|
|
634
639
|
/** Per-row visibility under the ceiling — the authorization question for reads. */
|
|
635
|
-
|
|
640
|
+
/** The exact repositories this console serves now: `up`'s live admitted set when it supplies one, else startup's. */
|
|
641
|
+
const exactRepos = () => options.admittedRepos?.() ?? ceiling.repos;
|
|
642
|
+
const liveCeiling = () => {
|
|
643
|
+
const repos = [...exactRepos(), ...(options.additionalProjectRepos?.() ?? [])];
|
|
644
|
+
// An admitted set that is empty for now is an empty ceiling, never the legacy unscoped mode.
|
|
645
|
+
return { repos: repos.length === 0 && ceiling.roots.length === 0 && !unscopedMode ? [NO_PROJECT] : repos, roots: ceiling.roots };
|
|
646
|
+
};
|
|
636
647
|
// Native tools share the installation's login and can read beyond a project.
|
|
637
648
|
// Account project membership cannot provide an OS read boundary.
|
|
638
649
|
function codingActorAllowed(actor) {
|
|
@@ -651,12 +662,21 @@ export function createDecisionServer(options) {
|
|
|
651
662
|
* ceiling; this server still filters every row through its own ceiling. */
|
|
652
663
|
/** Projects deleted while this console runs: gone from its lists though a builder started with them still watches. */
|
|
653
664
|
const deletedRepos = new Set();
|
|
665
|
+
/** Those still gone: a project added again (from here, `repos add`, or the lead) has its row back and shows again. */
|
|
666
|
+
const goneRepos = () => {
|
|
667
|
+
if (deletedRepos.size === 0)
|
|
668
|
+
return deletedRepos;
|
|
669
|
+
for (const one of store.listProjects())
|
|
670
|
+
deletedRepos.delete(one.path);
|
|
671
|
+
return deletedRepos;
|
|
672
|
+
};
|
|
654
673
|
const managedRepos = () => {
|
|
655
674
|
const seen = new Set();
|
|
656
675
|
const repos = [];
|
|
657
|
-
|
|
676
|
+
const gone = goneRepos();
|
|
677
|
+
for (const path of [...exactRepos(), ...(options.currentRepos?.() ?? [])]) {
|
|
658
678
|
const canonical = canonicalProject(path) ?? path;
|
|
659
|
-
if (seen.has(canonical) || !visible(canonical) ||
|
|
679
|
+
if (seen.has(canonical) || !visible(canonical) || gone.has(canonical))
|
|
660
680
|
continue;
|
|
661
681
|
seen.add(canonical);
|
|
662
682
|
repos.push(canonical);
|
|
@@ -761,6 +781,13 @@ export function createDecisionServer(options) {
|
|
|
761
781
|
const admissionList = () => restricted() ? [...(store.accountOf(requestContext.getStore().actor)?.projects ?? [])].filter(visible) : unscopedMode
|
|
762
782
|
? null
|
|
763
783
|
: [...new Set([...managedRepos(), ...(ceiling.roots.length === 0 ? [] : store.knownRepos().filter(visible))])];
|
|
784
|
+
/** Every project this person may pick in the console — the switcher, Projects, Settings, Tools and Flows read this
|
|
785
|
+
* one list: the live admitted set (taskless projects included) and every project with saved work, each through
|
|
786
|
+
* the ceiling and this account's project grants. A project removed or deleted here is gone from it. */
|
|
787
|
+
const consoleProjects = () => {
|
|
788
|
+
const gone = goneRepos();
|
|
789
|
+
return [...new Set([...(admissionList() ?? []), ...managedRepos(), ...store.knownRepos()])].filter(one => visible(one) && !gone.has(one));
|
|
790
|
+
};
|
|
764
791
|
/** The task behind a resource, for the ceiling check; null = no ref (visible). */
|
|
765
792
|
const taskRepoOf = (taskRef) => store.refForId(taskRef)?.repo ?? null;
|
|
766
793
|
const server = createServer((request, response) => {
|
|
@@ -1110,6 +1137,8 @@ export function createDecisionServer(options) {
|
|
|
1110
1137
|
response.end(`<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta http-equiv="refresh" content="0;url=${escape(to)}"><title>Toolroll</title>${HANDOFF_STYLE}<p>${escape(words)} <a href="${escape(to)}">Back to Toolroll</a></p>`);
|
|
1111
1138
|
};
|
|
1112
1139
|
const state = url.searchParams.get("state") ?? "";
|
|
1140
|
+
// Every arrival is logged (never the code or state): a sign-in that never came back is then visible as an absence.
|
|
1141
|
+
logEvent("info", "connect.callback", { error: url.searchParams.get("error"), known: connectVisits.has(state) });
|
|
1113
1142
|
if (!startedHere(request, state))
|
|
1114
1143
|
return done("/settings/tools", "problem", "That sign-in was started in another browser. Connect again from this one.");
|
|
1115
1144
|
const visit = connectVisits.get(state);
|
|
@@ -2519,7 +2548,7 @@ export function createDecisionServer(options) {
|
|
|
2519
2548
|
return void response.end(spendCsv(items, teammateNames));
|
|
2520
2549
|
}
|
|
2521
2550
|
const shift = (by) => { const at = new Date(`${month.name}-01T00:00:00.000Z`); at.setUTCMonth(at.getUTCMonth() + by); return at.toISOString().slice(0, 7); };
|
|
2522
|
-
const projects =
|
|
2551
|
+
const projects = consoleProjects();
|
|
2523
2552
|
const people = store.accountFacts().filter(one => one.revokedAt === null).map(one => one.name);
|
|
2524
2553
|
const view = {
|
|
2525
2554
|
month: month.name, previous: shift(-1), next: month.name === current.name ? null : shift(1),
|
|
@@ -2535,7 +2564,7 @@ export function createDecisionServer(options) {
|
|
|
2535
2564
|
}
|
|
2536
2565
|
if (url.pathname === "/ledger") {
|
|
2537
2566
|
const admitted = admissionList();
|
|
2538
|
-
const projects = admitted ??
|
|
2567
|
+
const projects = admitted ?? consoleProjects();
|
|
2539
2568
|
const chosen = url.searchParams.get("project") ?? "";
|
|
2540
2569
|
if (chosen !== "" && !projects.includes(chosen))
|
|
2541
2570
|
return refuse(response, who, 403, "That project is outside your access.", "/ledger");
|
|
@@ -2694,7 +2723,7 @@ export function createDecisionServer(options) {
|
|
|
2694
2723
|
return seen;
|
|
2695
2724
|
};
|
|
2696
2725
|
const csrf = who.via === "cookie" ? who.session.csrf : "";
|
|
2697
|
-
const projectChoices =
|
|
2726
|
+
const projectChoices = consoleProjects();
|
|
2698
2727
|
const accessFields = (selected) => `<label>Project access<select name="access"><option value="selected"${selected !== null ? " selected" : ""}>Selected projects</option><option value="all"${selected === null ? " selected" : ""}>All projects</option></select></label>` +
|
|
2699
2728
|
`<p class="meta">Operators with all-project access also manage the instance.</p><fieldset><legend>Projects</legend>${projectChoices.length === 0 ? `<p class="meta">Add a project before granting selected access.</p>` : projectChoices.map(repo => `<label class="row"><input type="checkbox" name="projects" value="${escape(repo)}"${selected?.includes(repo) ? " checked" : ""}>${escape(projectName(repo))}</label>`).join("")}</fieldset>`;
|
|
2700
2729
|
const cards = accounts.map(one => {
|
|
@@ -3192,7 +3221,7 @@ export function createDecisionServer(options) {
|
|
|
3192
3221
|
}
|
|
3193
3222
|
// Flows → New: the gallery, and each template's page.
|
|
3194
3223
|
if (url.pathname === "/flows/new") {
|
|
3195
|
-
const projects =
|
|
3224
|
+
const projects = consoleProjects();
|
|
3196
3225
|
const canUse = who.via === "cookie" && who.role === "approver" && projects.length > 0;
|
|
3197
3226
|
// The chosen project, else the one being looked at: its tools are what each card says is connected.
|
|
3198
3227
|
const asked = url.searchParams.get("repo");
|
|
@@ -3202,7 +3231,7 @@ export function createDecisionServer(options) {
|
|
|
3202
3231
|
const galleryRead = /^\/flows\/new\/([a-z-]{1,40})$/.exec(url.pathname);
|
|
3203
3232
|
if (galleryRead !== null) {
|
|
3204
3233
|
const template = galleryTemplateOf(galleryRead[1]);
|
|
3205
|
-
const projects =
|
|
3234
|
+
const projects = consoleProjects();
|
|
3206
3235
|
if (template === null)
|
|
3207
3236
|
return refuse(response, who, 404, "There's no template by that name.", "/flows/new");
|
|
3208
3237
|
if (who.via !== "cookie" || who.role !== "approver")
|
|
@@ -3216,13 +3245,13 @@ export function createDecisionServer(options) {
|
|
|
3216
3245
|
return sendGalleryUse(response, who, template, projects, repo, null, template.name, problem === null ? null : problem.slice(0, 400), said === null ? null : said.slice(0, 400));
|
|
3217
3246
|
}
|
|
3218
3247
|
if (url.pathname === "/flows") {
|
|
3219
|
-
const projects =
|
|
3248
|
+
const projects = consoleProjects();
|
|
3220
3249
|
const flows = store.listFlows(projects);
|
|
3221
3250
|
return sendScreen(response, 200, screen("Flows", `<h1>Flows</h1>${flowsListHtml(store, flows, projects, who.via === "cookie" ? who.session.csrf : "", who.via === "cookie" && who.role === "approver", url.searchParams.get("problem"))}`, { chrome: chromeFor(project, "flows"), functional: { script: FLOW_IMPORT_SCRIPT } }));
|
|
3222
3251
|
}
|
|
3223
3252
|
// starter kits — the gallery, and each kit's checklist in a project.
|
|
3224
3253
|
if (url.pathname === "/kits" || /^\/kits\/[a-z-]{1,40}$/.test(url.pathname)) {
|
|
3225
|
-
const projects =
|
|
3254
|
+
const projects = consoleProjects();
|
|
3226
3255
|
const csrf = who.via === "cookie" ? who.session.csrf : "", canSetUp = who.via === "cookie" && who.role === "approver";
|
|
3227
3256
|
if (url.pathname === "/kits") {
|
|
3228
3257
|
return sendScreen(response, 200, screen("Starter kits", `<h1>Starter kits</h1>${kitsGalleryHtml(store, projects, projectName, csrf, canSetUp, { problem: url.searchParams.get("problem") })}`, { chrome: chromeFor(project, "flows") }));
|
|
@@ -3235,7 +3264,7 @@ export function createDecisionServer(options) {
|
|
|
3235
3264
|
}
|
|
3236
3265
|
// v92: AI teammates — the team, and one page per teammate.
|
|
3237
3266
|
if (url.pathname === "/teammates") {
|
|
3238
|
-
const projects =
|
|
3267
|
+
const projects = consoleProjects();
|
|
3239
3268
|
return sendScreen(response, 200, screen("Teammates", `<h1>Teammates</h1>${teammatesListHtml(store, store.teammates(projects), projects, projectName, who.via === "cookie" ? who.session.csrf : "", who.via === "cookie" && who.role === "approver", { said: url.searchParams.get("said"), problem: url.searchParams.get("problem") })}`, { chrome: chromeFor(project, "flows") }));
|
|
3240
3269
|
}
|
|
3241
3270
|
const teammateRead = /^\/teammates\/([1-9][0-9]{0,9})(\/soul\.md)?$/.exec(url.pathname);
|
|
@@ -3307,7 +3336,7 @@ export function createDecisionServer(options) {
|
|
|
3307
3336
|
if (flow === null || flow.state !== "active" || !visible(flow.repo))
|
|
3308
3337
|
return refuse(response, who, 404, "No such flow in your projects.", "/flows");
|
|
3309
3338
|
const selected = Number(url.searchParams.get("card")), start = Number(url.searchParams.get("start"));
|
|
3310
|
-
const view = flowView(store, flow, { name: who.name, approver: who.via === "cookie" && who.role === "approver" }, Number.isSafeInteger(selected) && selected > 0 ? selected : null, { dir: options.configDir ?? null, repos:
|
|
3339
|
+
const view = flowView(store, flow, { name: who.name, approver: who.via === "cookie" && who.role === "approver" }, Number.isSafeInteger(selected) && selected > 0 ? selected : null, { dir: options.configDir ?? null, repos: consoleProjects(), startTrigger: Number.isSafeInteger(start) && start > 0 ? start : null,
|
|
3311
3340
|
sortReady: keyStatus("openrouter", providerHome).set, toolHome });
|
|
3312
3341
|
if (url.searchParams.get("format") === "json")
|
|
3313
3342
|
return respond(response, 200, "application/json; charset=utf-8", JSON.stringify(view));
|
|
@@ -3886,7 +3915,7 @@ export function createDecisionServer(options) {
|
|
|
3886
3915
|
return sendScreen(response, 200, screen("Models", `<p><a href="/settings">Settings</a></p><h1>Models</h1>${modelsHtml(view)}`, { chrome: chromeFor(project, "settings"), functional: { script: modelsScript() } }));
|
|
3887
3916
|
}
|
|
3888
3917
|
if (url.pathname === "/settings/skills") {
|
|
3889
|
-
const projects =
|
|
3918
|
+
const projects = consoleProjects();
|
|
3890
3919
|
const chosen = url.searchParams.get("repo") ?? project ?? projects[0] ?? "";
|
|
3891
3920
|
if (chosen && !projects.includes(chosen))
|
|
3892
3921
|
return refuse(response, who, 403, "That project is outside your access.", "/projects");
|
|
@@ -3904,7 +3933,7 @@ export function createDecisionServer(options) {
|
|
|
3904
3933
|
return sendScreen(response, 200, screen('Skills', `<p><a href="/settings">Settings</a></p><h1>Skills</h1>${url.searchParams.get('saved') === '1' ? '<p role="status">Saved.</p>' : ''}${selector}${content}`, { chrome: chromeFor(chosen || project, 'settings'), functional: { script: skillsScript() } }));
|
|
3905
3934
|
}
|
|
3906
3935
|
if (url.pathname === "/settings/tools") {
|
|
3907
|
-
const projects =
|
|
3936
|
+
const projects = consoleProjects();
|
|
3908
3937
|
const chosen = url.searchParams.get("repo") ?? project ?? projects[0] ?? "";
|
|
3909
3938
|
if (chosen && !projects.includes(chosen))
|
|
3910
3939
|
return refuse(response, who, 403, "That project is outside your access.", "/projects");
|
|
@@ -3928,7 +3957,7 @@ export function createDecisionServer(options) {
|
|
|
3928
3957
|
}
|
|
3929
3958
|
// Settings → Project: what Toolroll holds for a project; an instance operator deletes it here.
|
|
3930
3959
|
if (url.pathname === "/settings/project") {
|
|
3931
|
-
const projects =
|
|
3960
|
+
const projects = consoleProjects();
|
|
3932
3961
|
const asked = url.searchParams.get("repo");
|
|
3933
3962
|
const chosen = asked ?? (project != null && projects.includes(project) ? project : projects[0] ?? "");
|
|
3934
3963
|
if (chosen && !projects.includes(chosen))
|
|
@@ -3940,7 +3969,7 @@ export function createDecisionServer(options) {
|
|
|
3940
3969
|
}
|
|
3941
3970
|
// v102: Settings → Approval rules, a project's separation of duties. Anyone who sees the project reads them; an instance operator sets them.
|
|
3942
3971
|
if (url.pathname === "/settings/approval") {
|
|
3943
|
-
const projects =
|
|
3972
|
+
const projects = consoleProjects();
|
|
3944
3973
|
const chosen = url.searchParams.get("repo") ?? project ?? projects[0] ?? "";
|
|
3945
3974
|
if (chosen && !projects.includes(chosen))
|
|
3946
3975
|
return refuse(response, who, 403, "That project is outside your access.", "/settings/approval");
|
|
@@ -3951,7 +3980,7 @@ export function createDecisionServer(options) {
|
|
|
3951
3980
|
}
|
|
3952
3981
|
// Sprint 8: Settings → Policy, the organisation policy and its history. Anyone signed in reads it; an instance operator changes it.
|
|
3953
3982
|
if (url.pathname === "/settings/policy") {
|
|
3954
|
-
const repos =
|
|
3983
|
+
const repos = consoleProjects();
|
|
3955
3984
|
const toolNames = [...new Set(repos.flatMap(repo => store.projectTools(repo).map(one => one.name)))].sort();
|
|
3956
3985
|
const view = { policy: store.orgPolicy(), history: store.policyHistory(50), canChange: who.via === "cookie" && store.isInstanceOperator(who.name), toolNames };
|
|
3957
3986
|
return sendScreen(response, 200, screen("Policy", `<p><a href="/settings">Settings</a></p><h1>Policy</h1>${policyHtml(view, who.via === "cookie" ? who.session.csrf : "", { said: url.searchParams.get("said"), problem: url.searchParams.get("problem") })}`, { chrome: chromeFor(project, "settings") }));
|
|
@@ -3969,7 +3998,7 @@ export function createDecisionServer(options) {
|
|
|
3969
3998
|
// Settings → Flows: the starter flows, each switched on with one yes. ?starter= marks the one a task's
|
|
3970
3999
|
// "Do this every time…" asked about.
|
|
3971
4000
|
if (url.pathname === "/settings/flows") {
|
|
3972
|
-
const projects =
|
|
4001
|
+
const projects = consoleProjects();
|
|
3973
4002
|
const chosen = url.searchParams.get("repo") ?? project ?? projects[0] ?? "";
|
|
3974
4003
|
if (chosen && !projects.includes(chosen))
|
|
3975
4004
|
return refuse(response, who, 403, "That project is outside your access.", "/settings");
|
|
@@ -4093,13 +4122,13 @@ export function createDecisionServer(options) {
|
|
|
4093
4122
|
if (who.via !== "cookie" || !store.isInstanceOperator(who.name) || !options.configDir)
|
|
4094
4123
|
return refuse(response, who, 403, "An instance operator sets up sign-in.", "/settings");
|
|
4095
4124
|
const origin = consoleOrigin(request.headers.host);
|
|
4096
|
-
const projects =
|
|
4125
|
+
const projects = consoleProjects().map(path => ({ path, name: projectName(path) }));
|
|
4097
4126
|
const settings = ssoSettings();
|
|
4098
4127
|
const html = ssoSettingsHtml({ settings, redirect: origin === null ? null : `${origin}${SSO_CALLBACK}`, projects, linked: settings !== null && store.ssoIdentitiesOf(who.name).some(one => one.issuer.replace(/\/+$/, "") === settings.issuer) }, who.session.csrf, { said: url.searchParams.get("said"), problem: url.searchParams.get("problem") });
|
|
4099
4128
|
return sendScreen(response, 200, screen("Sign-in", `<p><a href="/settings">Settings</a></p><h1>Sign-in</h1>${html}`, { chrome: chromeFor(project, "settings") }));
|
|
4100
4129
|
}
|
|
4101
4130
|
if (url.pathname === "/settings/knowledge") {
|
|
4102
|
-
const projects =
|
|
4131
|
+
const projects = consoleProjects();
|
|
4103
4132
|
const chosen = url.searchParams.get("repo") ?? project ?? projects[0] ?? "";
|
|
4104
4133
|
if (chosen && (!projects.includes(chosen) || !visible(chosen)))
|
|
4105
4134
|
return refuse(response, who, 403, "That project is outside your access.", "/settings/knowledge");
|
|
@@ -4137,7 +4166,7 @@ export function createDecisionServer(options) {
|
|
|
4137
4166
|
return sendScreen(response, 200, screen('Knowledge', `<p><a href="/settings">Settings</a></p><h1>Knowledge</h1>${url.searchParams.get('saved') === '1' ? '<p role="status">Saved for future tasks.</p>' : ''}${selector}${content}`, { chrome: chromeFor(chosen || project, 'settings') }));
|
|
4138
4167
|
}
|
|
4139
4168
|
if (url.pathname === "/settings/learning") {
|
|
4140
|
-
const projects =
|
|
4169
|
+
const projects = consoleProjects();
|
|
4141
4170
|
const chosen = url.searchParams.get("repo") ?? project ?? projects[0] ?? "";
|
|
4142
4171
|
if (chosen && (!projects.includes(chosen) || !visible(chosen)))
|
|
4143
4172
|
return refuse(response, who, 403, "That project is outside your access.", "/settings/learning");
|
|
@@ -5231,7 +5260,7 @@ export function createDecisionServer(options) {
|
|
|
5231
5260
|
...store.listProjects().map(one => ({ path: one.path, name: one.name })),
|
|
5232
5261
|
...managedRepos().map(path => ({ path, name: projectName(path) })),
|
|
5233
5262
|
]) {
|
|
5234
|
-
if (seen.has(one.path) || !visible(one.path))
|
|
5263
|
+
if (seen.has(one.path) || !visible(one.path) || goneRepos().has(one.path))
|
|
5235
5264
|
continue;
|
|
5236
5265
|
seen.add(one.path);
|
|
5237
5266
|
rows.push(one);
|
|
@@ -5475,7 +5504,7 @@ export function createDecisionServer(options) {
|
|
|
5475
5504
|
const recent = store.listProjects().filter(one => visible(one.path));
|
|
5476
5505
|
const recentPaths = new Set(recent.map(one => one.path));
|
|
5477
5506
|
const candidates = new Set();
|
|
5478
|
-
for (const path of
|
|
5507
|
+
for (const path of consoleProjects()) {
|
|
5479
5508
|
const canonical = canonicalProject(path) ?? path;
|
|
5480
5509
|
if (!recentPaths.has(canonical) && visible(canonical))
|
|
5481
5510
|
candidates.add(canonical);
|
|
@@ -6765,7 +6794,7 @@ export function createDecisionServer(options) {
|
|
|
6765
6794
|
if (who.via !== 'cookie' || who.role !== 'approver')
|
|
6766
6795
|
return refuse(response, who, 403, 'Sign in as an approver to manage skills.', '/settings/skills');
|
|
6767
6796
|
const repo = body.get('repo') ?? '', back = `/settings/skills?repo=${encodeURIComponent(repo)}`;
|
|
6768
|
-
if (!visible(repo) || !
|
|
6797
|
+
if (!visible(repo) || !consoleProjects().includes(repo))
|
|
6769
6798
|
return refuse(response, who, 403, 'That project is outside your access.', '/projects');
|
|
6770
6799
|
if ([...new Set(body.keys())].some(k => body.getAll(k).length !== 1))
|
|
6771
6800
|
return refuse(response, who, 400, 'Invalid skills form.', back);
|
|
@@ -6821,7 +6850,7 @@ export function createDecisionServer(options) {
|
|
|
6821
6850
|
if (who.via !== "cookie" || who.role !== "approver")
|
|
6822
6851
|
return refuse(response, who, 403, "Sign in as an approver to set kits up.", "/kits");
|
|
6823
6852
|
const kit = kitOf(kitPost[1]);
|
|
6824
|
-
const projects =
|
|
6853
|
+
const projects = consoleProjects();
|
|
6825
6854
|
const repo = body.get("repo") ?? "";
|
|
6826
6855
|
if (kit === null || !projects.includes(repo) || !store.accountCanAccess(who.name, repo))
|
|
6827
6856
|
return redirect(response, `/kits?problem=${encodeURIComponent("Choose one of your projects.")}`);
|
|
@@ -6866,7 +6895,7 @@ export function createDecisionServer(options) {
|
|
|
6866
6895
|
if (who.role !== "approver")
|
|
6867
6896
|
return refuse(response, who, 403, "Sign in as an approver to look after teammates.", "/teammates");
|
|
6868
6897
|
if (teammatePost[2] === "new") {
|
|
6869
|
-
const projects =
|
|
6898
|
+
const projects = consoleProjects();
|
|
6870
6899
|
const repo = body.get("repo") ?? "";
|
|
6871
6900
|
if (!projects.includes(repo))
|
|
6872
6901
|
return redirect(response, `/teammates?problem=${encodeURIComponent("Choose one of your projects.")}`);
|
|
@@ -6938,7 +6967,7 @@ export function createDecisionServer(options) {
|
|
|
6938
6967
|
const galleryPost = /^\/flows\/new\/([a-z-]{1,40})$/.exec(url.pathname);
|
|
6939
6968
|
if (galleryPost !== null) {
|
|
6940
6969
|
const template = galleryTemplateOf(galleryPost[1]);
|
|
6941
|
-
const projects =
|
|
6970
|
+
const projects = consoleProjects();
|
|
6942
6971
|
if (template === null)
|
|
6943
6972
|
return refuse(response, who, 404, "There's no template by that name.", "/flows/new");
|
|
6944
6973
|
if (who.via !== "cookie" || who.role !== "approver" || store.isDemo())
|
|
@@ -6970,7 +6999,7 @@ export function createDecisionServer(options) {
|
|
|
6970
6999
|
if (url.pathname === "/flows/new" || url.pathname === "/flows/example" || url.pathname === "/flows/import" || flowPost !== null || triggerPost !== null) {
|
|
6971
7000
|
const now = clock();
|
|
6972
7001
|
const answer = (status, payload) => respond(response, status, "application/json; charset=utf-8", JSON.stringify(payload));
|
|
6973
|
-
const projects =
|
|
7002
|
+
const projects = consoleProjects();
|
|
6974
7003
|
if (who.via !== "cookie" || who.role !== "approver")
|
|
6975
7004
|
return url.pathname === "/flows/new" || url.pathname === "/flows/example" || url.pathname === "/flows/import" ? refuse(response, who, 403, "Sign in as an approver to create flows.", "/flows") : answer(403, { ok: false, said: "Sign in as an approver to change flows." });
|
|
6976
7005
|
if (url.pathname === "/flows/example") {
|
|
@@ -7153,12 +7182,13 @@ export function createDecisionServer(options) {
|
|
|
7153
7182
|
return redirect(response, "/flows");
|
|
7154
7183
|
}
|
|
7155
7184
|
if (action === "save") {
|
|
7156
|
-
let saved;
|
|
7185
|
+
let saved, drawn;
|
|
7157
7186
|
try {
|
|
7158
|
-
|
|
7187
|
+
drawn = JSON.parse(body.get("definition") ?? "null");
|
|
7188
|
+
saved = validateFlowDefinition(drawn);
|
|
7159
7189
|
}
|
|
7160
7190
|
catch (error) {
|
|
7161
|
-
return answer(400, { ok: false, said: error instanceof SyntaxError ? "That flow couldn't be read." : error instanceof Error ? error.message : "That flow isn't valid." });
|
|
7191
|
+
return answer(400, { ok: false, said: error instanceof SyntaxError ? "That flow couldn't be read." : error instanceof FlowContractError ? withZoneNames(error.lines, drawn) : error instanceof Error ? error.message : "That flow isn't valid." });
|
|
7162
7192
|
}
|
|
7163
7193
|
const name = (body.get("name") ?? flow.name).replace(/[\u0000-\u001f\u007f]/g, "").trim().slice(0, 80) || flow.name;
|
|
7164
7194
|
// The owner (v86) is whom "the owner decides" zones ask: someone who can approve on this project.
|
|
@@ -7244,7 +7274,7 @@ export function createDecisionServer(options) {
|
|
|
7244
7274
|
return back("problem", "Choose what the budget is for.");
|
|
7245
7275
|
const scope = target[1], key = target[2];
|
|
7246
7276
|
const known = scope === "installation" ? key === "*"
|
|
7247
|
-
: scope === "project" ?
|
|
7277
|
+
: scope === "project" ? consoleProjects().includes(key)
|
|
7248
7278
|
: scope === "person" ? store.accountFacts().some(one => one.name === key && one.revokedAt === null)
|
|
7249
7279
|
: store.teammates([...new Set([...managedRepos(), ...store.knownRepos()])]).some(one => String(one.id) === key);
|
|
7250
7280
|
if (!known)
|
|
@@ -7266,7 +7296,7 @@ export function createDecisionServer(options) {
|
|
|
7266
7296
|
// Settings → Project → Builds at once: an approver for the project sets it; the ledger keeps before → after.
|
|
7267
7297
|
if (url.pathname === "/settings/project/concurrency") {
|
|
7268
7298
|
const repo = body.get("repo") ?? "";
|
|
7269
|
-
const known =
|
|
7299
|
+
const known = consoleProjects();
|
|
7270
7300
|
if (!known.includes(repo))
|
|
7271
7301
|
return refuse(response, who, 403, "That project is outside your access.", "/settings/project");
|
|
7272
7302
|
if (who.via !== "cookie" || who.role !== "approver" || !store.accountCanAccess(who.name, repo))
|
|
@@ -7287,7 +7317,7 @@ export function createDecisionServer(options) {
|
|
|
7287
7317
|
if (who.via !== "cookie" || !store.isInstanceOperator(who.name))
|
|
7288
7318
|
return refuse(response, who, 403, "An instance operator deletes projects.", "/settings/project");
|
|
7289
7319
|
const repo = body.get("repo") ?? "";
|
|
7290
|
-
const known =
|
|
7320
|
+
const known = consoleProjects();
|
|
7291
7321
|
if (!known.includes(repo))
|
|
7292
7322
|
return refuse(response, who, 403, "That project is outside your access.", "/settings/project");
|
|
7293
7323
|
const back = (key, words, to = repo) => redirect(response, `/settings/project?${to === "" ? "" : `repo=${encodeURIComponent(to)}&`}${key}=${encodeURIComponent(words)}`);
|
|
@@ -7343,7 +7373,7 @@ export function createDecisionServer(options) {
|
|
|
7343
7373
|
if (who.via !== "cookie" || !store.isInstanceOperator(who.name))
|
|
7344
7374
|
return refuse(response, who, 403, "An instance operator sets approval rules.", "/settings/approval");
|
|
7345
7375
|
const repo = body.get("repo") ?? "";
|
|
7346
|
-
const known =
|
|
7376
|
+
const known = consoleProjects();
|
|
7347
7377
|
if (!known.includes(repo))
|
|
7348
7378
|
return refuse(response, who, 403, "That project is outside your access.", "/settings/approval");
|
|
7349
7379
|
const back = (key, words) => redirect(response, `/settings/approval?repo=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}`);
|
|
@@ -7744,7 +7774,7 @@ export function createDecisionServer(options) {
|
|
|
7744
7774
|
if (who.via !== "cookie" || who.role !== "approver")
|
|
7745
7775
|
return refuse(response, who, 403, "Sign in as an approver to connect tools.", "/settings/tools");
|
|
7746
7776
|
const repo = body.get("repo") ?? "";
|
|
7747
|
-
const reachable = (one) => visible(one) &&
|
|
7777
|
+
const reachable = (one) => visible(one) && consoleProjects().includes(one);
|
|
7748
7778
|
if (!reachable(repo))
|
|
7749
7779
|
return refuse(response, who, 403, "That project is outside your access.", "/projects");
|
|
7750
7780
|
// "Also connect to …": a service connected on the shown project, signed in again for another project.
|
|
@@ -7817,13 +7847,13 @@ export function createDecisionServer(options) {
|
|
|
7817
7847
|
if (who.via !== "cookie" || who.role !== "approver")
|
|
7818
7848
|
return refuse(response, who, 403, "Sign in as an approver to manage tools.", "/settings/tools");
|
|
7819
7849
|
const repo = body.get("repo") ?? "";
|
|
7820
|
-
if (!visible(repo) || !
|
|
7850
|
+
if (!visible(repo) || !consoleProjects().includes(repo))
|
|
7821
7851
|
return refuse(response, who, 403, "That project is outside your access.", "/projects");
|
|
7822
7852
|
const back = (key, words, anchor = "") => redirect(response, `/settings/tools?repo=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}${anchor}`);
|
|
7823
7853
|
// As for Connect: a post from a page that showed another project is refused, never applied here.
|
|
7824
7854
|
const shown = body.get("shown") ?? "";
|
|
7825
7855
|
if (shown !== repo) {
|
|
7826
|
-
const page = visible(shown) &&
|
|
7856
|
+
const page = visible(shown) && consoleProjects().includes(shown) ? shown : repo;
|
|
7827
7857
|
return redirect(response, `/settings/tools?repo=${encodeURIComponent(page)}&problem=${encodeURIComponent("The project changed; try again.")}`);
|
|
7828
7858
|
}
|
|
7829
7859
|
const action = body.get("action") ?? "";
|
|
@@ -7986,7 +8016,7 @@ export function createDecisionServer(options) {
|
|
|
7986
8016
|
if (who.via !== 'cookie' || who.role !== 'approver')
|
|
7987
8017
|
return refuse(response, who, 403, 'Sign in as an approver to refresh project context.', '/settings/knowledge');
|
|
7988
8018
|
const repo = body.get('repo') ?? '';
|
|
7989
|
-
if (!visible(repo) || !store.accountCanAccess(who.name, repo) || !
|
|
8019
|
+
if (!visible(repo) || !store.accountCanAccess(who.name, repo) || !consoleProjects().includes(repo))
|
|
7990
8020
|
return refuse(response, who, 403, 'That project is outside your access.', '/settings/knowledge');
|
|
7991
8021
|
const refreshed = await repositoryContext({ repo, query: '', refresh: true, cacheRoot: join(dirname(evidenceRoot), 'repository-context') });
|
|
7992
8022
|
return sendScreen(response, refreshed.index.status === 'ready' ? 200 : 503, screen('Project context', `<h1>Project context</h1><p>${refreshed.index.status === 'ready' ? 'Code index refreshed.' : 'The index is unavailable. Source search still works.'}</p><a class="button-link" href="/settings/knowledge?repo=${encodeURIComponent(repo)}">Open knowledge</a>`, { chrome: chromeFor(repo, 'settings') }));
|
|
@@ -7995,7 +8025,7 @@ export function createDecisionServer(options) {
|
|
|
7995
8025
|
if (who.via !== 'cookie' || who.role !== 'approver')
|
|
7996
8026
|
return refuse(response, who, 403, 'Sign in as an approver to decide memory proposals.', '/settings/knowledge');
|
|
7997
8027
|
const repo = body.get('repo') ?? '', decision = body.get('decision') ?? '';
|
|
7998
|
-
if (!visible(repo) || !
|
|
8028
|
+
if (!visible(repo) || !consoleProjects().includes(repo))
|
|
7999
8029
|
return refuse(response, who, 403, 'That project is outside your access.', '/settings/knowledge');
|
|
8000
8030
|
if (!['accept', 'reject'].includes(decision) || !/^[0-9]{1,12}$/.test(body.get('proposal') ?? ''))
|
|
8001
8031
|
return refuse(response, who, 400, 'Choose accept or reject for one proposal.', '/settings/knowledge');
|
|
@@ -8011,7 +8041,7 @@ export function createDecisionServer(options) {
|
|
|
8011
8041
|
if (who.via !== 'cookie' || who.role !== 'approver')
|
|
8012
8042
|
return refuse(response, who, 403, 'Sign in as an approver to change project decisions.', '/settings/knowledge');
|
|
8013
8043
|
const repo = body.get('repo') ?? '', action = body.get('action') ?? '';
|
|
8014
|
-
if (!visible(repo) || !
|
|
8044
|
+
if (!visible(repo) || !consoleProjects().includes(repo))
|
|
8015
8045
|
return refuse(response, who, 403, 'That project is outside your access.', '/settings/knowledge');
|
|
8016
8046
|
if (['repo', 'action', 'claim', 'why', 'decision', 'reason'].some(k => body.getAll(k).length > 1) || !['record', 'retire'].includes(action))
|
|
8017
8047
|
return refuse(response, who, 400, 'Choose record or retire.', '/settings/knowledge');
|
|
@@ -8063,7 +8093,7 @@ export function createDecisionServer(options) {
|
|
|
8063
8093
|
if (who.via !== "cookie")
|
|
8064
8094
|
return refuse(response, who, 403, "Sign in to change learning.", "/settings/learning");
|
|
8065
8095
|
const repo = body.get("repo") ?? "", action = body.get("action") ?? "";
|
|
8066
|
-
if (!visible(repo) || !
|
|
8096
|
+
if (!visible(repo) || !consoleProjects().includes(repo))
|
|
8067
8097
|
return refuse(response, who, 403, "That project is outside your access.", "/settings/learning");
|
|
8068
8098
|
if (["repo", "action", "identity", "revision", "lesson", "version", "sha"].some(k => body.getAll(k).length > 1) || !["adopt", "disable", "reset", "enable", "pause"].includes(action) || !/^[0-9]+$/.test(body.get("revision") ?? ""))
|
|
8069
8099
|
return refuse(response, who, 400, "Invalid learning form.", "/settings/learning");
|
|
@@ -8505,6 +8535,25 @@ export function createDecisionServer(options) {
|
|
|
8505
8535
|
sessions.persist(who.session);
|
|
8506
8536
|
return redirect(response, safeReturn(body.get("return")));
|
|
8507
8537
|
}
|
|
8538
|
+
if (url.pathname === "/projects/remove") {
|
|
8539
|
+
// The reversible remove: off every list and the builder; tasks, results and settings stay. Deleting is
|
|
8540
|
+
// Settings → Project → Delete project. The same admission code as `repos remove` (project-admission.ts).
|
|
8541
|
+
if (who.via !== "cookie" || !store.isInstanceOperator(who.name))
|
|
8542
|
+
return refuse(response, who, 403, "An instance operator removes projects.", "/settings/project");
|
|
8543
|
+
const repo = body.get("repo") ?? "";
|
|
8544
|
+
if (!consoleProjects().includes(repo))
|
|
8545
|
+
return refuse(response, who, 403, "That project is outside your access.", "/settings/project");
|
|
8546
|
+
const released = await releaseProject(store, { registryFile: options.registryPath ?? null, path: repo, actor: { label: who.name, origin: "console" }, now });
|
|
8547
|
+
if (!released.ok)
|
|
8548
|
+
return redirect(response, `/settings/project?repo=${encodeURIComponent(repo)}&problem=${encodeURIComponent(`${projectName(repo)} wasn't removed: ${released.message}`)}`);
|
|
8549
|
+
deletedRepos.add(repo);
|
|
8550
|
+
if (who.session.project === repo) {
|
|
8551
|
+
who.session.project = null;
|
|
8552
|
+
who.session.projectRevision += 1;
|
|
8553
|
+
sessions.persist(who.session);
|
|
8554
|
+
}
|
|
8555
|
+
return redirect(response, `/settings/project?said=${encodeURIComponent(`Removed ${projectName(repo)}. Its tasks and results stay saved.`)}`);
|
|
8556
|
+
}
|
|
8508
8557
|
if (url.pathname === "/projects/open") {
|
|
8509
8558
|
// Sessions only: a bearer caller names its project per request and has
|
|
8510
8559
|
// no session to mutate — refusing here keeps that boundary legible.
|
|
@@ -8526,18 +8575,19 @@ export function createDecisionServer(options) {
|
|
|
8526
8575
|
if (!(await isGitRepo(canonical))) {
|
|
8527
8576
|
return void projectsScreen(response, who, "that path is not a git repository", 400, safeReturn(body.get("return")));
|
|
8528
8577
|
}
|
|
8529
|
-
//
|
|
8530
|
-
|
|
8531
|
-
|
|
8532
|
-
|
|
8533
|
-
|
|
8534
|
-
|
|
8535
|
-
|
|
8536
|
-
|
|
8537
|
-
|
|
8578
|
+
// An account limited to listed projects never widens its own list.
|
|
8579
|
+
if (!visible(canonical)) {
|
|
8580
|
+
return void projectsScreen(response, who, "That project is outside your access.", 403, safeReturn(body.get("return")));
|
|
8581
|
+
}
|
|
8582
|
+
// Opening an allowed repository admits it the same way `repos add` does
|
|
8583
|
+
// (project-admission.ts). A co-located `up` notices the durable list and
|
|
8584
|
+
// connects its builder; there is no separate restart or binding step.
|
|
8585
|
+
const admitted = await admitProject(store, { registryFile: options.registryPath ?? null, path: canonical, actor: { label: who.name, origin: "console" }, now });
|
|
8586
|
+
if (!admitted.ok) {
|
|
8587
|
+
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}`, 400, safeReturn(body.get("return")));
|
|
8538
8588
|
}
|
|
8539
|
-
|
|
8540
|
-
who.session.project =
|
|
8589
|
+
deletedRepos.delete(admitted.repo);
|
|
8590
|
+
who.session.project = admitted.repo;
|
|
8541
8591
|
who.session.projectRevision++;
|
|
8542
8592
|
// The switcher (board pass) opens a project from any screen and
|
|
8543
8593
|
// returns there — a same-site path only, never an off-site road.
|
|
@@ -9270,14 +9320,11 @@ export function createDecisionServer(options) {
|
|
|
9270
9320
|
if (admitted === null) {
|
|
9271
9321
|
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);
|
|
9272
9322
|
}
|
|
9273
|
-
|
|
9274
|
-
|
|
9275
|
-
|
|
9276
|
-
if (!enrolled.ok) {
|
|
9277
|
-
return projectsScreen(response, who, `${cloned.target} is cloned but not enrolled — ${enrolled.message}`, 400);
|
|
9278
|
-
}
|
|
9323
|
+
const joined = await admitProject(store, { registryFile: options.registryPath ?? null, path: admitted, actor: { label: who.name, origin: "console" }, now });
|
|
9324
|
+
if (!joined.ok) {
|
|
9325
|
+
return projectsScreen(response, who, `${cloned.target} is cloned but not added — ${joined.message}`, 400);
|
|
9279
9326
|
}
|
|
9280
|
-
|
|
9327
|
+
deletedRepos.delete(admitted);
|
|
9281
9328
|
who.session.project = admitted;
|
|
9282
9329
|
who.session.projectRevision += 1;
|
|
9283
9330
|
return projectsScreen(response, who, options.upConsole === true
|
|
@@ -10853,8 +10900,8 @@ export function createDecisionServer(options) {
|
|
|
10853
10900
|
if (document === current.document.trim())
|
|
10854
10901
|
return redirect(response, taskHref(taskId));
|
|
10855
10902
|
const content = Buffer.from(`${document}\n`, "utf8");
|
|
10856
|
-
if (content.length >
|
|
10857
|
-
return taskScreen(response, who, taskId, `plan not saved: it is over ${
|
|
10903
|
+
if (content.length > TEXT_LIMITS.planDocumentBytes) {
|
|
10904
|
+
return taskScreen(response, who, taskId, `plan not saved: it is over ${TEXT_LIMITS.planDocumentBytes} bytes`, 400);
|
|
10858
10905
|
}
|
|
10859
10906
|
try {
|
|
10860
10907
|
const name = `plan-edit-${randomUUID().replace(/-/g, "")}.md`;
|
|
@@ -22461,32 +22508,6 @@ function runsPage(chrome, rows, liveIds, nextCursor, verdicts = new Map(), accep
|
|
|
22461
22508
|
const older = nextCursor === null ? "" : `<p><a href="/runs?before=${nextCursor}">older →</a></p>`;
|
|
22462
22509
|
return screen("builds", [`<h1>Builds <a class="badge" href="/peek">Peek at the live ones \u2192</a></h1>`, buildsViews("builds"), `<p class="hint">one build = one attempt by an agent to complete a task, on its own branch</p>`, list, older].join("\n"), { chrome });
|
|
22463
22510
|
}
|
|
22464
|
-
function structuredHandoffView(artifacts, root) {
|
|
22465
|
-
const artifact = [...artifacts].reverse().find(one => one.kind === "handoff");
|
|
22466
|
-
if (artifact === undefined)
|
|
22467
|
-
return null;
|
|
22468
|
-
const read = readVerifiedArtifact(root, artifact);
|
|
22469
|
-
if (!read.ok)
|
|
22470
|
-
return null;
|
|
22471
|
-
try {
|
|
22472
|
-
const parsed = JSON.parse(read.content.toString("utf8"));
|
|
22473
|
-
if (parsed === null || typeof parsed !== "object" || typeof parsed["conclusion"] !== "string")
|
|
22474
|
-
return null;
|
|
22475
|
-
const conclusion = oneLineOf(parsed["conclusion"], 600);
|
|
22476
|
-
if (conclusion === "" || hasForbiddenControls(conclusion))
|
|
22477
|
-
return null;
|
|
22478
|
-
const list = (name) => Array.isArray(parsed[name])
|
|
22479
|
-
? parsed[name]
|
|
22480
|
-
.filter((one) => typeof one === "string" && one.trim() !== "" && !hasForbiddenControls(one))
|
|
22481
|
-
.slice(0, 8)
|
|
22482
|
-
.map(one => oneLineOf(one, 240))
|
|
22483
|
-
: [];
|
|
22484
|
-
return { conclusion, changes: list("changes"), verification: list("verification"), followUps: list("followUps") };
|
|
22485
|
-
}
|
|
22486
|
-
catch {
|
|
22487
|
-
return null;
|
|
22488
|
-
}
|
|
22489
|
-
}
|
|
22490
22511
|
/** What a scout found, each with its link and picture, then any screenshot no item shows. Every link is the cited
|
|
22491
22512
|
* http(s) address the report parser admitted; every picture is served from verified evidence. */
|
|
22492
22513
|
function reportItemsHtml(items, shots, runId) {
|
|
@@ -22563,7 +22584,6 @@ function sharedResultFactsOf(run, proof, terminal, handoff, report, publication)
|
|
|
22563
22584
|
publicationWords: receiptPublicationWords(publication),
|
|
22564
22585
|
};
|
|
22565
22586
|
}
|
|
22566
|
-
const SCREENSHOT_CAPTURE = /^agent-claimed screenshot at (.+) \(validated (?:png|jpeg)\)/;
|
|
22567
22587
|
function proofBundleView(store, run, artifacts, root) {
|
|
22568
22588
|
const verdictRow = store.proofVerdictFor(run.id);
|
|
22569
22589
|
const acceptanceRow = store.proofAcceptance(run.id);
|
|
@@ -22679,74 +22699,6 @@ function completionReceiptView(store, run, artifacts, root, review = null) {
|
|
|
22679
22699
|
facts: sharedResultFactsOf(run, proof, terminal, handoff, report, publication),
|
|
22680
22700
|
};
|
|
22681
22701
|
}
|
|
22682
|
-
const CAPTURE_EXIT = /\(exit ([0-9]{1,4})\)\s*$/;
|
|
22683
|
-
/**
|
|
22684
|
-
* Assemble the terminal-diff card's facts. Every branch names its state:
|
|
22685
|
-
* a failed capture (nonzero exit recorded in the capture string) reads as
|
|
22686
|
-
* the failure it is, unverifiable bytes read as their problem, and absence
|
|
22687
|
-
* returns null so old runs simply show nothing rather than a broken card.
|
|
22688
|
-
*/
|
|
22689
|
-
function terminalDiffView(artifacts, root) {
|
|
22690
|
-
const patchArtifact = artifacts.find(one => one.kind === "terminal-diff");
|
|
22691
|
-
const statArtifact = artifacts.find(one => one.kind === "diff-stat");
|
|
22692
|
-
if (patchArtifact === undefined && statArtifact === undefined)
|
|
22693
|
-
return null;
|
|
22694
|
-
const view = { patch: null, stat: null };
|
|
22695
|
-
if (patchArtifact !== undefined) {
|
|
22696
|
-
const exit = CAPTURE_EXIT.exec(patchArtifact.capture);
|
|
22697
|
-
if (exit !== null && exit[1] !== "0") {
|
|
22698
|
-
view.patch = { problem: `capture failed — ${patchArtifact.capture}` };
|
|
22699
|
-
}
|
|
22700
|
-
else {
|
|
22701
|
-
const read = readVerifiedArtifact(root, patchArtifact);
|
|
22702
|
-
view.patch = read.ok
|
|
22703
|
-
? { text: read.content.toString("utf8"), truncated: patchArtifact.truncated, artifactId: patchArtifact.id }
|
|
22704
|
-
: { problem: `stored but unverifiable — ${read.problem}` };
|
|
22705
|
-
}
|
|
22706
|
-
}
|
|
22707
|
-
if (statArtifact !== undefined) {
|
|
22708
|
-
const exit = CAPTURE_EXIT.exec(statArtifact.capture);
|
|
22709
|
-
if (exit !== null && exit[1] !== "0") {
|
|
22710
|
-
view.stat = { problem: `capture failed — ${statArtifact.capture}` };
|
|
22711
|
-
}
|
|
22712
|
-
else {
|
|
22713
|
-
const read = readVerifiedArtifact(root, statArtifact);
|
|
22714
|
-
if (!read.ok) {
|
|
22715
|
-
view.stat = { problem: `stored but unverifiable — ${read.problem}` };
|
|
22716
|
-
}
|
|
22717
|
-
else {
|
|
22718
|
-
try {
|
|
22719
|
-
const parsed = JSON.parse(read.content.toString("utf8"));
|
|
22720
|
-
// Every field this page renders is type-proved (arc 6, finding 7):
|
|
22721
|
-
// "an object with a base key" was accepting any shape at all.
|
|
22722
|
-
const count = (value) => typeof value === "number" && Number.isFinite(value);
|
|
22723
|
-
const wellFormed = parsed !== null &&
|
|
22724
|
-
typeof parsed === "object" &&
|
|
22725
|
-
typeof parsed["base"] === "string" &&
|
|
22726
|
-
typeof parsed["head"] === "string" &&
|
|
22727
|
-
count(parsed["fileCount"]) &&
|
|
22728
|
-
count(parsed["additions"]) &&
|
|
22729
|
-
count(parsed["deletions"]) &&
|
|
22730
|
-
count(parsed["binaryCount"]) &&
|
|
22731
|
-
typeof parsed["filesTruncated"] === "boolean" &&
|
|
22732
|
-
Array.isArray(parsed["files"]) &&
|
|
22733
|
-
parsed["files"].every(one => one !== null &&
|
|
22734
|
-
typeof one === "object" &&
|
|
22735
|
-
typeof one["path"] === "string" &&
|
|
22736
|
-
(count(one["additions"]) || one["additions"] === null) &&
|
|
22737
|
-
(count(one["deletions"]) || one["deletions"] === null));
|
|
22738
|
-
view.stat = wellFormed
|
|
22739
|
-
? parsed
|
|
22740
|
-
: { problem: "stat is not the shape this page knows" };
|
|
22741
|
-
}
|
|
22742
|
-
catch {
|
|
22743
|
-
view.stat = { problem: "stat did not parse as JSON" };
|
|
22744
|
-
}
|
|
22745
|
-
}
|
|
22746
|
-
}
|
|
22747
|
-
}
|
|
22748
|
-
return view;
|
|
22749
|
-
}
|
|
22750
22702
|
const REVIEW_DIFF_LINE_CAP = 4_000;
|
|
22751
22703
|
/** Parse only the stable structure Git's unified patch format guarantees.
|
|
22752
22704
|
* Unknown metadata remains visible, and a patch that cannot be structured
|