toolroll 0.9.102 → 0.9.104
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 +13 -0
- package/README.md +10 -8
- package/dist/assignment.d.ts +32 -0
- package/dist/assignment.js +140 -4
- package/dist/browser/workspace.js +25 -25
- package/dist/browser-workspace.d.ts +21 -5
- package/dist/contracts/console-api.d.ts +3 -2
- package/dist/contracts/console-api.js +4 -2
- package/dist/desktop-update.d.ts +2 -2
- package/dist/flow-engine.d.ts +3 -0
- package/dist/flow-engine.js +4 -1
- package/dist/flows-ui.d.ts +31 -1
- package/dist/flows-ui.js +40 -12
- package/dist/keymap.d.ts +3 -1
- package/dist/keymap.js +3 -0
- package/dist/local-owner-acts.d.ts +83 -0
- package/dist/local-owner-acts.js +221 -0
- package/dist/local-owner.d.ts +12 -1
- package/dist/local-owner.js +26 -0
- package/dist/operate-remote-arguments.js +4 -2
- package/dist/operate.js +76 -1
- package/dist/plan-auto.js +5 -0
- package/dist/project-knowledge.d.ts +6 -6
- package/dist/public-access.d.ts +9 -0
- package/dist/public-access.js +15 -0
- package/dist/pull-request-flow.js +9 -0
- package/dist/release-approval.d.ts +9 -20
- package/dist/release-approval.js +14 -53
- package/dist/release-card.d.ts +15 -1
- package/dist/release-card.js +35 -12
- package/dist/release-refusal.d.ts +32 -0
- package/dist/release-refusal.js +73 -0
- package/dist/result-completion.d.ts +6 -2
- package/dist/result-completion.js +13 -7
- package/dist/scope.d.ts +32 -1
- package/dist/scope.js +116 -25
- package/dist/serve.js +13 -14
- package/dist/server/guards.d.ts +1 -0
- package/dist/server/guards.js +11 -1
- package/dist/server/home-asks.d.ts +45 -0
- package/dist/server/home-asks.js +108 -0
- package/dist/server/home.d.ts +15 -5
- package/dist/server/home.js +53 -18
- package/dist/server/pages.js +15 -3
- package/dist/server/remote.d.ts +1 -0
- package/dist/server/render-results.d.ts +4 -0
- package/dist/server/render-tasks.d.ts +9 -0
- package/dist/server/render-tasks.js +4 -4
- package/dist/server/route-table.js +4 -0
- package/dist/server/runtime.d.ts +1 -0
- package/dist/server/task-views.js +3 -0
- package/dist/server/tasks.d.ts +25 -0
- package/dist/server/tasks.js +46 -6
- package/dist/surface.js +12 -2
- package/dist/task-outcome-cli.d.ts +18 -0
- package/dist/task-outcome-cli.js +60 -2
- package/dist/tui-contract.d.ts +61 -3
- package/dist/tui-source.js +91 -20
- package/dist/tui.js +54 -54
- package/dist/work-index.d.ts +10 -5
- package/dist/work-index.js +23 -13
- package/package.json +2 -2
package/dist/serve.js
CHANGED
|
@@ -20,8 +20,6 @@ import { html, htmlString, jsonScript, textHtml } from "./html.js";
|
|
|
20
20
|
import { browserAssetsAvailable, browserWorkspaceDocument } from './browser-shell.js';
|
|
21
21
|
import { accessKey, countsKey, indexKey, projectSummaryOf, RevisionCache, summaryKey } from './console-chrome.js';
|
|
22
22
|
import { usageChipOf } from './limits-ui.js';
|
|
23
|
-
/** Any header a proxy adds: a request carrying one is never "from this computer" (password freshness, one-time links). */
|
|
24
|
-
const PROXY_HEADER = /^(forwarded|via|x-forwarded-.+|x-real-ip|x-client-ip|x-original-.+|x-cluster-client-ip|true-client-ip|cf-connecting-ip|fastly-client-ip|x-proxyuser-ip)$/i;
|
|
25
23
|
/** How often the background checks look at what is due (docs/plans/get-side-effects.md), and how often this
|
|
26
24
|
* computer's agent sign-ins are asked again. */
|
|
27
25
|
const BACKGROUND_CHECKS_MS = 30_000;
|
|
@@ -137,7 +135,7 @@ import { canonicalProject, projectName, resolveCeiling, rowVisible } from "./pro
|
|
|
137
135
|
import { noteSignInProbe, signInNotices } from "./provider-auth.js";
|
|
138
136
|
import { createConnectionChecker } from "./provider-connection.js";
|
|
139
137
|
import { ALL_CREDENTIAL_ENV } from "./provider.js";
|
|
140
|
-
import { hstsFor, LOOPBACK_PEERS, plainHttpRefusal, transportOf } from "./public-access.js";
|
|
138
|
+
import { hstsFor, LOOPBACK_PEERS, plainHttpRefusal, requestFromThisComputer, transportOf } from "./public-access.js";
|
|
141
139
|
import { attachReadExecutor, defaultReadWorkers, ReadExecutor } from "./read-executor.js";
|
|
142
140
|
import { cachedRelease, isNewer, runnerVersions, updateChecksOff } from "./releases.js";
|
|
143
141
|
import { limitWords, FLUSH_MS as REQUEST_BUDGET_FLUSH_MS, RequestBudget, SOURCE_BUDGET_DEFAULTS, SourceAdmission } from "./request-budget.js";
|
|
@@ -158,7 +156,7 @@ import { createPeopleHandlers } from './server/people-tokens.js';
|
|
|
158
156
|
import { createSettingsHandlers } from './server/settings.js';
|
|
159
157
|
import { readPurpose, requestContext } from "./server/request-context.js";
|
|
160
158
|
import { inPlaceRoute, prefetchRoute } from "./server/in-place.js";
|
|
161
|
-
import { asLocalOwner, localModeOn, localOwnerRefusal, localOwnerStandsNow } from "./local-owner.js";
|
|
159
|
+
import { LOCAL_OWNER_ELSEWHERE, asLocalOwner, localModeOn, localOwnerRefusal, localOwnerStandsNow } from "./local-owner.js";
|
|
162
160
|
import { defaultAgentOf, fillDefaultRouting } from "./model-choices.js";
|
|
163
161
|
import { chromeScript, DEMO_BANNER, DEMO_BANNER_SHORT, focusDocument, KBD_HELP, pinnedTheme, QUEUE_VIEW, refuse, screen, SENSITIVE_INPUT, shell, sidebarScript } from "./server/chrome.js";
|
|
164
162
|
import { BODY_CAP, form, loginHref, passwordSetNext, refusalNext, matchTaskPath, page, projectChatHref, redactedPath, redirect, respond, safeReturn, SHUTDOWN_WAIT_MS, taskChatHref } from "./server/http.js";
|
|
@@ -379,16 +377,9 @@ export function createDecisionServer(options) {
|
|
|
379
377
|
/** v147: this console start's key epoch (easy-start C2): the browser-key wave binds each session's mutation key to it. */
|
|
380
378
|
const keyEpoch = options.keyEpoch ?? randomBytes(16).toString("hex");
|
|
381
379
|
/** This computer, and nothing in front of it: a loopback peer that names a loopback address, with no sign of a proxy —
|
|
382
|
-
* any forwarding header at all (Forwarded, X-Forwarded-*, X-Real-IP, Via, …) disqualifies the request.
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
if (!LOOPBACK_PEERS.has(peer))
|
|
386
|
-
return false;
|
|
387
|
-
for (const name of Object.keys(request.headers))
|
|
388
|
-
if (PROXY_HEADER.test(name))
|
|
389
|
-
return false;
|
|
390
|
-
return /^(localhost|127\.0\.0\.1|\[::1\]):[0-9]{1,5}$/.test(request.headers.host ?? "");
|
|
391
|
-
};
|
|
380
|
+
* any forwarding header at all (Forwarded, X-Forwarded-*, X-Real-IP, Via, …) disqualifies the request. The one reading
|
|
381
|
+
* (public-access.ts), which guards.ts also applies to every request a local owner's session makes. */
|
|
382
|
+
const fromThisComputer = (request) => requestFromThisComputer(request);
|
|
392
383
|
/**
|
|
393
384
|
* Cockpit B2: a request's password-freshness facts — the one place they are made (scope.ts honours them). Every
|
|
394
385
|
* reader looks at the session as it is at the moment of the act, never a snapshot taken before the body arrived:
|
|
@@ -911,6 +902,14 @@ export function createDecisionServer(options) {
|
|
|
911
902
|
const refusedToken = {};
|
|
912
903
|
const who = identify(request, touch, edgeRoute?.domain === 'live', refusedToken);
|
|
913
904
|
timeline.mark("signed in");
|
|
905
|
+
// Local-owner slice 0: a local owner's session presented from anywhere but this computer (another address, a
|
|
906
|
+
// proxy, a tailnet or LAN name) signs in no one and changes nothing. Sign-in pages still answer (a password signs
|
|
907
|
+
// in there); everything else is refused in plain words.
|
|
908
|
+
if (refusedToken.elsewhere === true && edgeRoute?.domain !== 'people') {
|
|
909
|
+
request.resume();
|
|
910
|
+
response.setHeader("x-toolroll-refused", "local-owner-elsewhere");
|
|
911
|
+
return respond(response, 403, "text/plain; charset=utf-8", LOCAL_OWNER_ELSEWHERE);
|
|
912
|
+
}
|
|
914
913
|
if (refusedToken.principal !== undefined) {
|
|
915
914
|
if (refuseBudget(requestBudget.admit(refusedToken.principal.tokenId, "api")))
|
|
916
915
|
return;
|
package/dist/server/guards.d.ts
CHANGED
|
@@ -58,6 +58,7 @@ export declare function createSharedGuards(runtime: GuardsRuntime): {
|
|
|
58
58
|
projectRequestAllowed: (route: RouteDeclaration, source: RouteDeclaration['project'], url: URL, who: Who, request: IncomingMessage, response: ServerResponse, body: URLSearchParams | null) => boolean;
|
|
59
59
|
identify: (request: IncomingMessage, touch?: boolean, limited?: boolean, refusedToken?: {
|
|
60
60
|
principal?: Principal;
|
|
61
|
+
elsewhere?: boolean;
|
|
61
62
|
}) => Who | null;
|
|
62
63
|
lookupSession: (candidate: string, touch?: boolean) => Session | null;
|
|
63
64
|
restricted: () => boolean;
|
package/dist/server/guards.js
CHANGED
|
@@ -15,6 +15,7 @@ import { requestContext } from "./request-context.js";
|
|
|
15
15
|
import { refuse } from "./chrome.js";
|
|
16
16
|
import { matchTaskPath } from "./http.js";
|
|
17
17
|
import { PersistentSessions, SESSION_COOKIE, sessionWithinLifetime } from "./session.js";
|
|
18
|
+
import { requestFromThisComputer } from "../public-access.js";
|
|
18
19
|
export function createSharedGuards(runtime) {
|
|
19
20
|
const syncIdentities = syncProjectIdentities();
|
|
20
21
|
// v99: wrong passwords. One address guessing across names runs out of tries here; each name
|
|
@@ -333,7 +334,16 @@ export function createSharedGuards(runtime) {
|
|
|
333
334
|
const match = new RegExp(`(?:^|;\\s*)${SESSION_COOKIE}=([0-9a-f]{64})`).exec(cookies);
|
|
334
335
|
if (match === null)
|
|
335
336
|
return null;
|
|
336
|
-
|
|
337
|
+
// Local-owner slice 0: a local owner's session works only from this computer — a loopback peer naming a loopback
|
|
338
|
+
// Host, with no forwarding header — on every request, not only when it was made. From anywhere else it signs in
|
|
339
|
+
// no one, and is not touched (refusedToken.elsewhere says why, for the refusal).
|
|
340
|
+
const found = lookupSession(match[1], false);
|
|
341
|
+
if (found !== null && found.authority !== "full" && !requestFromThisComputer(request)) {
|
|
342
|
+
if (refusedToken !== undefined)
|
|
343
|
+
refusedToken.elsewhere = true;
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
const session = touch && found !== null ? lookupSession(match[1], true) : found;
|
|
337
347
|
return session === null ? null : { name: session.name, via: "cookie", session, role: session.role, authority: session.authority };
|
|
338
348
|
}
|
|
339
349
|
/**
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { FirstRunStep } from "../first-run.js";
|
|
2
|
+
import type { Store } from "../store.js";
|
|
3
|
+
import type { WorkSummaryAccess } from "../work-summary.js";
|
|
4
|
+
/** One row Home lists that isn't a task's own row. `id` is namespaced so it never meets a task's id; `at` (when it
|
|
5
|
+
* started waiting) gives its age, or null for a setup step; `repo` its project. */
|
|
6
|
+
export type HomeAskRow = {
|
|
7
|
+
id: string;
|
|
8
|
+
title: string;
|
|
9
|
+
href: string;
|
|
10
|
+
repo: string | null;
|
|
11
|
+
at: string | null;
|
|
12
|
+
line: string | null;
|
|
13
|
+
action: {
|
|
14
|
+
label: string;
|
|
15
|
+
href: string;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
export type HomeAsks = {
|
|
19
|
+
/** A release's line for an approver (its version, or why the sheet can't take a yes now); null when it isn't one. */
|
|
20
|
+
release: (taskId: string) => string | null;
|
|
21
|
+
/** Releases whose publication waits for a person, one per family, only where that family isn't already a row. */
|
|
22
|
+
held: HomeAskRow[];
|
|
23
|
+
/** Flow cards waiting on this person. */
|
|
24
|
+
flows: HomeAskRow[];
|
|
25
|
+
/** First-run steps still to do, keyed by step. */
|
|
26
|
+
setup: (HomeAskRow & {
|
|
27
|
+
key: FirstRunStep["key"];
|
|
28
|
+
})[];
|
|
29
|
+
};
|
|
30
|
+
export declare const NO_HOME_ASKS: HomeAsks;
|
|
31
|
+
/** The release sheet: where a release is read and approved, never Home. */
|
|
32
|
+
export declare const releaseSheetHref: (taskId: string) => string;
|
|
33
|
+
/** A release's line (its version, or the sheet's refusal) and every held publication in view. Approvers only: the sheet
|
|
34
|
+
* mints its approval only for them; anyone else reads a release as an ordinary task row. */
|
|
35
|
+
export declare function releaseAsks(store: Store, now: Date, access: WorkSummaryAccess, project: string | null, approver: boolean): Pick<HomeAsks, "release" | "held">;
|
|
36
|
+
/** Every active card, in a flow of a project this person may see, that waits on them (flows-ui.ts flowCardWaitsOn, the
|
|
37
|
+
* flow page's own rule). Newest wait first. */
|
|
38
|
+
export declare function flowAsks(store: Store, repos: readonly string[], viewer: {
|
|
39
|
+
name: string;
|
|
40
|
+
approver: boolean;
|
|
41
|
+
}, project: string | null): HomeAskRow[];
|
|
42
|
+
/** The first-run steps (first-run.ts, with the builder step) still to do, for an approver. A step that is done, or only
|
|
43
|
+
* being checked (no answer yet, so no action), is not a row. Null steps (a restricted console, the demo, after the first
|
|
44
|
+
* success) are none. */
|
|
45
|
+
export declare function setupAsks(steps: readonly FirstRunStep[] | null, approver: boolean): HomeAsks["setup"];
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What else waits on a person at Home (docs/plans/home-other-asks.md): a release's line and a held publication, a flow
|
|
3
|
+
* card waiting on them, and the first-run steps left to do. Reads only, each through the same admission as the page it
|
|
4
|
+
* links to; nothing here acts: every row's one link opens its own page, whose own checks decide what this person may do
|
|
5
|
+
* there. `homeView` (home.ts) assembles these with the work index's rows.
|
|
6
|
+
*/
|
|
7
|
+
import { flowCardHref, flowDefinitionOf } from "../flow-engine.js";
|
|
8
|
+
import { browserViewOf } from "../flow-browser.js";
|
|
9
|
+
import { flowCardWaitsOn } from "../flows-ui.js";
|
|
10
|
+
import { releaseAskOf, releaseTaskOf } from "../release-approval.js";
|
|
11
|
+
import { autoPublishHeld, PUBLISH_ALLOWED_ACTION, PUBLISH_HELD_ACTION } from "../release-auto.js";
|
|
12
|
+
import { taskArchived } from "../task-archive.js";
|
|
13
|
+
import { workStateRows } from "../work-index.js";
|
|
14
|
+
export const NO_HOME_ASKS = { release: () => null, held: [], flows: [], setup: [] };
|
|
15
|
+
/** The release sheet: where a release is read and approved, never Home. */
|
|
16
|
+
export const releaseSheetHref = (taskId) => `/t/${encodeURIComponent(taskId)}/release`;
|
|
17
|
+
const REVIEW_RELEASE = "Review the release";
|
|
18
|
+
/** A release's line (its version, or the sheet's refusal) and every held publication in view. Approvers only: the sheet
|
|
19
|
+
* mints its approval only for them; anyone else reads a release as an ordinary task row. */
|
|
20
|
+
export function releaseAsks(store, now, access, project, approver) {
|
|
21
|
+
if (!approver)
|
|
22
|
+
return { release: () => null, held: [] };
|
|
23
|
+
const release = (taskId) => {
|
|
24
|
+
const ask = releaseAskOf(store, taskId);
|
|
25
|
+
return ask === null ? null : ask.refusal ?? `Version ${ask.version} waits for your approval.`;
|
|
26
|
+
};
|
|
27
|
+
const shown = (repo) => (access.repos === null || access.repos.includes(repo)) && (project === null || repo === project);
|
|
28
|
+
// Only what is still held: the newest held-or-allowed act per task is the held one (release-auto.ts autoPublishHeld's
|
|
29
|
+
// own rule, read in one statement). Each is then a release this person may see in this scope, before anything heavier.
|
|
30
|
+
const marked = store.handle.prepare(`SELECT a.task_id, a.at, a.detail FROM action_ledger a WHERE a.action = $held AND a.task_id IS NOT NULL
|
|
31
|
+
AND a.id = (SELECT MAX(b.id) FROM action_ledger b WHERE b.task_id = a.task_id AND b.action IN ($held, $allowed)) ORDER BY a.at DESC, a.id DESC`)
|
|
32
|
+
.all({ $held: PUBLISH_HELD_ACTION, $allowed: PUBLISH_ALLOWED_ACTION });
|
|
33
|
+
const candidates = marked.flatMap(row => {
|
|
34
|
+
const taskId = String(row["task_id"]);
|
|
35
|
+
const found = releaseTaskOf(store, taskId);
|
|
36
|
+
return found === null || !shown(found.ref.repo) || taskArchived(store, taskId) ? []
|
|
37
|
+
: [{ taskId, ref: found.ref.id, repo: found.ref.repo, at: String(row["at"]), why: autoPublishHeld(store, taskId) ?? String(row["detail"] ?? "") }];
|
|
38
|
+
});
|
|
39
|
+
if (candidates.length === 0)
|
|
40
|
+
return { release, held: [] };
|
|
41
|
+
// Every family in this person's view, read once (the Tasks list's own projection): a held release's family that is
|
|
42
|
+
// already a row that needs them (in any group) is not listed twice, and one the view doesn't admit is not listed.
|
|
43
|
+
const familyOf = new Map();
|
|
44
|
+
for (const family of workStateRows(store, now, access))
|
|
45
|
+
for (const member of family.members)
|
|
46
|
+
familyOf.set(member, { rootId: family.rootId, needs: family.needs });
|
|
47
|
+
const families = new Set();
|
|
48
|
+
const held = [];
|
|
49
|
+
for (const one of candidates) {
|
|
50
|
+
const family = familyOf.get(one.ref);
|
|
51
|
+
if (family === undefined || family.needs || families.has(family.rootId))
|
|
52
|
+
continue;
|
|
53
|
+
families.add(family.rootId);
|
|
54
|
+
held.push({ id: `release:${family.rootId}`, title: store.getTask(one.taskId)?.title ?? one.taskId, href: releaseSheetHref(one.taskId), repo: one.repo, at: one.at,
|
|
55
|
+
line: one.why, action: { label: REVIEW_RELEASE, href: releaseSheetHref(one.taskId) } });
|
|
56
|
+
}
|
|
57
|
+
return { release, held };
|
|
58
|
+
}
|
|
59
|
+
/** Every active card, in a flow of a project this person may see, that waits on them (flows-ui.ts flowCardWaitsOn, the
|
|
60
|
+
* flow page's own rule). Newest wait first. */
|
|
61
|
+
export function flowAsks(store, repos, viewer, project) {
|
|
62
|
+
if (!viewer.approver)
|
|
63
|
+
return [];
|
|
64
|
+
const rows = [];
|
|
65
|
+
for (const flow of store.listFlows(repos)) {
|
|
66
|
+
if (flow.state !== "active" || (project !== null && flow.repo !== project) || !store.accountCanAccess(viewer.name, flow.repo))
|
|
67
|
+
continue;
|
|
68
|
+
const stages = flowDefinitionOf(flow)?.stages ?? [];
|
|
69
|
+
// The flow's browser standing, read once and only when a card waits in a zone that starts a browser.
|
|
70
|
+
let browser;
|
|
71
|
+
for (const card of store.flowCards(flow.id, false)) {
|
|
72
|
+
if (card.state !== "active")
|
|
73
|
+
continue;
|
|
74
|
+
// A card whose zone was removed still has its subagent's open question (the flow page still takes the answer).
|
|
75
|
+
const stage = stages.find(one => one.id === card.stage);
|
|
76
|
+
if (stage?.kind === "check" && stage.browser === true && browser === undefined)
|
|
77
|
+
browser = browserViewOf(store, flow);
|
|
78
|
+
const waits = flowCardWaitsOn(store, flow, card, viewer, { stage, ...(browser === undefined ? {} : { browser }) });
|
|
79
|
+
if (!waits.decide && !waits.choose && !waits.answer && !waits.browser && !waits.reply)
|
|
80
|
+
continue;
|
|
81
|
+
const asked = waits.answer ? store.openSubagentQuestionOn(card.id, card.entry)?.question ?? null : null;
|
|
82
|
+
// The card's own waiting words (a browser wait without the command the page offers as a button).
|
|
83
|
+
const waiting = card.waiting?.replace(/ Approve it with: toolroll flows browser \d+ on$/, "") ?? null;
|
|
84
|
+
const said = waiting ?? asked ?? (waits.choose ? "Waits for your choice." : "Waits for your decision.");
|
|
85
|
+
const href = flowCardHref(flow.id, card.id);
|
|
86
|
+
rows.push({ id: `flow:${card.id}`, title: card.title, href, repo: flow.repo, at: store.flowCardEnteredAt(card.id) ?? card.updatedAt,
|
|
87
|
+
line: `${flow.name}: ${said}`, action: { label: "Open the card", href } });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return rows.sort((a, b) => (b.at ?? "").localeCompare(a.at ?? ""));
|
|
91
|
+
}
|
|
92
|
+
const SETUP_TITLES = {
|
|
93
|
+
agent: "Sign in to a coding agent", project: "Add a project", builder: "Connect a builder", task: "File your first task",
|
|
94
|
+
};
|
|
95
|
+
/** The first-run steps (first-run.ts, with the builder step) still to do, for an approver. A step that is done, or only
|
|
96
|
+
* being checked (no answer yet, so no action), is not a row. Null steps (a restricted console, the demo, after the first
|
|
97
|
+
* success) are none. */
|
|
98
|
+
export function setupAsks(steps, approver) {
|
|
99
|
+
if (steps === null || !approver)
|
|
100
|
+
return [];
|
|
101
|
+
return steps.flatMap(step => {
|
|
102
|
+
if (step.done || step.checking === true || step.action === null)
|
|
103
|
+
return [];
|
|
104
|
+
const action = step.action.kind === "link" ? { label: step.action.label, href: step.action.href } : { label: "Open setup", href: "/welcome" };
|
|
105
|
+
return [{ key: step.key, id: `setup:${step.key}`, title: SETUP_TITLES[step.key], href: action.href, repo: null, at: null,
|
|
106
|
+
line: step.action.kind === "command" ? `Run ${step.action.command} in a terminal.` : null, action }];
|
|
107
|
+
});
|
|
108
|
+
}
|
package/dist/server/home.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Home ("/", docs/plans/live-pages-and-home.md B1): what needs this person, grouped by what is asked
|
|
3
|
-
*
|
|
4
|
-
* count; one quiet line for what runs and waits; with nothing waiting, what runs and what finished today. The figures
|
|
2
|
+
* Home ("/", docs/plans/live-pages-and-home.md B1, home-other-asks.md): what needs this person, grouped by what is asked
|
|
3
|
+
* (Approve a release, Approve a plan, Answer a question, Flow decisions, Something went wrong, Review results, Finish
|
|
4
|
+
* setting up, and Needs you for the rest), each group with its exact count; one quiet line for what runs and waits; with nothing waiting, what runs and what finished today. The figures
|
|
5
5
|
* are read from the same work index the Tasks list filters, so a count and the list it opens always agree. Reads
|
|
6
6
|
* only: every act keeps its own route and rules (an agent's question is answered through POST /d/<id>/answer, the
|
|
7
7
|
* same one-click act as its own page).
|
|
@@ -9,12 +9,15 @@
|
|
|
9
9
|
import type { BrowserHomeAsk, BrowserHomeView } from "../browser-workspace.js";
|
|
10
10
|
import type { Store } from "../store.js";
|
|
11
11
|
import type { WorkSummaryAccess } from "../work-summary.js";
|
|
12
|
+
import { type HomeAsks } from "./home-asks.js";
|
|
12
13
|
/** How many waiting rows Home lists; each group says how many more open in Tasks. */
|
|
13
14
|
export declare const HOME_NEEDS_SHOWN = 30;
|
|
14
15
|
/** How many rows finished today Home lists; its figure counts them all. */
|
|
15
16
|
export declare const HOME_DONE_SHOWN = 8;
|
|
16
17
|
/** Each group's heading, in the order Home shows them. */
|
|
17
18
|
export declare const HOME_ASK_LABEL: Readonly<Record<BrowserHomeAsk, string>>;
|
|
19
|
+
/** The groups in Home's one order. */
|
|
20
|
+
export declare const HOME_ASK_ORDER: readonly BrowserHomeAsk[];
|
|
18
21
|
export type HomeFacts = {
|
|
19
22
|
store: Store;
|
|
20
23
|
now: Date;
|
|
@@ -31,6 +34,11 @@ export type HomeFacts = {
|
|
|
31
34
|
chat: boolean;
|
|
32
35
|
/** This person can answer an agent's question (an approver in a browser session). */
|
|
33
36
|
canAnswer: boolean;
|
|
37
|
+
/** An approver in a browser session (default: `canAnswer`): a release is then read in Approve a release, never
|
|
38
|
+
* approved here; for anyone else it stays an ordinary task row. */
|
|
39
|
+
approver?: boolean;
|
|
40
|
+
/** What else waits on this person (home-asks.ts), already read through each page's own admission. */
|
|
41
|
+
asks?: HomeAsks;
|
|
34
42
|
/** Why this task's plan can't take a yes right now (render-tasks.ts consentDoorOf), or null when it can. */
|
|
35
43
|
closedDoor: (taskId: string) => {
|
|
36
44
|
title: string;
|
|
@@ -44,8 +52,10 @@ export type HomeFacts = {
|
|
|
44
52
|
at: string;
|
|
45
53
|
} | null;
|
|
46
54
|
};
|
|
47
|
-
/** "19 tasks need you"
|
|
48
|
-
|
|
55
|
+
/** "19 tasks need you", "3 tasks and 2 other things need you", "2 things need you": the heading, said once. Tasks
|
|
56
|
+
* are the same total as the rail's count; `others` are the rows that aren't tasks (a held publication, a flow card, a
|
|
57
|
+
* setup step). */
|
|
58
|
+
export declare function homeSentence(needs: number, any: boolean, others?: number): string;
|
|
49
59
|
/** The lead's words as one plain line: no markdown marks, no line breaks, cut to about one line. */
|
|
50
60
|
export declare function leadLineOf(text: string): string;
|
|
51
61
|
/** Midnight today on this computer's clock: "finished today" starts here. */
|
package/dist/server/home.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { browserWorkActionHref } from "../browser-crew.js";
|
|
2
2
|
import { NO_REASON_RECORDED } from "../needs-you.js";
|
|
3
3
|
import { projectName } from "../project.js";
|
|
4
|
-
import {
|
|
4
|
+
import { workIndexPage } from "../work-index.js";
|
|
5
5
|
import { oneLineOf, relativeAge } from "./chrome.js";
|
|
6
6
|
import { taskHref } from "./http.js";
|
|
7
|
+
import { NO_HOME_ASKS, releaseSheetHref } from "./home-asks.js";
|
|
7
8
|
/** How many waiting rows Home lists; each group says how many more open in Tasks. */
|
|
8
9
|
export const HOME_NEEDS_SHOWN = 30;
|
|
9
10
|
const HOME_RUNNING_SHOWN = 8;
|
|
@@ -13,15 +14,21 @@ export const HOME_DONE_SHOWN = 8;
|
|
|
13
14
|
const LEAD_LINE_LIMIT = 160;
|
|
14
15
|
/** Each group's heading, in the order Home shows them. */
|
|
15
16
|
export const HOME_ASK_LABEL = {
|
|
16
|
-
|
|
17
|
+
release: "Approve a release", approve: "Approve a plan", answer: "Answer a question", flow: "Flow decisions", wrong: "Something went wrong",
|
|
18
|
+
review: "Review results", setup: "Finish setting up", other: "Needs you",
|
|
17
19
|
};
|
|
18
|
-
/**
|
|
19
|
-
export
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
/** The groups in Home's one order. */
|
|
21
|
+
export const HOME_ASK_ORDER = ["release", "approve", "answer", "flow", "wrong", "review", "setup", "other"];
|
|
22
|
+
/** "19 tasks need you", "3 tasks and 2 other things need you", "2 things need you": the heading, said once. Tasks
|
|
23
|
+
* are the same total as the rail's count; `others` are the rows that aren't tasks (a held publication, a flow card, a
|
|
24
|
+
* setup step). */
|
|
25
|
+
export function homeSentence(needs, any, others = 0) {
|
|
26
|
+
const tasks = needs === 1 ? "1 task" : `${needs} tasks`;
|
|
27
|
+
if (others === 0)
|
|
28
|
+
return !any ? "No tasks yet" : needs === 0 ? "Nothing needs you" : `${tasks} ${needs === 1 ? "needs" : "need"} you`;
|
|
22
29
|
if (needs === 0)
|
|
23
|
-
return "
|
|
24
|
-
return
|
|
30
|
+
return others === 1 ? "1 thing needs you" : `${others} things need you`;
|
|
31
|
+
return `${tasks} and ${others} other ${others === 1 ? "thing" : "things"} need you`;
|
|
25
32
|
}
|
|
26
33
|
/** The lead's words as one plain line: no markdown marks, no line breaks, cut to about one line. */
|
|
27
34
|
export function leadLineOf(text) {
|
|
@@ -38,7 +45,15 @@ export function leadLineOf(text) {
|
|
|
38
45
|
export function startOfToday(now) {
|
|
39
46
|
return new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
40
47
|
}
|
|
41
|
-
function rowOf(store, item, now, multi, canAnswer, closedDoor, state) {
|
|
48
|
+
function rowOf(store, item, now, multi, canAnswer, closedDoor, state, release) {
|
|
49
|
+
// A release (approvers): read on its own sheet, never approved here. Its line is its version, or why the sheet can't
|
|
50
|
+
// take a yes now; its one link opens the sheet.
|
|
51
|
+
if (state === "needs" && item.askGroup === "release" && release !== null) {
|
|
52
|
+
const href = releaseSheetHref(item.activeTaskId);
|
|
53
|
+
return { id: item.rootId, title: item.title, href, state, ask: "release", reason: null,
|
|
54
|
+
project: multi || item.repo === null ? (item.repo === null ? "Unplaced" : projectName(item.repo)) : null,
|
|
55
|
+
age: relativeAge(item.updatedAt, now), line: release(item.activeTaskId), action: { label: "Review the release", href }, question: null };
|
|
56
|
+
}
|
|
42
57
|
const action = item.primaryAction;
|
|
43
58
|
const href = action?.code === "open-result" && action.target.runId != null
|
|
44
59
|
? `/review?result=${encodeURIComponent(action.target.taskId)}&run=${action.target.runId}` : browserWorkActionHref(item);
|
|
@@ -50,7 +65,8 @@ function rowOf(store, item, now, multi, canAnswer, closedDoor, state) {
|
|
|
50
65
|
id: decision.id, text: decision.question, recap: decision.recap,
|
|
51
66
|
options: decision.options.map(option => ({ id: option.id, label: option.label, consequence: option.consequence, recommended: option.id === decision.recommendation })),
|
|
52
67
|
} : null;
|
|
53
|
-
|
|
68
|
+
// Anyone else reads a release as the plan it is.
|
|
69
|
+
const ask = state === "needs" ? (item.askGroup === "release" ? "approve" : item.askGroup ?? "other") : null;
|
|
54
70
|
const said = item.status.detail !== item.status.label ? item.status.detail : null;
|
|
55
71
|
const line = state === "needs" && open ? decision.question : item.progress ?? said;
|
|
56
72
|
// Only a row where something went wrong says why, in one short line.
|
|
@@ -101,21 +117,40 @@ export function homeView(facts) {
|
|
|
101
117
|
const live = take(running.items, "running");
|
|
102
118
|
const queued = take(waiting.items, "queued");
|
|
103
119
|
const done = take(today.items, "completed");
|
|
104
|
-
const
|
|
105
|
-
const
|
|
120
|
+
const approver = facts.approver ?? facts.canAnswer;
|
|
121
|
+
const asks = facts.asks ?? NO_HOME_ASKS;
|
|
122
|
+
// A held publication, a flow card and a setup step are an approver's alone (home-asks.ts reads them only for one).
|
|
123
|
+
const held = approver ? asks.held : [], flows = approver ? asks.flows : [];
|
|
124
|
+
// The builder step waits behind a task row that already asks for a builder: that row says it.
|
|
125
|
+
const setup = approver ? asks.setup.filter(one => one.key !== "builder" || needs.builderWaits === 0) : [];
|
|
126
|
+
const extra = [...held, ...flows, ...setup];
|
|
127
|
+
const multi = project === null && new Set([...yours, ...live, ...queued, ...done].map(one => one.repo ?? "").concat(extra.flatMap(one => one.repo === null ? [] : [one.repo]))).size > 1;
|
|
128
|
+
const row = (item, state) => rowOf(store, item, now, multi, facts.canAnswer, facts.closedDoor, state, approver ? asks.release : null);
|
|
106
129
|
const rows = yours.map(one => row(one, "needs"));
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
130
|
+
const askRow = (one, ask) => ({ id: one.id, title: one.title, href: one.href, state: "needs", ask, reason: null,
|
|
131
|
+
project: multi && one.repo !== null ? projectName(one.repo) : null, age: one.at === null ? "" : relativeAge(one.at, now), line: one.line, action: one.action, question: null });
|
|
132
|
+
// Each group's count is exact over every row in view, not only those listed: the work index's total for a task ask
|
|
133
|
+
// (a release counted once, in its own group for an approver and as a plan for anyone else), and every other row.
|
|
134
|
+
const askTotals = {
|
|
135
|
+
release: (approver ? needs.asks.release : 0) + held.length, approve: needs.asks.approve + (approver ? 0 : needs.asks.release), answer: needs.asks.answer,
|
|
136
|
+
flow: flows.length, wrong: needs.asks.wrong, review: needs.asks.review, setup: setup.length, other: needs.asks.other,
|
|
137
|
+
};
|
|
138
|
+
const listedOf = {
|
|
139
|
+
release: [...rows.filter(one => one.ask === "release"), ...held.map(one => askRow(one, "release"))], approve: rows.filter(one => one.ask === "approve"),
|
|
140
|
+
answer: rows.filter(one => one.ask === "answer"), flow: flows.map(one => askRow(one, "flow")), wrong: rows.filter(one => one.ask === "wrong"),
|
|
141
|
+
review: rows.filter(one => one.ask === "review"), setup: setup.map(one => askRow(one, "setup")), other: rows.filter(one => one.ask === "other"),
|
|
142
|
+
};
|
|
143
|
+
const groups = HOME_ASK_ORDER.flatMap(key => {
|
|
144
|
+
const count = askTotals[key], listed = listedOf[key];
|
|
111
145
|
return count === 0 && listed.length === 0 ? [] : [{ key, label: HOME_ASK_LABEL[key], count: Math.max(count, listed.length), rows: listed }];
|
|
112
146
|
});
|
|
147
|
+
const others = extra.length;
|
|
113
148
|
const any = totals.all > 0 || totals.completed > 0 || (totals.archived ?? 0) > 0;
|
|
114
149
|
const lead = facts.chat ? facts.leadMessage ?? null : null;
|
|
115
150
|
const leadText = lead === null ? "" : leadLineOf(lead.text);
|
|
116
151
|
return {
|
|
117
152
|
kind: "home",
|
|
118
|
-
say: homeSentence(totals["needs-you"], any),
|
|
153
|
+
say: homeSentence(totals["needs-you"], any, others),
|
|
119
154
|
project,
|
|
120
155
|
scopes: facts.projects.length < 2 ? null : [
|
|
121
156
|
{ key: "", label: "All projects", href: "/", active: project === null },
|
|
@@ -127,6 +162,6 @@ export function homeView(facts) {
|
|
|
127
162
|
newTask: "/tasks/new",
|
|
128
163
|
ask: facts.chat ? { project, placeholder: "Message the lead" } : null,
|
|
129
164
|
lastLead: lead === null || leadText === "" ? null : { text: leadText, age: relativeAge(lead.at, now), href: "/chat" },
|
|
130
|
-
empty: any ? null : { title: "No tasks yet", text: "Describe a change in a sentence; an agent writes a plan for you to approve." },
|
|
165
|
+
empty: any || others > 0 ? null : { title: "No tasks yet", text: "Describe a change in a sentence; an agent writes a plan for you to approve." },
|
|
131
166
|
};
|
|
132
167
|
}
|
package/dist/server/pages.js
CHANGED
|
@@ -11,7 +11,8 @@ import { realpathSync } from "node:fs";
|
|
|
11
11
|
import {} from "node:http";
|
|
12
12
|
import { homedir, hostname } from "node:os";
|
|
13
13
|
import { basename, dirname, join, resolve } from "node:path";
|
|
14
|
-
import { buildersFor } from "../dispatch.js";
|
|
14
|
+
import { builderReadyFor, buildersFor, firstRunBuilderRepo } from "../dispatch.js";
|
|
15
|
+
import { withBuilderStep } from "../first-run.js";
|
|
15
16
|
import { listFolder, localOrigins } from "../folder-look.js";
|
|
16
17
|
import { Readable } from "node:stream";
|
|
17
18
|
import { pipeline } from "node:stream/promises";
|
|
@@ -70,6 +71,7 @@ import { consentDoorOf, decisionAnswerScript, taskBody } from "./render-tasks.js
|
|
|
70
71
|
import {} from "./session.js";
|
|
71
72
|
import { rereadBrowser } from "./live.js";
|
|
72
73
|
import { homeView } from "./home.js";
|
|
74
|
+
import { flowAsks, releaseAsks, setupAsks } from "./home-asks.js";
|
|
73
75
|
import { workIndexPage } from "../work-index.js";
|
|
74
76
|
export function createPagesHandlers(runtime) {
|
|
75
77
|
const { identities, clock, lastChecks, store, visible, consoleProjects, restricted, ceiling, options, projectFamilyPeek, sendScreen, chromeFor, unscopedMode, admissionList, taskRepoOf, taskViewData, runVisible, evidenceRoot, workAccess, identify, liveCeiling, sessions, deletedRepos, authenticateApprover, authenticateCredential, projectOf, lookupSession, firstRunStepsNow, chatEnablement, leadPrincipal, leadConversationRows } = runtime;
|
|
@@ -204,8 +206,9 @@ export function createPagesHandlers(runtime) {
|
|
|
204
206
|
return door.open ? null : door;
|
|
205
207
|
};
|
|
206
208
|
const chat = chrome.chat === true;
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
+
const access = workAccess(), approver = who.role === "approver";
|
|
210
|
+
const view = { ...homeView({ store, now, access, evidenceRoot, project, projects, chat, canAnswer: approver, approver, closedDoor,
|
|
211
|
+
leadMessage: chat ? leadLastMessage(who, now) : null, asks: homeAsksFor(who, access, now, project) }),
|
|
209
212
|
// Until the first result: the welcome's three steps stay one link away.
|
|
210
213
|
setup: firstRunStepsNow(now) !== null ? "/welcome" : null };
|
|
211
214
|
return sendScreen(response, 200, screen("Home", html `<div data-kit-page><h1>Home</h1><p>${view.say}</p><p><a href="/tasks">Tasks</a></p></div>`, { chrome, workspace: { view } }));
|
|
@@ -1226,5 +1229,14 @@ export function createPagesHandlers(runtime) {
|
|
|
1226
1229
|
const said = leadConversationRows(who, principal, null, now, null).messages.filter(one => one.role === "assistant" && one.pending !== true).at(-1);
|
|
1227
1230
|
return said === undefined ? null : { text: said.text, at: said.createdAt };
|
|
1228
1231
|
}
|
|
1232
|
+
/** What else waits on this person at Home (home-asks.ts): releases and held publications in the work index's own
|
|
1233
|
+
* access, flow cards in the projects this person may pick (the Flows list's admission, each flow's project re-checked
|
|
1234
|
+
* against the account), and the first-run steps with the builder step as Chat reads them. Approvers only. */
|
|
1235
|
+
function homeAsksFor(who, access, now, project) {
|
|
1236
|
+
const approver = who.role === "approver";
|
|
1237
|
+
const steps = withBuilderStep(firstRunStepsNow(now), builderReadyFor(store, firstRunBuilderRepo(store, project, visible, runtime.managedRepos()), now));
|
|
1238
|
+
return { ...releaseAsks(store, now, access, project, approver), flows: flowAsks(store, consoleProjects(), { name: who.name, approver }, project),
|
|
1239
|
+
setup: setupAsks(steps, approver) };
|
|
1240
|
+
}
|
|
1229
1241
|
return { registrations, projectsScreen };
|
|
1230
1242
|
}
|
package/dist/server/remote.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { type Who } from "./session.js";
|
|
|
12
12
|
export interface RemoteRuntime {
|
|
13
13
|
identify: (request: IncomingMessage, touch?: boolean, limited?: boolean, refusedToken?: {
|
|
14
14
|
principal?: Principal;
|
|
15
|
+
elsewhere?: boolean;
|
|
15
16
|
}) => Who | null;
|
|
16
17
|
store: Store;
|
|
17
18
|
admitPasswordSource: (request: IncomingMessage) => Admission;
|
|
@@ -513,6 +513,15 @@ export declare function taskBodyParts(data: {
|
|
|
513
513
|
needsTwo: boolean;
|
|
514
514
|
} | null;
|
|
515
515
|
plan: "requested" | "drafted" | null;
|
|
516
|
+
/** Local-owner slice 2: the planning source this page read (local-owner-acts.ts planSourceOf), which the plan button
|
|
517
|
+
* posts so a local owner's request binds to exactly it. Null when it can't be read. */
|
|
518
|
+
planSource?: string | null;
|
|
519
|
+
/** Local-owner slice 3: the build and approved plan this page read (local-owner-acts.ts retryPinsOf), which Retry posts
|
|
520
|
+
* so a local owner's retry binds to exactly them. */
|
|
521
|
+
retryPins?: {
|
|
522
|
+
run: number;
|
|
523
|
+
digest: string;
|
|
524
|
+
} | null;
|
|
516
525
|
planDocument: string | null;
|
|
517
526
|
planAuto?: boolean;
|
|
518
527
|
/** Hash of the verified plan artifact currently shown. */
|
|
@@ -2096,9 +2096,9 @@ export function taskBodyParts(data) {
|
|
|
2096
2096
|
// A failed task's status card carries Retry itself (the same requeue); here it would be a second ink act.
|
|
2097
2097
|
const cardRetries = task.state === "failed" && data.assignment != null && data.csrf !== "";
|
|
2098
2098
|
const primaryAct = stopControlsActive || cardRetries ? null : stalled && !data.claimed
|
|
2099
|
-
? { html: act("requeue", "Retry, keeping the branch and workspace"), why: "Clears the repeated failures and queues the task again. The branch and workspace are kept.", whyClass: "retry" }
|
|
2099
|
+
? { html: act("requeue", "Retry, keeping the branch and workspace", data.retryPins == null ? {} : { run: String(data.retryPins.run), digest: data.retryPins.digest }), why: "Clears the repeated failures and queues the task again. The branch and workspace are kept.", whyClass: "retry" }
|
|
2100
2100
|
: canPlan
|
|
2101
|
-
? { html: act("plan", "plan first"), why: "plan first sends an agent to read the project, ask you questions and propose a plan. Nothing builds until you approve it.", whyClass: "plan" }
|
|
2101
|
+
? { html: act("plan", "plan first", data.planSource == null ? {} : { source: data.planSource }), why: "plan first sends an agent to read the project, ask you questions and propose a plan. Nothing builds until you approve it.", whyClass: "plan" }
|
|
2102
2102
|
: data.plan === "requested"
|
|
2103
2103
|
? null
|
|
2104
2104
|
: task.state === "queued" && !data.claimed && (data.position?.position ?? 1) > 1
|
|
@@ -2464,9 +2464,9 @@ export function taskBodyParts(data) {
|
|
|
2464
2464
|
};
|
|
2465
2465
|
// Build again, in place, names its form after the card's own act.
|
|
2466
2466
|
const rebuild = data.csrf !== "" && data.assignment?.primaryAction?.code === "retry-task" && data.assignment.need != null && "key" in data.assignment.need && data.assignment.need.key === "rebuild"
|
|
2467
|
-
? { action: `${taskHref(data.assignment.rootId)}/requeue
|
|
2467
|
+
? { action: `${taskHref(data.assignment.rootId)}/requeue`, pins: data.retryPins ?? null } : null;
|
|
2468
2468
|
const retry = failedCard && task.state === "failed" && !data.claimed && !stopControlsActive && data.csrf !== ""
|
|
2469
|
-
? { action: `${taskHref(task.id)}/requeue`, ...(data.failure == null ? {} : { note: retryNoteOf(data.failure.suggestion) }) } : null;
|
|
2469
|
+
? { action: `${taskHref(task.id)}/requeue`, pins: data.retryPins ?? null, ...(data.failure == null ? {} : { note: retryNoteOf(data.failure.suggestion) }) } : null;
|
|
2470
2470
|
// Run checks happens in place: a status row's Run checks posts here and comes back; with nothing to run, the row offers nothing.
|
|
2471
2471
|
const runChecks = data.runChecks ?? null;
|
|
2472
2472
|
// A row's link to a result in Chat opens that result's own page instead: Chat would only lead back here.
|
|
@@ -79,6 +79,10 @@ const LOCAL_POSTS = Object.freeze({
|
|
|
79
79
|
"task.act.resume-arm": "resume-run",
|
|
80
80
|
"task.act.resume": "resume-run",
|
|
81
81
|
"task.act.complete": "complete-task",
|
|
82
|
+
// Local-owner slice 2: the plan button, on its own road (tasks.ts localOwnerPlan; the writer checks this exact act).
|
|
83
|
+
"task.act.plan": "request-plan",
|
|
84
|
+
// Local-owner slice 3: Retry, on its own road (tasks.ts localOwnerRetry; the writer checks this exact act).
|
|
85
|
+
"task.act.requeue": "retry-build",
|
|
82
86
|
"run.act.revise": "request-revision",
|
|
83
87
|
"run.act.note": "request-revision",
|
|
84
88
|
"run.act.comment": "request-revision",
|
package/dist/server/runtime.d.ts
CHANGED
|
@@ -92,6 +92,7 @@ export interface ServerRuntime {
|
|
|
92
92
|
taskRooms: TaskRooms;
|
|
93
93
|
identify: (request: IncomingMessage, touch?: boolean, limited?: boolean, refusedToken?: {
|
|
94
94
|
principal?: Principal;
|
|
95
|
+
elsewhere?: boolean;
|
|
95
96
|
}) => Who | null;
|
|
96
97
|
liveCeiling: () => {
|
|
97
98
|
repos: string[];
|
|
@@ -25,6 +25,7 @@ import { assignmentOf } from '../assignment.js';
|
|
|
25
25
|
import { knowledgeContextHtml } from "../knowledge-ui.js";
|
|
26
26
|
import { failedAttemptSentence, failingCheckSuggestion, GENERAL_SUGGESTION, isInternalErrorReason, latestFinishedAttempt, missedRequirementLine, missedRequirementSuggestion, NO_REASON_RECORDED, stopSuggestionOf } from '../needs-you.js';
|
|
27
27
|
import { planAutoPending } from "../plan-auto.js";
|
|
28
|
+
import { planSourceOf, retryPinsOf } from "../local-owner-acts.js";
|
|
28
29
|
import { readKnowledgeSnapshot } from "../project-knowledge.js";
|
|
29
30
|
import { learningView } from "../project-learning.js";
|
|
30
31
|
import { readSkillsSnapshot, skillTestResult } from "../project-skills.js";
|
|
@@ -384,6 +385,8 @@ export function createTaskViews(runtime) {
|
|
|
384
385
|
})(),
|
|
385
386
|
plan: ref?.plan ?? null,
|
|
386
387
|
planAuto: planAutoPending(runtime.store, taskId, now),
|
|
388
|
+
planSource: planSourceOf(runtime.store, taskId),
|
|
389
|
+
retryPins: retryPinsOf(runtime.store, taskId),
|
|
387
390
|
planDocument: planView?.document ?? null,
|
|
388
391
|
planSha: planView?.sha256 ?? null,
|
|
389
392
|
planContract: ref === null || planView === null ? null : planContractViewOf(ref.id, scope),
|
package/dist/server/tasks.d.ts
CHANGED
|
@@ -81,3 +81,28 @@ export type ApprovalOutcome = "approved" | "vote" | "approved-not-rebuilt" | "re
|
|
|
81
81
|
/** The answer to an approval asked in place: its named outcome and the same words the page would show, as JSON. Written
|
|
82
82
|
* only after the approve route's proofs, by the caller that knows the outcome. */
|
|
83
83
|
export declare function answerApprovalInPlace(response: ServerResponse, outcome: ApprovalOutcome, words: string, status: number): void;
|
|
84
|
+
/**
|
|
85
|
+
* The task page's plan button for the laptop's local owner (local-owner slice 2): bound to the planning source the page
|
|
86
|
+
* read (the form's hidden `source`), never the task as it is now; the writer (local-owner-acts.ts) proves the rest in one
|
|
87
|
+
* transaction. No mode pre-authorization and no pins: the full session's road above is never reached from here.
|
|
88
|
+
*/
|
|
89
|
+
export declare function localOwnerPlan(store: Store, taskId: string, source: string | null | undefined, now: Date): {
|
|
90
|
+
ok: true;
|
|
91
|
+
} | {
|
|
92
|
+
ok: false;
|
|
93
|
+
status: number;
|
|
94
|
+
message: string;
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Retry for the laptop's local owner (local-owner slice 3): bound to the build and the approved plan the page read (the
|
|
98
|
+
* form's hidden `run` and `digest`), never the task as it is now; the writer proves the rest in one transaction. A form
|
|
99
|
+
* that carries neither (an older page) is refused, never bound to whatever is current.
|
|
100
|
+
*/
|
|
101
|
+
export declare function localOwnerRetry(store: Store, taskId: string, run: string | null | undefined, digest: string | null | undefined, now: Date): {
|
|
102
|
+
ok: true;
|
|
103
|
+
replayed: boolean;
|
|
104
|
+
} | {
|
|
105
|
+
ok: false;
|
|
106
|
+
status: number;
|
|
107
|
+
message: string;
|
|
108
|
+
};
|