toolroll 0.9.32 → 0.9.34
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 +22 -0
- 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 +2 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +124 -63
- package/dist/coding-context.js +2 -2
- package/dist/coding-handoff.js +2 -2
- package/dist/contracts/contract-test.d.ts +45 -0
- package/dist/contracts/contract-test.js +69 -0
- package/dist/contracts/contract.d.ts +59 -0
- package/dist/contracts/contract.js +181 -0
- package/dist/contracts/plan.d.ts +58 -0
- package/dist/contracts/plan.js +67 -0
- package/dist/desktop-update.d.ts +1 -1
- package/dist/evidence.js +2 -2
- package/dist/guides.js +3 -0
- package/dist/knowledge-cli.d.ts +31 -0
- package/dist/knowledge-cli.js +35 -2
- package/dist/knowledge-ui.d.ts +1 -1
- package/dist/knowledge-ui.js +7 -0
- package/dist/operate.js +65 -13
- package/dist/plan.d.ts +12 -12
- package/dist/plan.js +129 -72
- package/dist/planner-source.js +2 -1
- package/dist/planner.js +55 -25
- 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/project-delete.js +1 -1
- package/dist/project-knowledge.d.ts +28 -3
- package/dist/project-knowledge.js +64 -25
- package/dist/project-learning.d.ts +33 -0
- package/dist/project-learning.js +105 -15
- package/dist/project-skills.js +27 -21
- 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 +111 -65
- package/dist/store.d.ts +2 -0
- package/dist/store.js +4 -0
- package/dist/surface.js +9 -3
- package/dist/text-limits.d.ts +17 -0
- package/dist/text-limits.js +16 -0
- package/dist/when-html.d.ts +2 -0
- package/dist/when-html.js +13 -0
- package/package.json +3 -2
package/dist/scope.js
CHANGED
|
@@ -30,6 +30,7 @@ import { claimActor, currentActor, parseLeadToken } from "./actor.js";
|
|
|
30
30
|
*/
|
|
31
31
|
import { createHash, randomBytes, scryptSync, timingSafeEqual } from "node:crypto";
|
|
32
32
|
import { hasForbiddenControls } from "./decision.js";
|
|
33
|
+
import { TEXT_LIMITS } from "./text-limits.js";
|
|
33
34
|
import { NO_READINESS, exactModelId, isRiskLevel, legOf, projectRoute, routeDigestOf, routeFromJson, routeWords, } from "./phase-routing.js";
|
|
34
35
|
/** Same shape as a runner's credential, for the same reasons. */
|
|
35
36
|
function mintToken() {
|
|
@@ -399,11 +400,11 @@ export function profileFromJson(json) {
|
|
|
399
400
|
export const EVIDENCE_KINDS = ["check", "screenshot", "changed-path", "manual-review"];
|
|
400
401
|
export const ACCEPTANCE_LIMITS = {
|
|
401
402
|
criteria: 12,
|
|
402
|
-
id:
|
|
403
|
+
id: TEXT_LIMITS.acceptanceIdBytes,
|
|
403
404
|
// A statement is one testable outcome; three hundred bytes forced people to
|
|
404
405
|
// drop the qualifying clause that made it testable.
|
|
405
|
-
statement:
|
|
406
|
-
how:
|
|
406
|
+
statement: TEXT_LIMITS.acceptanceStatementBytes,
|
|
407
|
+
how: TEXT_LIMITS.acceptanceHowBytes,
|
|
407
408
|
evidenceKinds: EVIDENCE_KINDS.length,
|
|
408
409
|
};
|
|
409
410
|
function acceptanceDescribe(value) {
|
package/dist/serve.d.ts
CHANGED
|
@@ -145,6 +145,10 @@ export type ServeOptions = {
|
|
|
145
145
|
desktopIdentity?: string;
|
|
146
146
|
/** Trusted native admission callback; enrollment rows alone never supply this authority. */
|
|
147
147
|
additionalProjectRepos?: () => readonly string[];
|
|
148
|
+
/** The co-located `up`'s exact admitted repositories, read on every request: its startup projects plus those
|
|
149
|
+
* added since (`repos add`, the lead, the console) and minus those removed. When given it replaces `repos` as
|
|
150
|
+
* the exact part of the ceiling, so an addition or removal shows without a restart. */
|
|
151
|
+
admittedRepos?: () => readonly string[];
|
|
148
152
|
connectionProbe?: typeof execRun;
|
|
149
153
|
connectionHome?: string;
|
|
150
154
|
/** Test seam for Chat's first tasks: the `gh` and `git grep` reads. */
|
package/dist/serve.js
CHANGED
|
@@ -56,7 +56,7 @@ import { chooseFlowCard } from "./flow-send.js";
|
|
|
56
56
|
import { FLOW_TEMPLATES, validateFlowDefinition } from "./flows.js";
|
|
57
57
|
import { TOOL_CATALOG, addToolTo, catalogTool, discoverTools, localAppOf, projectToolsOf, removeToolFrom, secretsSetFor, setToolSecret, splitCommandLine, testToolOf } from "./project-tools.js";
|
|
58
58
|
import { changeLearning, learningView } from "./project-learning.js";
|
|
59
|
-
import { changeKnowledge, knowledgeView, knowledgeVersion, readKnowledgeSnapshot } from "./project-knowledge.js";
|
|
59
|
+
import { applySavedKnowledge, changeKnowledge, knowledgeView, knowledgeVersion, readKnowledgeSnapshot } from "./project-knowledge.js";
|
|
60
60
|
import { knowledgeHtml, knowledgeContextHtml, KNOWLEDGE_CSS, decisionsHtml, memorySearchHtml, MEMORY_INTRO, proposalsHtml } from "./knowledge-ui.js";
|
|
61
61
|
import { listDecisions, recordDecision, retireDecision, searchMemory } from "./project-memory.js";
|
|
62
62
|
import { decideProposal, listProposals, memoryStatus } from "./memory-pass.js";
|
|
@@ -178,12 +178,13 @@ import { parseReport } from "./scout-report.js";
|
|
|
178
178
|
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
179
|
import { fileTaskProposal, fileRoutineProposal } from "./proposal.js";
|
|
180
180
|
import { TASK_TEXT_LIMITS, validateTaskText } from "./task-text.js";
|
|
181
|
+
import { TEXT_LIMITS } from "./text-limits.js";
|
|
181
182
|
import {} from "./store.js";
|
|
182
183
|
import { attendedLivenessState } from "./liveness.js";
|
|
183
184
|
import { approvalOf, approve as approveScope, attendedDigestOf, attendedTermsJson, authenticateApprover as checkApproverPassword, canonicalProfileJson, chainFromJson, profileDigestOf, proposeGuarded, scopeAuthorityOf, acceptanceLinesToInput, acceptanceToLines, acceptanceWords, parseAcceptanceCriteria, } from "./scope.js";
|
|
184
185
|
import { isQualityMode, qualityModeTitle } from "./quality.js";
|
|
185
186
|
import { LIMITS, hasForbiddenControls, validateNote } from "./decision.js";
|
|
186
|
-
import { parseExecutionPlanDocument,
|
|
187
|
+
import { parseExecutionPlanDocument, milestonesOf } from "./plan.js";
|
|
187
188
|
import { contractChangesOf, decodePlanContractRecord, describeContractChanges } from "./planner-source.js";
|
|
188
189
|
import { observeWorktree, parseBaseTreeSnapshot, aggregateNewNames, PEEK_LIMITS } from "./peek.js";
|
|
189
190
|
import { readLiveWindow } from "./live.js";
|
|
@@ -194,7 +195,8 @@ import { parseGithubRepo, previewGithubRepo, cloneGithubRepo, listGithubRepos, i
|
|
|
194
195
|
import { verifiedAuthor, LEAD_THREAD, isDigestTime, RESULT_SCREENSHOTS, MATE_ASK_OTHER } from "./store.js";
|
|
195
196
|
import { RESULT_SHOT_CHOICES } from "./result-shots.js";
|
|
196
197
|
import { digestTimes } from "./digest-times.js";
|
|
197
|
-
import { updateRepos,
|
|
198
|
+
import { updateRepos, removeRepos } from "./repos.js";
|
|
199
|
+
import { admitProject, releaseProject } from "./project-admission.js";
|
|
198
200
|
import { run as execRun } from "./exec.js";
|
|
199
201
|
import { findFirstTasks, firstResultWords, firstRunSteps, firstTaskJourney, firstTaskSuggestions, HOW_IT_WORKS, SANDBOX_COMMAND, signInCommandFor, START_COMMAND } from "./first-run.js";
|
|
200
202
|
/** A user agent, reduced to safe display words — never echoed raw. */
|
|
@@ -276,6 +278,8 @@ import { PACKAGE_VERSION } from "./version.js";
|
|
|
276
278
|
import { updateNoticeWords } from "./update-notice.js";
|
|
277
279
|
import { whenHtml } from "./when-html.js";
|
|
278
280
|
const SESSION_COOKIE = "standing-orders_session";
|
|
281
|
+
/** Stands in for "no project" in a ceiling: no folder resolves to it, so it admits nothing. */
|
|
282
|
+
const NO_PROJECT = "/\0no-project";
|
|
279
283
|
/** Where `up`'s one-time sign-in link points, and how long it works. */
|
|
280
284
|
export const SIGN_IN_LINK_PATH = "/login/once/";
|
|
281
285
|
export const SIGN_IN_LINK_MS = 10 * 60_000;
|
|
@@ -632,7 +636,13 @@ export function createDecisionServer(options) {
|
|
|
632
636
|
/** No ceiling configured at all: the legacy trust-everything mode, named. */
|
|
633
637
|
const unscopedMode = ceiling.repos.length === 0 && ceiling.roots.length === 0;
|
|
634
638
|
/** Per-row visibility under the ceiling — the authorization question for reads. */
|
|
635
|
-
|
|
639
|
+
/** The exact repositories this console serves now: `up`'s live admitted set when it supplies one, else startup's. */
|
|
640
|
+
const exactRepos = () => options.admittedRepos?.() ?? ceiling.repos;
|
|
641
|
+
const liveCeiling = () => {
|
|
642
|
+
const repos = [...exactRepos(), ...(options.additionalProjectRepos?.() ?? [])];
|
|
643
|
+
// An admitted set that is empty for now is an empty ceiling, never the legacy unscoped mode.
|
|
644
|
+
return { repos: repos.length === 0 && ceiling.roots.length === 0 && !unscopedMode ? [NO_PROJECT] : repos, roots: ceiling.roots };
|
|
645
|
+
};
|
|
636
646
|
// Native tools share the installation's login and can read beyond a project.
|
|
637
647
|
// Account project membership cannot provide an OS read boundary.
|
|
638
648
|
function codingActorAllowed(actor) {
|
|
@@ -651,12 +661,21 @@ export function createDecisionServer(options) {
|
|
|
651
661
|
* ceiling; this server still filters every row through its own ceiling. */
|
|
652
662
|
/** Projects deleted while this console runs: gone from its lists though a builder started with them still watches. */
|
|
653
663
|
const deletedRepos = new Set();
|
|
664
|
+
/** Those still gone: a project added again (from here, `repos add`, or the lead) has its row back and shows again. */
|
|
665
|
+
const goneRepos = () => {
|
|
666
|
+
if (deletedRepos.size === 0)
|
|
667
|
+
return deletedRepos;
|
|
668
|
+
for (const one of store.listProjects())
|
|
669
|
+
deletedRepos.delete(one.path);
|
|
670
|
+
return deletedRepos;
|
|
671
|
+
};
|
|
654
672
|
const managedRepos = () => {
|
|
655
673
|
const seen = new Set();
|
|
656
674
|
const repos = [];
|
|
657
|
-
|
|
675
|
+
const gone = goneRepos();
|
|
676
|
+
for (const path of [...exactRepos(), ...(options.currentRepos?.() ?? [])]) {
|
|
658
677
|
const canonical = canonicalProject(path) ?? path;
|
|
659
|
-
if (seen.has(canonical) || !visible(canonical) ||
|
|
678
|
+
if (seen.has(canonical) || !visible(canonical) || gone.has(canonical))
|
|
660
679
|
continue;
|
|
661
680
|
seen.add(canonical);
|
|
662
681
|
repos.push(canonical);
|
|
@@ -761,6 +780,13 @@ export function createDecisionServer(options) {
|
|
|
761
780
|
const admissionList = () => restricted() ? [...(store.accountOf(requestContext.getStore().actor)?.projects ?? [])].filter(visible) : unscopedMode
|
|
762
781
|
? null
|
|
763
782
|
: [...new Set([...managedRepos(), ...(ceiling.roots.length === 0 ? [] : store.knownRepos().filter(visible))])];
|
|
783
|
+
/** Every project this person may pick in the console — the switcher, Projects, Settings, Tools and Flows read this
|
|
784
|
+
* one list: the live admitted set (taskless projects included) and every project with saved work, each through
|
|
785
|
+
* the ceiling and this account's project grants. A project removed or deleted here is gone from it. */
|
|
786
|
+
const consoleProjects = () => {
|
|
787
|
+
const gone = goneRepos();
|
|
788
|
+
return [...new Set([...(admissionList() ?? []), ...managedRepos(), ...store.knownRepos()])].filter(one => visible(one) && !gone.has(one));
|
|
789
|
+
};
|
|
764
790
|
/** The task behind a resource, for the ceiling check; null = no ref (visible). */
|
|
765
791
|
const taskRepoOf = (taskRef) => store.refForId(taskRef)?.repo ?? null;
|
|
766
792
|
const server = createServer((request, response) => {
|
|
@@ -2519,7 +2545,7 @@ export function createDecisionServer(options) {
|
|
|
2519
2545
|
return void response.end(spendCsv(items, teammateNames));
|
|
2520
2546
|
}
|
|
2521
2547
|
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 =
|
|
2548
|
+
const projects = consoleProjects();
|
|
2523
2549
|
const people = store.accountFacts().filter(one => one.revokedAt === null).map(one => one.name);
|
|
2524
2550
|
const view = {
|
|
2525
2551
|
month: month.name, previous: shift(-1), next: month.name === current.name ? null : shift(1),
|
|
@@ -2535,7 +2561,7 @@ export function createDecisionServer(options) {
|
|
|
2535
2561
|
}
|
|
2536
2562
|
if (url.pathname === "/ledger") {
|
|
2537
2563
|
const admitted = admissionList();
|
|
2538
|
-
const projects = admitted ??
|
|
2564
|
+
const projects = admitted ?? consoleProjects();
|
|
2539
2565
|
const chosen = url.searchParams.get("project") ?? "";
|
|
2540
2566
|
if (chosen !== "" && !projects.includes(chosen))
|
|
2541
2567
|
return refuse(response, who, 403, "That project is outside your access.", "/ledger");
|
|
@@ -2694,7 +2720,7 @@ export function createDecisionServer(options) {
|
|
|
2694
2720
|
return seen;
|
|
2695
2721
|
};
|
|
2696
2722
|
const csrf = who.via === "cookie" ? who.session.csrf : "";
|
|
2697
|
-
const projectChoices =
|
|
2723
|
+
const projectChoices = consoleProjects();
|
|
2698
2724
|
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
2725
|
`<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
2726
|
const cards = accounts.map(one => {
|
|
@@ -3192,7 +3218,7 @@ export function createDecisionServer(options) {
|
|
|
3192
3218
|
}
|
|
3193
3219
|
// Flows → New: the gallery, and each template's page.
|
|
3194
3220
|
if (url.pathname === "/flows/new") {
|
|
3195
|
-
const projects =
|
|
3221
|
+
const projects = consoleProjects();
|
|
3196
3222
|
const canUse = who.via === "cookie" && who.role === "approver" && projects.length > 0;
|
|
3197
3223
|
// The chosen project, else the one being looked at: its tools are what each card says is connected.
|
|
3198
3224
|
const asked = url.searchParams.get("repo");
|
|
@@ -3202,7 +3228,7 @@ export function createDecisionServer(options) {
|
|
|
3202
3228
|
const galleryRead = /^\/flows\/new\/([a-z-]{1,40})$/.exec(url.pathname);
|
|
3203
3229
|
if (galleryRead !== null) {
|
|
3204
3230
|
const template = galleryTemplateOf(galleryRead[1]);
|
|
3205
|
-
const projects =
|
|
3231
|
+
const projects = consoleProjects();
|
|
3206
3232
|
if (template === null)
|
|
3207
3233
|
return refuse(response, who, 404, "There's no template by that name.", "/flows/new");
|
|
3208
3234
|
if (who.via !== "cookie" || who.role !== "approver")
|
|
@@ -3216,13 +3242,13 @@ export function createDecisionServer(options) {
|
|
|
3216
3242
|
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
3243
|
}
|
|
3218
3244
|
if (url.pathname === "/flows") {
|
|
3219
|
-
const projects =
|
|
3245
|
+
const projects = consoleProjects();
|
|
3220
3246
|
const flows = store.listFlows(projects);
|
|
3221
3247
|
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
3248
|
}
|
|
3223
3249
|
// starter kits — the gallery, and each kit's checklist in a project.
|
|
3224
3250
|
if (url.pathname === "/kits" || /^\/kits\/[a-z-]{1,40}$/.test(url.pathname)) {
|
|
3225
|
-
const projects =
|
|
3251
|
+
const projects = consoleProjects();
|
|
3226
3252
|
const csrf = who.via === "cookie" ? who.session.csrf : "", canSetUp = who.via === "cookie" && who.role === "approver";
|
|
3227
3253
|
if (url.pathname === "/kits") {
|
|
3228
3254
|
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 +3261,7 @@ export function createDecisionServer(options) {
|
|
|
3235
3261
|
}
|
|
3236
3262
|
// v92: AI teammates — the team, and one page per teammate.
|
|
3237
3263
|
if (url.pathname === "/teammates") {
|
|
3238
|
-
const projects =
|
|
3264
|
+
const projects = consoleProjects();
|
|
3239
3265
|
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
3266
|
}
|
|
3241
3267
|
const teammateRead = /^\/teammates\/([1-9][0-9]{0,9})(\/soul\.md)?$/.exec(url.pathname);
|
|
@@ -3307,7 +3333,7 @@ export function createDecisionServer(options) {
|
|
|
3307
3333
|
if (flow === null || flow.state !== "active" || !visible(flow.repo))
|
|
3308
3334
|
return refuse(response, who, 404, "No such flow in your projects.", "/flows");
|
|
3309
3335
|
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:
|
|
3336
|
+
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
3337
|
sortReady: keyStatus("openrouter", providerHome).set, toolHome });
|
|
3312
3338
|
if (url.searchParams.get("format") === "json")
|
|
3313
3339
|
return respond(response, 200, "application/json; charset=utf-8", JSON.stringify(view));
|
|
@@ -3886,7 +3912,7 @@ export function createDecisionServer(options) {
|
|
|
3886
3912
|
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
3913
|
}
|
|
3888
3914
|
if (url.pathname === "/settings/skills") {
|
|
3889
|
-
const projects =
|
|
3915
|
+
const projects = consoleProjects();
|
|
3890
3916
|
const chosen = url.searchParams.get("repo") ?? project ?? projects[0] ?? "";
|
|
3891
3917
|
if (chosen && !projects.includes(chosen))
|
|
3892
3918
|
return refuse(response, who, 403, "That project is outside your access.", "/projects");
|
|
@@ -3904,7 +3930,7 @@ export function createDecisionServer(options) {
|
|
|
3904
3930
|
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
3931
|
}
|
|
3906
3932
|
if (url.pathname === "/settings/tools") {
|
|
3907
|
-
const projects =
|
|
3933
|
+
const projects = consoleProjects();
|
|
3908
3934
|
const chosen = url.searchParams.get("repo") ?? project ?? projects[0] ?? "";
|
|
3909
3935
|
if (chosen && !projects.includes(chosen))
|
|
3910
3936
|
return refuse(response, who, 403, "That project is outside your access.", "/projects");
|
|
@@ -3928,7 +3954,7 @@ export function createDecisionServer(options) {
|
|
|
3928
3954
|
}
|
|
3929
3955
|
// Settings → Project: what Toolroll holds for a project; an instance operator deletes it here.
|
|
3930
3956
|
if (url.pathname === "/settings/project") {
|
|
3931
|
-
const projects =
|
|
3957
|
+
const projects = consoleProjects();
|
|
3932
3958
|
const asked = url.searchParams.get("repo");
|
|
3933
3959
|
const chosen = asked ?? (project != null && projects.includes(project) ? project : projects[0] ?? "");
|
|
3934
3960
|
if (chosen && !projects.includes(chosen))
|
|
@@ -3940,7 +3966,7 @@ export function createDecisionServer(options) {
|
|
|
3940
3966
|
}
|
|
3941
3967
|
// v102: Settings → Approval rules, a project's separation of duties. Anyone who sees the project reads them; an instance operator sets them.
|
|
3942
3968
|
if (url.pathname === "/settings/approval") {
|
|
3943
|
-
const projects =
|
|
3969
|
+
const projects = consoleProjects();
|
|
3944
3970
|
const chosen = url.searchParams.get("repo") ?? project ?? projects[0] ?? "";
|
|
3945
3971
|
if (chosen && !projects.includes(chosen))
|
|
3946
3972
|
return refuse(response, who, 403, "That project is outside your access.", "/settings/approval");
|
|
@@ -3951,7 +3977,7 @@ export function createDecisionServer(options) {
|
|
|
3951
3977
|
}
|
|
3952
3978
|
// Sprint 8: Settings → Policy, the organisation policy and its history. Anyone signed in reads it; an instance operator changes it.
|
|
3953
3979
|
if (url.pathname === "/settings/policy") {
|
|
3954
|
-
const repos =
|
|
3980
|
+
const repos = consoleProjects();
|
|
3955
3981
|
const toolNames = [...new Set(repos.flatMap(repo => store.projectTools(repo).map(one => one.name)))].sort();
|
|
3956
3982
|
const view = { policy: store.orgPolicy(), history: store.policyHistory(50), canChange: who.via === "cookie" && store.isInstanceOperator(who.name), toolNames };
|
|
3957
3983
|
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 +3995,7 @@ export function createDecisionServer(options) {
|
|
|
3969
3995
|
// Settings → Flows: the starter flows, each switched on with one yes. ?starter= marks the one a task's
|
|
3970
3996
|
// "Do this every time…" asked about.
|
|
3971
3997
|
if (url.pathname === "/settings/flows") {
|
|
3972
|
-
const projects =
|
|
3998
|
+
const projects = consoleProjects();
|
|
3973
3999
|
const chosen = url.searchParams.get("repo") ?? project ?? projects[0] ?? "";
|
|
3974
4000
|
if (chosen && !projects.includes(chosen))
|
|
3975
4001
|
return refuse(response, who, 403, "That project is outside your access.", "/settings");
|
|
@@ -4093,13 +4119,13 @@ export function createDecisionServer(options) {
|
|
|
4093
4119
|
if (who.via !== "cookie" || !store.isInstanceOperator(who.name) || !options.configDir)
|
|
4094
4120
|
return refuse(response, who, 403, "An instance operator sets up sign-in.", "/settings");
|
|
4095
4121
|
const origin = consoleOrigin(request.headers.host);
|
|
4096
|
-
const projects =
|
|
4122
|
+
const projects = consoleProjects().map(path => ({ path, name: projectName(path) }));
|
|
4097
4123
|
const settings = ssoSettings();
|
|
4098
4124
|
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
4125
|
return sendScreen(response, 200, screen("Sign-in", `<p><a href="/settings">Settings</a></p><h1>Sign-in</h1>${html}`, { chrome: chromeFor(project, "settings") }));
|
|
4100
4126
|
}
|
|
4101
4127
|
if (url.pathname === "/settings/knowledge") {
|
|
4102
|
-
const projects =
|
|
4128
|
+
const projects = consoleProjects();
|
|
4103
4129
|
const chosen = url.searchParams.get("repo") ?? project ?? projects[0] ?? "";
|
|
4104
4130
|
if (chosen && (!projects.includes(chosen) || !visible(chosen)))
|
|
4105
4131
|
return refuse(response, who, 403, "That project is outside your access.", "/settings/knowledge");
|
|
@@ -4113,7 +4139,7 @@ export function createDecisionServer(options) {
|
|
|
4113
4139
|
const revision = Number(url.searchParams.get('version'));
|
|
4114
4140
|
const previous = knowledgeVersion(store, chosen, who.name, revision);
|
|
4115
4141
|
const restore = csrf && who.role === 'approver' && revision !== view.revision ? `<form class="knowledge" method="post" action="/settings/knowledge/change">${hiddenFields({ csrf, repo: chosen, identity: view.identity, revision: String(view.revision), action: 'restore', restore: String(revision) })}<p>Replaces knowledge for future tasks. Existing runs keep their saved context.</p><button>Restore version ${revision}</button></form>` : '';
|
|
4116
|
-
return sendScreen(response, 200, screen('Knowledge history', `<p><a href="/settings/knowledge?repo=${encodeURIComponent(chosen)}">Current knowledge</a></p><h1>Version ${revision}</h1>${knowledgeHtml({ ...view, knowledge: previous, history: [] }, '', false)}${restore}`, { chrome: chromeFor(chosen, 'settings') }));
|
|
4142
|
+
return sendScreen(response, 200, screen('Knowledge history', `<p><a href="/settings/knowledge?repo=${encodeURIComponent(chosen)}">Current knowledge</a></p><h1>Version ${revision}</h1>${knowledgeHtml({ ...view, knowledge: previous, history: [], stale: null }, '', false)}${restore}`, { chrome: chromeFor(chosen, 'settings') }));
|
|
4117
4143
|
}
|
|
4118
4144
|
const query = (url.searchParams.get('q') ?? '').slice(0, 1000);
|
|
4119
4145
|
const result = query.trim() ? repositoryContextRead({ repo: chosen, query, mode: url.searchParams.get('mode') === 'impact' ? 'impact' : 'search', cacheRoot: join(dirname(evidenceRoot), 'repository-context') }) : null;
|
|
@@ -4137,7 +4163,7 @@ export function createDecisionServer(options) {
|
|
|
4137
4163
|
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
4164
|
}
|
|
4139
4165
|
if (url.pathname === "/settings/learning") {
|
|
4140
|
-
const projects =
|
|
4166
|
+
const projects = consoleProjects();
|
|
4141
4167
|
const chosen = url.searchParams.get("repo") ?? project ?? projects[0] ?? "";
|
|
4142
4168
|
if (chosen && (!projects.includes(chosen) || !visible(chosen)))
|
|
4143
4169
|
return refuse(response, who, 403, "That project is outside your access.", "/settings/learning");
|
|
@@ -5231,7 +5257,7 @@ export function createDecisionServer(options) {
|
|
|
5231
5257
|
...store.listProjects().map(one => ({ path: one.path, name: one.name })),
|
|
5232
5258
|
...managedRepos().map(path => ({ path, name: projectName(path) })),
|
|
5233
5259
|
]) {
|
|
5234
|
-
if (seen.has(one.path) || !visible(one.path))
|
|
5260
|
+
if (seen.has(one.path) || !visible(one.path) || goneRepos().has(one.path))
|
|
5235
5261
|
continue;
|
|
5236
5262
|
seen.add(one.path);
|
|
5237
5263
|
rows.push(one);
|
|
@@ -5475,7 +5501,7 @@ export function createDecisionServer(options) {
|
|
|
5475
5501
|
const recent = store.listProjects().filter(one => visible(one.path));
|
|
5476
5502
|
const recentPaths = new Set(recent.map(one => one.path));
|
|
5477
5503
|
const candidates = new Set();
|
|
5478
|
-
for (const path of
|
|
5504
|
+
for (const path of consoleProjects()) {
|
|
5479
5505
|
const canonical = canonicalProject(path) ?? path;
|
|
5480
5506
|
if (!recentPaths.has(canonical) && visible(canonical))
|
|
5481
5507
|
candidates.add(canonical);
|
|
@@ -6765,7 +6791,7 @@ export function createDecisionServer(options) {
|
|
|
6765
6791
|
if (who.via !== 'cookie' || who.role !== 'approver')
|
|
6766
6792
|
return refuse(response, who, 403, 'Sign in as an approver to manage skills.', '/settings/skills');
|
|
6767
6793
|
const repo = body.get('repo') ?? '', back = `/settings/skills?repo=${encodeURIComponent(repo)}`;
|
|
6768
|
-
if (!visible(repo) || !
|
|
6794
|
+
if (!visible(repo) || !consoleProjects().includes(repo))
|
|
6769
6795
|
return refuse(response, who, 403, 'That project is outside your access.', '/projects');
|
|
6770
6796
|
if ([...new Set(body.keys())].some(k => body.getAll(k).length !== 1))
|
|
6771
6797
|
return refuse(response, who, 400, 'Invalid skills form.', back);
|
|
@@ -6821,7 +6847,7 @@ export function createDecisionServer(options) {
|
|
|
6821
6847
|
if (who.via !== "cookie" || who.role !== "approver")
|
|
6822
6848
|
return refuse(response, who, 403, "Sign in as an approver to set kits up.", "/kits");
|
|
6823
6849
|
const kit = kitOf(kitPost[1]);
|
|
6824
|
-
const projects =
|
|
6850
|
+
const projects = consoleProjects();
|
|
6825
6851
|
const repo = body.get("repo") ?? "";
|
|
6826
6852
|
if (kit === null || !projects.includes(repo) || !store.accountCanAccess(who.name, repo))
|
|
6827
6853
|
return redirect(response, `/kits?problem=${encodeURIComponent("Choose one of your projects.")}`);
|
|
@@ -6866,7 +6892,7 @@ export function createDecisionServer(options) {
|
|
|
6866
6892
|
if (who.role !== "approver")
|
|
6867
6893
|
return refuse(response, who, 403, "Sign in as an approver to look after teammates.", "/teammates");
|
|
6868
6894
|
if (teammatePost[2] === "new") {
|
|
6869
|
-
const projects =
|
|
6895
|
+
const projects = consoleProjects();
|
|
6870
6896
|
const repo = body.get("repo") ?? "";
|
|
6871
6897
|
if (!projects.includes(repo))
|
|
6872
6898
|
return redirect(response, `/teammates?problem=${encodeURIComponent("Choose one of your projects.")}`);
|
|
@@ -6938,7 +6964,7 @@ export function createDecisionServer(options) {
|
|
|
6938
6964
|
const galleryPost = /^\/flows\/new\/([a-z-]{1,40})$/.exec(url.pathname);
|
|
6939
6965
|
if (galleryPost !== null) {
|
|
6940
6966
|
const template = galleryTemplateOf(galleryPost[1]);
|
|
6941
|
-
const projects =
|
|
6967
|
+
const projects = consoleProjects();
|
|
6942
6968
|
if (template === null)
|
|
6943
6969
|
return refuse(response, who, 404, "There's no template by that name.", "/flows/new");
|
|
6944
6970
|
if (who.via !== "cookie" || who.role !== "approver" || store.isDemo())
|
|
@@ -6970,7 +6996,7 @@ export function createDecisionServer(options) {
|
|
|
6970
6996
|
if (url.pathname === "/flows/new" || url.pathname === "/flows/example" || url.pathname === "/flows/import" || flowPost !== null || triggerPost !== null) {
|
|
6971
6997
|
const now = clock();
|
|
6972
6998
|
const answer = (status, payload) => respond(response, status, "application/json; charset=utf-8", JSON.stringify(payload));
|
|
6973
|
-
const projects =
|
|
6999
|
+
const projects = consoleProjects();
|
|
6974
7000
|
if (who.via !== "cookie" || who.role !== "approver")
|
|
6975
7001
|
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
7002
|
if (url.pathname === "/flows/example") {
|
|
@@ -7244,7 +7270,7 @@ export function createDecisionServer(options) {
|
|
|
7244
7270
|
return back("problem", "Choose what the budget is for.");
|
|
7245
7271
|
const scope = target[1], key = target[2];
|
|
7246
7272
|
const known = scope === "installation" ? key === "*"
|
|
7247
|
-
: scope === "project" ?
|
|
7273
|
+
: scope === "project" ? consoleProjects().includes(key)
|
|
7248
7274
|
: scope === "person" ? store.accountFacts().some(one => one.name === key && one.revokedAt === null)
|
|
7249
7275
|
: store.teammates([...new Set([...managedRepos(), ...store.knownRepos()])]).some(one => String(one.id) === key);
|
|
7250
7276
|
if (!known)
|
|
@@ -7266,7 +7292,7 @@ export function createDecisionServer(options) {
|
|
|
7266
7292
|
// Settings → Project → Builds at once: an approver for the project sets it; the ledger keeps before → after.
|
|
7267
7293
|
if (url.pathname === "/settings/project/concurrency") {
|
|
7268
7294
|
const repo = body.get("repo") ?? "";
|
|
7269
|
-
const known =
|
|
7295
|
+
const known = consoleProjects();
|
|
7270
7296
|
if (!known.includes(repo))
|
|
7271
7297
|
return refuse(response, who, 403, "That project is outside your access.", "/settings/project");
|
|
7272
7298
|
if (who.via !== "cookie" || who.role !== "approver" || !store.accountCanAccess(who.name, repo))
|
|
@@ -7287,7 +7313,7 @@ export function createDecisionServer(options) {
|
|
|
7287
7313
|
if (who.via !== "cookie" || !store.isInstanceOperator(who.name))
|
|
7288
7314
|
return refuse(response, who, 403, "An instance operator deletes projects.", "/settings/project");
|
|
7289
7315
|
const repo = body.get("repo") ?? "";
|
|
7290
|
-
const known =
|
|
7316
|
+
const known = consoleProjects();
|
|
7291
7317
|
if (!known.includes(repo))
|
|
7292
7318
|
return refuse(response, who, 403, "That project is outside your access.", "/settings/project");
|
|
7293
7319
|
const back = (key, words, to = repo) => redirect(response, `/settings/project?${to === "" ? "" : `repo=${encodeURIComponent(to)}&`}${key}=${encodeURIComponent(words)}`);
|
|
@@ -7343,7 +7369,7 @@ export function createDecisionServer(options) {
|
|
|
7343
7369
|
if (who.via !== "cookie" || !store.isInstanceOperator(who.name))
|
|
7344
7370
|
return refuse(response, who, 403, "An instance operator sets approval rules.", "/settings/approval");
|
|
7345
7371
|
const repo = body.get("repo") ?? "";
|
|
7346
|
-
const known =
|
|
7372
|
+
const known = consoleProjects();
|
|
7347
7373
|
if (!known.includes(repo))
|
|
7348
7374
|
return refuse(response, who, 403, "That project is outside your access.", "/settings/approval");
|
|
7349
7375
|
const back = (key, words) => redirect(response, `/settings/approval?repo=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}`);
|
|
@@ -7744,7 +7770,7 @@ export function createDecisionServer(options) {
|
|
|
7744
7770
|
if (who.via !== "cookie" || who.role !== "approver")
|
|
7745
7771
|
return refuse(response, who, 403, "Sign in as an approver to connect tools.", "/settings/tools");
|
|
7746
7772
|
const repo = body.get("repo") ?? "";
|
|
7747
|
-
const reachable = (one) => visible(one) &&
|
|
7773
|
+
const reachable = (one) => visible(one) && consoleProjects().includes(one);
|
|
7748
7774
|
if (!reachable(repo))
|
|
7749
7775
|
return refuse(response, who, 403, "That project is outside your access.", "/projects");
|
|
7750
7776
|
// "Also connect to …": a service connected on the shown project, signed in again for another project.
|
|
@@ -7817,13 +7843,13 @@ export function createDecisionServer(options) {
|
|
|
7817
7843
|
if (who.via !== "cookie" || who.role !== "approver")
|
|
7818
7844
|
return refuse(response, who, 403, "Sign in as an approver to manage tools.", "/settings/tools");
|
|
7819
7845
|
const repo = body.get("repo") ?? "";
|
|
7820
|
-
if (!visible(repo) || !
|
|
7846
|
+
if (!visible(repo) || !consoleProjects().includes(repo))
|
|
7821
7847
|
return refuse(response, who, 403, "That project is outside your access.", "/projects");
|
|
7822
7848
|
const back = (key, words, anchor = "") => redirect(response, `/settings/tools?repo=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}${anchor}`);
|
|
7823
7849
|
// As for Connect: a post from a page that showed another project is refused, never applied here.
|
|
7824
7850
|
const shown = body.get("shown") ?? "";
|
|
7825
7851
|
if (shown !== repo) {
|
|
7826
|
-
const page = visible(shown) &&
|
|
7852
|
+
const page = visible(shown) && consoleProjects().includes(shown) ? shown : repo;
|
|
7827
7853
|
return redirect(response, `/settings/tools?repo=${encodeURIComponent(page)}&problem=${encodeURIComponent("The project changed; try again.")}`);
|
|
7828
7854
|
}
|
|
7829
7855
|
const action = body.get("action") ?? "";
|
|
@@ -7986,7 +8012,7 @@ export function createDecisionServer(options) {
|
|
|
7986
8012
|
if (who.via !== 'cookie' || who.role !== 'approver')
|
|
7987
8013
|
return refuse(response, who, 403, 'Sign in as an approver to refresh project context.', '/settings/knowledge');
|
|
7988
8014
|
const repo = body.get('repo') ?? '';
|
|
7989
|
-
if (!visible(repo) || !store.accountCanAccess(who.name, repo) || !
|
|
8015
|
+
if (!visible(repo) || !store.accountCanAccess(who.name, repo) || !consoleProjects().includes(repo))
|
|
7990
8016
|
return refuse(response, who, 403, 'That project is outside your access.', '/settings/knowledge');
|
|
7991
8017
|
const refreshed = await repositoryContext({ repo, query: '', refresh: true, cacheRoot: join(dirname(evidenceRoot), 'repository-context') });
|
|
7992
8018
|
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 +8021,7 @@ export function createDecisionServer(options) {
|
|
|
7995
8021
|
if (who.via !== 'cookie' || who.role !== 'approver')
|
|
7996
8022
|
return refuse(response, who, 403, 'Sign in as an approver to decide memory proposals.', '/settings/knowledge');
|
|
7997
8023
|
const repo = body.get('repo') ?? '', decision = body.get('decision') ?? '';
|
|
7998
|
-
if (!visible(repo) || !
|
|
8024
|
+
if (!visible(repo) || !consoleProjects().includes(repo))
|
|
7999
8025
|
return refuse(response, who, 403, 'That project is outside your access.', '/settings/knowledge');
|
|
8000
8026
|
if (!['accept', 'reject'].includes(decision) || !/^[0-9]{1,12}$/.test(body.get('proposal') ?? ''))
|
|
8001
8027
|
return refuse(response, who, 400, 'Choose accept or reject for one proposal.', '/settings/knowledge');
|
|
@@ -8011,7 +8037,7 @@ export function createDecisionServer(options) {
|
|
|
8011
8037
|
if (who.via !== 'cookie' || who.role !== 'approver')
|
|
8012
8038
|
return refuse(response, who, 403, 'Sign in as an approver to change project decisions.', '/settings/knowledge');
|
|
8013
8039
|
const repo = body.get('repo') ?? '', action = body.get('action') ?? '';
|
|
8014
|
-
if (!visible(repo) || !
|
|
8040
|
+
if (!visible(repo) || !consoleProjects().includes(repo))
|
|
8015
8041
|
return refuse(response, who, 403, 'That project is outside your access.', '/settings/knowledge');
|
|
8016
8042
|
if (['repo', 'action', 'claim', 'why', 'decision', 'reason'].some(k => body.getAll(k).length > 1) || !['record', 'retire'].includes(action))
|
|
8017
8043
|
return refuse(response, who, 400, 'Choose record or retire.', '/settings/knowledge');
|
|
@@ -8040,11 +8066,14 @@ export function createDecisionServer(options) {
|
|
|
8040
8066
|
const repo = body.get('repo') ?? '', action = body.get('action') ?? '';
|
|
8041
8067
|
if (!visible(repo) || ![...(admissionList() ?? []), ...managedRepos(), ...store.knownRepos()].includes(repo))
|
|
8042
8068
|
return refuse(response, who, 403, 'That project is outside your access.', '/settings/knowledge');
|
|
8043
|
-
if (['repo', 'action', 'identity', 'revision', 'instructions', 'title', 'content', 'path', 'id', 'restore'].some(k => body.getAll(k).length > 1) || !['instructions', 'save', 'remove', 'restore'].includes(action) || !/^[0-9]+$/.test(body.get('revision') ?? ''))
|
|
8069
|
+
if (['repo', 'action', 'identity', 'revision', 'instructions', 'title', 'content', 'path', 'id', 'restore'].some(k => body.getAll(k).length > 1) || !['instructions', 'save', 'remove', 'restore', 'apply'].includes(action) || !/^[0-9]+$/.test(body.get('revision') ?? ''))
|
|
8044
8070
|
return refuse(response, who, 400, 'Invalid knowledge form.', '/settings/knowledge');
|
|
8045
8071
|
const draft = Object.fromEntries(['instructions', 'title', 'content', 'path', 'id'].filter(k => body.has(k)).map(k => [k, body.get(k)]));
|
|
8046
8072
|
try {
|
|
8047
|
-
|
|
8073
|
+
if (action === 'apply')
|
|
8074
|
+
applySavedKnowledge(store, repo, who.name, clock());
|
|
8075
|
+
else
|
|
8076
|
+
changeKnowledge(store, { repo, actor: who.name, identity: body.get('identity') ?? '', revision: Number(body.get('revision')), action: action, draft, restore: Number(body.get('restore')) }, clock());
|
|
8048
8077
|
}
|
|
8049
8078
|
catch (error) {
|
|
8050
8079
|
let content = `<p role="alert">Project knowledge is unavailable. Your unsaved draft is below.</p><pre class="knowledge">${escape(JSON.stringify(draft, null, 2))}</pre>`;
|
|
@@ -8060,7 +8089,7 @@ export function createDecisionServer(options) {
|
|
|
8060
8089
|
if (who.via !== "cookie")
|
|
8061
8090
|
return refuse(response, who, 403, "Sign in to change learning.", "/settings/learning");
|
|
8062
8091
|
const repo = body.get("repo") ?? "", action = body.get("action") ?? "";
|
|
8063
|
-
if (!visible(repo) || !
|
|
8092
|
+
if (!visible(repo) || !consoleProjects().includes(repo))
|
|
8064
8093
|
return refuse(response, who, 403, "That project is outside your access.", "/settings/learning");
|
|
8065
8094
|
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") ?? ""))
|
|
8066
8095
|
return refuse(response, who, 400, "Invalid learning form.", "/settings/learning");
|
|
@@ -8502,6 +8531,25 @@ export function createDecisionServer(options) {
|
|
|
8502
8531
|
sessions.persist(who.session);
|
|
8503
8532
|
return redirect(response, safeReturn(body.get("return")));
|
|
8504
8533
|
}
|
|
8534
|
+
if (url.pathname === "/projects/remove") {
|
|
8535
|
+
// The reversible remove: off every list and the builder; tasks, results and settings stay. Deleting is
|
|
8536
|
+
// Settings → Project → Delete project. The same admission code as `repos remove` (project-admission.ts).
|
|
8537
|
+
if (who.via !== "cookie" || !store.isInstanceOperator(who.name))
|
|
8538
|
+
return refuse(response, who, 403, "An instance operator removes projects.", "/settings/project");
|
|
8539
|
+
const repo = body.get("repo") ?? "";
|
|
8540
|
+
if (!consoleProjects().includes(repo))
|
|
8541
|
+
return refuse(response, who, 403, "That project is outside your access.", "/settings/project");
|
|
8542
|
+
const released = await releaseProject(store, { registryFile: options.registryPath ?? null, path: repo, actor: { label: who.name, origin: "console" }, now });
|
|
8543
|
+
if (!released.ok)
|
|
8544
|
+
return redirect(response, `/settings/project?repo=${encodeURIComponent(repo)}&problem=${encodeURIComponent(`${projectName(repo)} wasn't removed: ${released.message}`)}`);
|
|
8545
|
+
deletedRepos.add(repo);
|
|
8546
|
+
if (who.session.project === repo) {
|
|
8547
|
+
who.session.project = null;
|
|
8548
|
+
who.session.projectRevision += 1;
|
|
8549
|
+
sessions.persist(who.session);
|
|
8550
|
+
}
|
|
8551
|
+
return redirect(response, `/settings/project?said=${encodeURIComponent(`Removed ${projectName(repo)}. Its tasks and results stay saved.`)}`);
|
|
8552
|
+
}
|
|
8505
8553
|
if (url.pathname === "/projects/open") {
|
|
8506
8554
|
// Sessions only: a bearer caller names its project per request and has
|
|
8507
8555
|
// no session to mutate — refusing here keeps that boundary legible.
|
|
@@ -8523,18 +8571,19 @@ export function createDecisionServer(options) {
|
|
|
8523
8571
|
if (!(await isGitRepo(canonical))) {
|
|
8524
8572
|
return void projectsScreen(response, who, "that path is not a git repository", 400, safeReturn(body.get("return")));
|
|
8525
8573
|
}
|
|
8526
|
-
//
|
|
8527
|
-
|
|
8528
|
-
|
|
8529
|
-
if (options.registryPath !== undefined) {
|
|
8530
|
-
const enrolled = await updateRepos(options.registryPath, repos => addRepos(repos, [canonical]));
|
|
8531
|
-
deletedRepos.delete(canonical);
|
|
8532
|
-
if (!enrolled.ok) {
|
|
8533
|
-
return void projectsScreen(response, who, `that project is valid, but it could not be added — ${enrolled.message}`, 400, safeReturn(body.get("return")));
|
|
8534
|
-
}
|
|
8574
|
+
// An account limited to listed projects never widens its own list.
|
|
8575
|
+
if (!visible(canonical)) {
|
|
8576
|
+
return void projectsScreen(response, who, "That project is outside your access.", 403, safeReturn(body.get("return")));
|
|
8535
8577
|
}
|
|
8536
|
-
|
|
8537
|
-
|
|
8578
|
+
// Opening an allowed repository admits it the same way `repos add` does
|
|
8579
|
+
// (project-admission.ts). A co-located `up` notices the durable list and
|
|
8580
|
+
// connects its builder; there is no separate restart or binding step.
|
|
8581
|
+
const admitted = await admitProject(store, { registryFile: options.registryPath ?? null, path: canonical, actor: { label: who.name, origin: "console" }, now });
|
|
8582
|
+
if (!admitted.ok) {
|
|
8583
|
+
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")));
|
|
8584
|
+
}
|
|
8585
|
+
deletedRepos.delete(admitted.repo);
|
|
8586
|
+
who.session.project = admitted.repo;
|
|
8538
8587
|
who.session.projectRevision++;
|
|
8539
8588
|
// The switcher (board pass) opens a project from any screen and
|
|
8540
8589
|
// returns there — a same-site path only, never an off-site road.
|
|
@@ -9267,14 +9316,11 @@ export function createDecisionServer(options) {
|
|
|
9267
9316
|
if (admitted === null) {
|
|
9268
9317
|
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);
|
|
9269
9318
|
}
|
|
9270
|
-
|
|
9271
|
-
|
|
9272
|
-
|
|
9273
|
-
if (!enrolled.ok) {
|
|
9274
|
-
return projectsScreen(response, who, `${cloned.target} is cloned but not enrolled — ${enrolled.message}`, 400);
|
|
9275
|
-
}
|
|
9319
|
+
const joined = await admitProject(store, { registryFile: options.registryPath ?? null, path: admitted, actor: { label: who.name, origin: "console" }, now });
|
|
9320
|
+
if (!joined.ok) {
|
|
9321
|
+
return projectsScreen(response, who, `${cloned.target} is cloned but not added — ${joined.message}`, 400);
|
|
9276
9322
|
}
|
|
9277
|
-
|
|
9323
|
+
deletedRepos.delete(admitted);
|
|
9278
9324
|
who.session.project = admitted;
|
|
9279
9325
|
who.session.projectRevision += 1;
|
|
9280
9326
|
return projectsScreen(response, who, options.upConsole === true
|
|
@@ -10850,8 +10896,8 @@ export function createDecisionServer(options) {
|
|
|
10850
10896
|
if (document === current.document.trim())
|
|
10851
10897
|
return redirect(response, taskHref(taskId));
|
|
10852
10898
|
const content = Buffer.from(`${document}\n`, "utf8");
|
|
10853
|
-
if (content.length >
|
|
10854
|
-
return taskScreen(response, who, taskId, `plan not saved: it is over ${
|
|
10899
|
+
if (content.length > TEXT_LIMITS.planDocumentBytes) {
|
|
10900
|
+
return taskScreen(response, who, taskId, `plan not saved: it is over ${TEXT_LIMITS.planDocumentBytes} bytes`, 400);
|
|
10855
10901
|
}
|
|
10856
10902
|
try {
|
|
10857
10903
|
const name = `plan-edit-${randomUUID().replace(/-/g, "")}.md`;
|
package/dist/store.d.ts
CHANGED
|
@@ -6980,6 +6980,8 @@ export declare class Store {
|
|
|
6980
6980
|
/** Remember a project was opened. Upsert keeps added_at; recency always moves.
|
|
6981
6981
|
* A project added for the first time starts on Quick checks. */
|
|
6982
6982
|
upsertProject(path: string, name: string, now: Date): void;
|
|
6983
|
+
/** A project removed from the lists (project-admission.ts): only its row goes; its tasks, results and settings stay. */
|
|
6984
|
+
forgetProject(path: string): void;
|
|
6983
6985
|
/** Most recently opened first — the opener page's order. */
|
|
6984
6986
|
listProjects(): {
|
|
6985
6987
|
path: string;
|
package/dist/store.js
CHANGED
|
@@ -20463,6 +20463,10 @@ export class Store {
|
|
|
20463
20463
|
stampNewProjectLevel(this, path, now);
|
|
20464
20464
|
});
|
|
20465
20465
|
}
|
|
20466
|
+
/** A project removed from the lists (project-admission.ts): only its row goes; its tasks, results and settings stay. */
|
|
20467
|
+
forgetProject(path) {
|
|
20468
|
+
this.db.prepare("DELETE FROM project WHERE path = ?").run(path);
|
|
20469
|
+
}
|
|
20466
20470
|
/** Most recently opened first — the opener page's order. */
|
|
20467
20471
|
listProjects() {
|
|
20468
20472
|
return this.db
|