toolroll 0.9.56 → 0.9.58
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 +16 -0
- package/dist/action-ledger.js +0 -4
- package/dist/actor.d.ts +0 -10
- package/dist/actor.js +1 -18
- package/dist/api-tokens.d.ts +33 -2
- package/dist/api-tokens.js +39 -11
- package/dist/assignment-adapters.d.ts +5 -0
- package/dist/assignment-delivery.js +1 -1
- package/dist/assignment-status.js +1 -1
- package/dist/assignment.d.ts +24 -3
- package/dist/assignment.js +39 -12
- package/dist/browser-workspace.js +1 -1
- package/dist/chat-delivery.js +3 -3
- package/dist/chat-voice.js +1 -1
- package/dist/check-levels.d.ts +3 -0
- package/dist/check-levels.js +5 -0
- package/dist/cli-http.d.ts +7 -0
- package/dist/cli-http.js +30 -4
- package/dist/cli.js +2 -1
- package/dist/coordinator.d.ts +2 -1
- package/dist/coordinator.js +22 -31
- package/dist/desktop-update.d.ts +1 -1
- package/dist/dispose.js +0 -5
- package/dist/export.js +2 -2
- package/dist/lead-status.d.ts +1 -1
- package/dist/lead-status.js +2 -2
- package/dist/mcp-http.js +5 -5
- package/dist/operate.js +19 -9
- package/dist/password-bearer.d.ts +27 -0
- package/dist/password-bearer.js +59 -0
- package/dist/proof.d.ts +9 -0
- package/dist/proof.js +12 -0
- package/dist/release-coverage.d.ts +16 -0
- package/dist/release-coverage.js +92 -0
- package/dist/remote-command.js +0 -2
- package/dist/remote-exec.d.ts +19 -0
- package/dist/remote-exec.js +9 -8
- package/dist/request-budget.d.ts +1 -1
- package/dist/scope.js +2 -1
- package/dist/serve.js +15 -0
- package/dist/server/chat.js +8 -6
- package/dist/server/edge-refusal.d.ts +2 -0
- package/dist/server/edge-refusal.js +8 -6
- package/dist/server/guards.js +5 -4
- package/dist/server/http.d.ts +2 -0
- package/dist/server/remote.js +6 -13
- package/dist/server/render-results.d.ts +4 -1
- package/dist/server/render-results.js +23 -9
- package/dist/server/route-table.js +0 -3
- package/dist/server/tasks.js +28 -25
- package/dist/session-cli.d.ts +38 -9
- package/dist/session-cli.js +141 -154
- package/dist/session-contract.js +2 -2
- package/dist/session-remote.d.ts +19 -0
- package/dist/session-remote.js +86 -0
- package/dist/store.d.ts +31 -11
- package/dist/store.js +276 -95
- package/dist/task-outcome-cli.js +3 -1
- package/dist/task-status.d.ts +59 -7
- package/dist/task-status.js +132 -16
- package/dist/team-cli.d.ts +3 -0
- package/dist/team-cli.js +23 -3
- package/dist/team-runtime.js +1 -1
- package/dist/telegram-status.d.ts +11 -4
- package/dist/telegram-status.js +17 -10
- package/dist/telegram.js +4 -4
- package/dist/toolroll-update.js +3 -1
- package/dist/work-index.d.ts +6 -1
- package/dist/work-index.js +50 -8
- package/dist/workspace-ui.js +2 -1
- package/package.json +1 -1
- package/dist/session-http.d.ts +0 -24
- package/dist/session-http.js +0 -170
- package/dist/session-server.d.ts +0 -17
- package/dist/session-server.js +0 -39
package/dist/remote-exec.js
CHANGED
|
@@ -14,11 +14,12 @@ import { envelopeJson } from './envelope.js';
|
|
|
14
14
|
import { COMMAND_GUIDE } from './surface.js';
|
|
15
15
|
import { centralProfile, UsageError } from './team-cli.js';
|
|
16
16
|
import { CLI_FILES_BYTES, CLI_WAIT_SECONDS } from './cli-http.js';
|
|
17
|
+
import { TOKEN_REPLACEMENT } from './password-bearer.js';
|
|
17
18
|
import { contractRow, fileArguments, STEP_UP_MESSAGE } from './remote-command.js';
|
|
18
19
|
export { contractRow, STEP_UP_MESSAGE } from './remote-command.js';
|
|
19
20
|
const EXIT = { ok: 0, failed: 1, usage: 2, refused: 3 };
|
|
20
|
-
/** Team commands own their own central path (team-cli.ts). */
|
|
21
|
-
const TEAM_COMMANDS = new Set(['connect', 'lead', 'conversation', 'chat', 'brief']);
|
|
21
|
+
/** Team commands own their own central path (team-cli.ts); `session` sends its own checked request (session-cli.ts). */
|
|
22
|
+
const TEAM_COMMANDS = new Set(['connect', 'lead', 'conversation', 'chat', 'brief', 'session']);
|
|
22
23
|
const RESPONSE_BYTES = 16 * 1024 * 1024;
|
|
23
24
|
const REQUEST_TIMEOUT_MS = (CLI_WAIT_SECONDS + 35) * 1_000;
|
|
24
25
|
async function bounded(response) {
|
|
@@ -45,7 +46,7 @@ async function bounded(response) {
|
|
|
45
46
|
return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks)));
|
|
46
47
|
}
|
|
47
48
|
/** One request, never retried. */
|
|
48
|
-
async function
|
|
49
|
+
export async function postCli(origin, token, request, options) {
|
|
49
50
|
try {
|
|
50
51
|
const timeout = AbortSignal.timeout(REQUEST_TIMEOUT_MS);
|
|
51
52
|
const response = await (options.fetch ?? fetch)(`${origin}/api/cli`, {
|
|
@@ -152,7 +153,7 @@ export async function maybeRunRemoteCommand(argv, options = {}) {
|
|
|
152
153
|
if (profile === null || !profile.apiToken) {
|
|
153
154
|
if (!explicit)
|
|
154
155
|
return null;
|
|
155
|
-
return refuse('usage',
|
|
156
|
+
return refuse('usage', `This profile has no API token. ${TOKEN_REPLACEMENT}, then connect again with it (toolroll connect <origin> --token-stdin).`, EXIT.usage);
|
|
156
157
|
}
|
|
157
158
|
if (row === null || mode === 'no') {
|
|
158
159
|
return refuse('usage', `${command} runs only on this computer. Drop --profile to run it here.`, EXIT.usage);
|
|
@@ -192,7 +193,7 @@ export async function maybeRunRemoteCommand(argv, options = {}) {
|
|
|
192
193
|
};
|
|
193
194
|
if (row?.invocation === 'task wait')
|
|
194
195
|
return waitRemotely(sent, files, profile, options, deliver);
|
|
195
|
-
return deliver(await
|
|
196
|
+
return deliver(await postCli(profile.origin, profile.token, { argv: sent, files }, options));
|
|
196
197
|
}
|
|
197
198
|
/**
|
|
198
199
|
* `task wait` holds the server for at most CLI_WAIT_SECONDS per request; the client keeps asking within the caller's
|
|
@@ -203,7 +204,7 @@ async function waitRemotely(argv, files, profile, options, deliver) {
|
|
|
203
204
|
const given = at < 0 ? null : Number(argv[at + 1]);
|
|
204
205
|
const base = at < 0 ? argv : argv.filter((_one, index) => index !== at && index !== at + 1);
|
|
205
206
|
if (given !== null && (!Number.isFinite(given) || given < 0))
|
|
206
|
-
return deliver(await
|
|
207
|
+
return deliver(await postCli(profile.origin, profile.token, { argv, files }, options));
|
|
207
208
|
const json = base.includes('--json');
|
|
208
209
|
const probe = json ? base : [...base, '--json'];
|
|
209
210
|
const started = Date.now();
|
|
@@ -212,7 +213,7 @@ async function waitRemotely(argv, files, profile, options, deliver) {
|
|
|
212
213
|
while (true) {
|
|
213
214
|
const remaining = given === null ? CLI_WAIT_SECONDS : Math.max(0, given - (Date.now() - started) / 1_000);
|
|
214
215
|
const slice = Math.min(CLI_WAIT_SECONDS, Math.floor(remaining * 1_000) / 1_000);
|
|
215
|
-
const outcome = await
|
|
216
|
+
const outcome = await postCli(profile.origin, profile.token, { argv: [...probe, '--timeout', String(slice)], files }, options);
|
|
216
217
|
if (outcome.kind !== 'answer')
|
|
217
218
|
return deliver(outcome);
|
|
218
219
|
let reason;
|
|
@@ -227,7 +228,7 @@ async function waitRemotely(argv, files, profile, options, deliver) {
|
|
|
227
228
|
if (json)
|
|
228
229
|
return deliver(outcome);
|
|
229
230
|
// Settled (or out of time): the server renders the final answer in the person's own format.
|
|
230
|
-
return deliver(await
|
|
231
|
+
return deliver(await postCli(profile.origin, profile.token, { argv: [...base, '--timeout', '0'], files }, options));
|
|
231
232
|
}
|
|
232
233
|
if (signal.aborted)
|
|
233
234
|
return deliver(outcome);
|
package/dist/request-budget.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Request budgets for person API tokens (v112): how many requests one `so_` token may make across all accepting routes.
|
|
3
|
-
* One budget per token, keyed by its
|
|
3
|
+
* One budget per token, keyed by its credential row id (a person's API token, v118) (never the secret, its name or an address), shared by those
|
|
4
4
|
* routes. A read token gets 120 requests in any sliding minute, an act token 30, and either 10,000 in a rolling day;
|
|
5
5
|
* an instance operator may override any of these for the installation or for one token in the console (step-up).
|
|
6
6
|
*
|
package/dist/scope.js
CHANGED
|
@@ -5,7 +5,8 @@ import { validateScopeText } from "./task-text.js";
|
|
|
5
5
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
6
6
|
import { projectAuthority } from "./project-access.js";
|
|
7
7
|
import { activeRemote } from "./remote-run.js";
|
|
8
|
-
import { claimActor, currentActor
|
|
8
|
+
import { claimActor, currentActor } from "./actor.js";
|
|
9
|
+
import { parseLeadToken } from "./api-tokens.js";
|
|
9
10
|
/**
|
|
10
11
|
* What a task is allowed to become, agreed before anything builds it.
|
|
11
12
|
*
|
package/dist/serve.js
CHANGED
|
@@ -16,6 +16,7 @@ import { MOBILE_VIEWPORT_SCRIPT } from "./mobile-viewport.js";
|
|
|
16
16
|
import { projectAuthority } from "./project-access.js";
|
|
17
17
|
import { discoverTools, projectToolsOf, secretsSetFor, TOOL_CATALOG } from "./project-tools.js";
|
|
18
18
|
import { isEdgeAddress, refuseEdge } from "./server/edge-refusal.js";
|
|
19
|
+
import { announcePasswordBearer, isPasswordBearer, PASSWORD_BEARER_REFUSED, passwordBearerAccepted } from "./password-bearer.js";
|
|
19
20
|
import { createSharedGuards } from "./server/guards.js";
|
|
20
21
|
import { createHandlerRegistry } from "./server/handler-registry.js";
|
|
21
22
|
import {} from "./server/remote-hooks.js";
|
|
@@ -606,6 +607,17 @@ export function createDecisionServer(options) {
|
|
|
606
607
|
const matchedRoute = matchRoute(method, hook.pathname);
|
|
607
608
|
const edgeRoute = matchedRoute?.stage === 'edge' ? matchedRoute : null;
|
|
608
609
|
const wrongMethodEdge = edgeRoute === null ? ROUTES.find(row => row.stage === 'edge' && new RegExp(row.pattern).test(hook.pathname)) ?? null : null;
|
|
610
|
+
// D7: a password on a request still signs in this release, announced on every answer and named in every refusal;
|
|
611
|
+
// from the release that removes it, it is refused here before anything checks it.
|
|
612
|
+
if (isPasswordBearer(request.headers.authorization)) {
|
|
613
|
+
if (!(options.passwordBearerAccepted ?? passwordBearerAccepted())) {
|
|
614
|
+
request.resume();
|
|
615
|
+
return hook.pathname.startsWith('/api/')
|
|
616
|
+
? respond(response, 401, 'application/json; charset=utf-8', JSON.stringify({ ...(hook.pathname.startsWith('/api/team') ? { version: 1 } : {}), ok: false, code: 'password-bearer-refused', message: PASSWORD_BEARER_REFUSED }))
|
|
617
|
+
: respond(response, 401, 'text/plain; charset=utf-8', PASSWORD_BEARER_REFUSED);
|
|
618
|
+
}
|
|
619
|
+
announcePasswordBearer(response);
|
|
620
|
+
}
|
|
609
621
|
if (edgeRoute?.host === 'before')
|
|
610
622
|
return dispatchEdge(edgeRoute, { url: hook, who: null, request, response, method });
|
|
611
623
|
if (wrongMethodEdge?.host === 'before')
|
|
@@ -667,6 +679,9 @@ export function createDecisionServer(options) {
|
|
|
667
679
|
if (edgeRoute?.domain === 'people')
|
|
668
680
|
return provenPasswordAccount.run(passwordProof, () => dispatchEdge(edgeRoute, { url, who, request, response, method }));
|
|
669
681
|
if (who === null) {
|
|
682
|
+
// A password on a request that did not sign in is a machine's refusal, never a login page.
|
|
683
|
+
if (isPasswordBearer(request.headers.authorization))
|
|
684
|
+
return respond(response, 401, "text/plain; charset=utf-8", "authenticate first");
|
|
670
685
|
// A GET to an exact task or result (a phone's deep link) signs in and
|
|
671
686
|
// comes back to it — the destination is a same-site path only.
|
|
672
687
|
return method === "GET"
|
package/dist/server/chat.js
CHANGED
|
@@ -40,7 +40,7 @@ import { refuse, screen } from "./chrome.js";
|
|
|
40
40
|
import { chatResultHref, chatReturnWithLatest, chatReturnWithSaid, projectChatHref, redirect, respond, safeChatReturn, safeReturn, taskChatHref, taskHref } from "./http.js";
|
|
41
41
|
import { chatAckPage, chatPage, coordinatorProposalsSection, decisionsFor, leadAfterComposerHtml, leadChatVersion, leadMintCard, leadPage, leadThreadHtml, PHONE_CARD_FACT, TASK_COMPOSER_MODES, taskChatLiveRegion, teamProposalCardParts } from "./render-chat.js";
|
|
42
42
|
import {} from "./render-pages.js";
|
|
43
|
-
import { completionForm, homePhaseWords, owedAcceptanceOf, resultPanelHtml } from "./render-results.js";
|
|
43
|
+
import { completionForm, hardBlockerOf, homePhaseWords, owedAcceptanceOf, resultPanelHtml } from "./render-results.js";
|
|
44
44
|
import { browserCaller, oneLineUa } from "./session.js";
|
|
45
45
|
export function createChatHandlers(runtime) {
|
|
46
46
|
const { firstRunStepsNow, managedRepos, leadWords, store, options, phoneSetup, visible, liveTurns, leadSaid, evidenceRoot, clock, sessions, CHAT_CANDIDATE_TTL_MS, chatFetcher, CHAT_CANDIDATES_PER_APPROVER, chatCeilingDigest, workAccess, familyOf, firstTasks, leadPrincipal, taskChatFocus, chatScopeOf, leadConversationRows, demoLeadHere, sendScreen, chromeFor, teamBrowserReply, team, teamChatProvider, runVisible, runIsLive, resultDetailOf, pullRequestTargetOf, chatEnablement, startLeadConversation, projectFamilyPeek, needsYouBadge, liveRefreshSeconds, chatKeyFor, chatCatalog, providerHome, mintApprovalNonce, checkLocalAgents, agentSignInCommand, bustBadge, authenticateApprover, revisionDestination, armTaskResume, consumeApprovalNonce } = runtime;
|
|
@@ -185,7 +185,9 @@ export function createChatHandlers(runtime) {
|
|
|
185
185
|
const roomId = url.searchParams.get('conversation');
|
|
186
186
|
const resultLink = (href) => roomId ? href + '&conversation=' + encodeURIComponent(roomId) : href;
|
|
187
187
|
// Accept and finish is here: the one act that accepts the person's own checks and finishes the task.
|
|
188
|
-
|
|
188
|
+
// Never offered where Complete is refused whatever is accepted (a failed check, a HIGH finding).
|
|
189
|
+
const finishes = resultRun !== null && who.role === 'approver' && focusTask?.assignment?.state === 'ready-to-check' && focusTask.assignment.receipt?.runId === resultRun.id &&
|
|
190
|
+
hardBlockerOf(focusTask.assignment) === null;
|
|
189
191
|
const resultPanel = resultRun === null
|
|
190
192
|
? null
|
|
191
193
|
: html `${resultPanelHtml(resultDetailOf(resultRun, who, now), {
|
|
@@ -267,7 +269,7 @@ export function createChatHandlers(runtime) {
|
|
|
267
269
|
if (repos.length > 0) {
|
|
268
270
|
try {
|
|
269
271
|
fleetSnapshot = store.chatSnapshot(repos, now);
|
|
270
|
-
const summaries = workIndexPage(store, now, { principal: 'operator', repos, includeUnplaced: false, viewer: who.name }, { limit: 100 }).items;
|
|
272
|
+
const summaries = workIndexPage(store, now, { principal: 'operator', repos, includeUnplaced: false, viewer: who.name }, { limit: 100, root: evidenceRoot }).items;
|
|
271
273
|
fleetSnapshot.assignmentStates = Object.fromEntries(summaries.flatMap(value => [value.rootId, value.activeTaskId].map(id => [id, { state: value.assignmentState, label: value.status.label, detail: value.status.detail }])));
|
|
272
274
|
fleetSnapshot.attentionCount = needsYouBadge(null);
|
|
273
275
|
}
|
|
@@ -1291,9 +1293,9 @@ export function createChatHandlers(runtime) {
|
|
|
1291
1293
|
agent: `${providerName(run.provider)} on ${run.runner}`, phase: homePhaseWords(run), project: run.repo === null ? null : projectName(run.repo), since: run.startedAt,
|
|
1292
1294
|
activity: runActivityOf(store, run, now) };
|
|
1293
1295
|
});
|
|
1294
|
-
const all = workIndexPage(store, now, access, { view: "all", limit: WORK_INDEX_MAX_LIMIT });
|
|
1295
|
-
const needs = workIndexPage(store, now, access, { view: "needs-you", limit: WORK_INDEX_MAX_LIMIT });
|
|
1296
|
-
const done = workIndexPage(store, now, access, { view: "completed", limit: WORK_INDEX_MAX_LIMIT });
|
|
1296
|
+
const all = workIndexPage(store, now, access, { view: "all", limit: WORK_INDEX_MAX_LIMIT, root: evidenceRoot });
|
|
1297
|
+
const needs = workIndexPage(store, now, access, { view: "needs-you", limit: WORK_INDEX_MAX_LIMIT, root: evidenceRoot });
|
|
1298
|
+
const done = workIndexPage(store, now, access, { view: "completed", limit: WORK_INDEX_MAX_LIMIT, root: evidenceRoot });
|
|
1297
1299
|
const weekAgo = new Date(now.getTime() - 7 * 86_400_000).toISOString();
|
|
1298
1300
|
const counts = [
|
|
1299
1301
|
{ key: "working", label: "Working now", value: agents.length, href: "/work?view=running" },
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
2
|
import { type RouteDeclaration } from './route-table.js';
|
|
3
|
+
/** What a request to the retired /api/sessions transport hears (D7). */
|
|
4
|
+
export declare const SESSIONS_MOVED: string;
|
|
3
5
|
export declare function isEdgeAddress(path: string): boolean;
|
|
4
6
|
/** Preserve each transport's refusal envelope without invoking an undeclared adapter. */
|
|
5
7
|
export declare function refuseEdge(request: IncomingMessage, response: ServerResponse, path: string, declared: RouteDeclaration | null): void;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { TOKEN_REPLACEMENT } from '../password-bearer.js';
|
|
2
2
|
import { ROUTES } from './route-table.js';
|
|
3
|
+
/** What a request to the retired /api/sessions transport hears (D7). */
|
|
4
|
+
export const SESSIONS_MOVED = `Session operations moved to /api/cli. Update toolroll, ${TOKEN_REPLACEMENT.charAt(0).toLowerCase()}${TOKEN_REPLACEMENT.slice(1)}, connect with it, and run toolroll session <operation>.`;
|
|
3
5
|
export function isEdgeAddress(path) {
|
|
4
6
|
return ROUTES.some(row => row.stage === 'edge' && new RegExp(row.pattern).test(path))
|
|
5
7
|
|| /^\/(?:oauth|hooks|api\/(?:sessions|team|cli))(?:\/|$)/.test(path)
|
|
@@ -15,13 +17,13 @@ export function refuseEdge(request, response, path, declared) {
|
|
|
15
17
|
type = 'application/json';
|
|
16
18
|
body = JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32000, message: 'the MCP gateway takes POST only — one JSON-RPC message per request' } });
|
|
17
19
|
}
|
|
18
|
-
else if (path.startsWith('/api/sessions/')) {
|
|
19
|
-
|
|
20
|
+
else if (path === '/api/sessions' || path.startsWith('/api/sessions/')) {
|
|
21
|
+
// D7: session operations moved to /api/cli. An older client hears that nothing was sent, and what to use instead.
|
|
22
|
+
response.statusCode = 404;
|
|
20
23
|
type = 'application/json';
|
|
21
24
|
response.setHeader('X-Toolroll-Delivery', 'not-sent');
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
body = JSON.stringify({ version: 1, ...(spec ? { operation: spec.operation } : {}), ok: false, status: 'rejected', delivery: 'not-sent', retry: spec?.mutation === false ? 'safe-read' : 'never', reason: spec ? 'method-not-allowed' : 'unsupported-operation', message: spec ? 'Session operations require POST.' : 'This session operation is not supported.', nextActions: [] });
|
|
25
|
+
response.setHeader('X-Standing-Orders-Session-Delivery', 'not-sent');
|
|
26
|
+
body = JSON.stringify({ version: 1, ok: false, status: 'rejected', delivery: 'not-sent', retry: 'never', reason: 'moved', message: SESSIONS_MOVED, nextActions: [] });
|
|
25
27
|
}
|
|
26
28
|
else if (path === '/api/cli' || path.startsWith('/api/team')) {
|
|
27
29
|
type = 'application/json';
|
package/dist/server/guards.js
CHANGED
|
@@ -2,7 +2,7 @@ import { OWNER_ACTIONS, sharedActionPayload } from '../chat-actions.js';
|
|
|
2
2
|
import { CONSOLE_FORMS, readForm } from "../contracts/console-api.js";
|
|
3
3
|
import { timingSafeEqual } from "node:crypto";
|
|
4
4
|
import {} from "node:http";
|
|
5
|
-
import {
|
|
5
|
+
import { tokenLive, tokenProjects } from "../api-tokens.js";
|
|
6
6
|
import { oauthProjects, oauthTokenAllowed } from "../mcp-oauth.js";
|
|
7
7
|
import { REMOTE_MESSAGES, reproveRemote } from "../operate-remote.js";
|
|
8
8
|
import { canonicalProject, rowVisible } from "../project.js";
|
|
@@ -264,9 +264,10 @@ export function createSharedGuards(runtime) {
|
|
|
264
264
|
const source = runtime.joinSourceOf(request), at = Date.now();
|
|
265
265
|
if (runtime.signInBudget.waitFor(source, at) > 0)
|
|
266
266
|
return null;
|
|
267
|
-
|
|
268
|
-
const
|
|
269
|
-
|
|
267
|
+
// The one credential check (api-tokens.ts): only a person's API token signs in here.
|
|
268
|
+
const found = runtime.store.presentedCredential(presented);
|
|
269
|
+
const kept = found?.kind === "person" ? runtime.store.apiTokenSecret(found.id) : null;
|
|
270
|
+
if (kept === null) {
|
|
270
271
|
runtime.signInBudget.failed(source, at);
|
|
271
272
|
return null;
|
|
272
273
|
}
|
package/dist/server/http.d.ts
CHANGED
|
@@ -43,6 +43,8 @@ export type ServeOptions = {
|
|
|
43
43
|
cliRunner?: RunOperateAs;
|
|
44
44
|
/** Tests: remote command metadata until the shared contract declares it. */
|
|
45
45
|
cliModeOf?: CliHttpOptions['modeOf'];
|
|
46
|
+
/** Tests: whether a password on a request is still accepted (default: this release's version, password-bearer.ts). */
|
|
47
|
+
passwordBearerAccepted?: boolean;
|
|
46
48
|
/** Tests: the clock request budgets for API tokens count by (request-budget.ts). */
|
|
47
49
|
requestBudgetClock?: () => number;
|
|
48
50
|
/** Where repos.json lives — every enrollment locks exactly this file. */
|
package/dist/server/remote.js
CHANGED
|
@@ -9,7 +9,7 @@ import { reproveRemote } from "../operate-remote.js";
|
|
|
9
9
|
import { rowVisible } from "../project.js";
|
|
10
10
|
import { RequestBudget, SourceAdmission } from "../request-budget.js";
|
|
11
11
|
import { authenticateAccount } from "../scope.js";
|
|
12
|
-
import {
|
|
12
|
+
import { createSessionRunner } from '../session-remote.js';
|
|
13
13
|
import {} from "../store.js";
|
|
14
14
|
import { handleTeamHttp, TEAM_PASSWORD_REVERIFY_MS } from '../team-http.js';
|
|
15
15
|
import { handleTeamsHttp } from "../teams.js";
|
|
@@ -97,16 +97,17 @@ export function createRemoteHandlers(runtime) {
|
|
|
97
97
|
return who?.via === 'cookie' ? teamBrowserReply(reply, actor, who.session.csrf) : reply;
|
|
98
98
|
},
|
|
99
99
|
});
|
|
100
|
+
// D7: native session operations are remote commands too (`session …` over /api/cli), run by the session owner.
|
|
101
|
+
const sessionRunner = createSessionRunner({ store, workspace: coding, projects: codingProjects, projectAllowed: repo => rowVisible(liveCeiling(), repo), clock });
|
|
100
102
|
// Remote CLI: one live API token names the person; the shared command boundary decides everything else.
|
|
101
103
|
const cliEndpoint = (request, response) => handleCliHttp(request, response, {
|
|
102
104
|
admit: principal => requestBudget.admit(principal.tokenId, "api"),
|
|
103
105
|
authenticate: tokenPrincipalOf,
|
|
104
106
|
run: async () => options.cliRunner ?? (await import("../operate.js")).runOperateAs ?? null,
|
|
105
107
|
modeOf: options.cliModeOf,
|
|
108
|
+
session: sessionRunner,
|
|
106
109
|
store,
|
|
107
110
|
});
|
|
108
|
-
const sessionEndpoint = createSessionEndpoint({ store, workspace: coding, projects: codingProjects, projectAllowed: repo => rowVisible(liveCeiling(), repo),
|
|
109
|
-
admit: admitPasswordSource, admitAuthenticated: admitBearer });
|
|
110
111
|
const mcpHttp = createMcpHttp({ store, clock, evidenceRoot, ...(options.requestBudgetClock === undefined ? {} : { requestBudgetClock: options.requestBudgetClock }), signedIn: request => identify(request, true, true) !== null, admit: person => requestBudget.admit(person.principal.tokenId, "mcp"),
|
|
111
112
|
resourceMetadata: request => { const origin = consoleOrigin(request.headers.host); return origin === null ? null : resourceMetadataUrl(origin); },
|
|
112
113
|
enrolled: () => [...new Set([...managedRepos(), ...store.listProjects().map(project => project.path)])], ...(options.runOperateAs === undefined ? {} : { runAs: options.runOperateAs }) });
|
|
@@ -138,8 +139,8 @@ export function createRemoteHandlers(runtime) {
|
|
|
138
139
|
return;
|
|
139
140
|
return respond(response, 404, 'text/plain; charset=utf-8', 'No such address.');
|
|
140
141
|
};
|
|
141
|
-
// D5:
|
|
142
|
-
//
|
|
142
|
+
// D5: the central team service is deprecated (still served this release); each answer says so in a Deprecation
|
|
143
|
+
// header (RFC 9745).
|
|
143
144
|
// Each protocol adapter keeps its own operation boundary; every row still names its own entry.
|
|
144
145
|
const registrations = handlersOf("remote", {}, {
|
|
145
146
|
"edge.telegram-hook": async ({ request, response }) => { await telegramHook(hookContext, request, response); },
|
|
@@ -152,14 +153,6 @@ export function createRemoteHandlers(runtime) {
|
|
|
152
153
|
"edge.oauth-token": async ({ request, response, url }) => { await oauthHttp(request, response, url); },
|
|
153
154
|
"edge.oauth-authorize": async ({ request, response, url }) => { await oauthHttp(request, response, url); },
|
|
154
155
|
"edge.oauth-consent": async ({ request, response, url }) => { await oauthHttp(request, response, url); },
|
|
155
|
-
"edge.sessions-list": async ({ request, response }) => { response.setHeader('Deprecation', 'true'); await sessionEndpoint(request, response); },
|
|
156
|
-
"edge.sessions-show": async ({ request, response }) => { response.setHeader('Deprecation', 'true'); await sessionEndpoint(request, response); },
|
|
157
|
-
"edge.sessions-changes": async ({ request, response }) => { response.setHeader('Deprecation', 'true'); await sessionEndpoint(request, response); },
|
|
158
|
-
"edge.sessions-start": async ({ request, response }) => { response.setHeader('Deprecation', 'true'); await sessionEndpoint(request, response); },
|
|
159
|
-
"edge.sessions-send": async ({ request, response }) => { response.setHeader('Deprecation', 'true'); await sessionEndpoint(request, response); },
|
|
160
|
-
"edge.sessions-stop": async ({ request, response }) => { response.setHeader('Deprecation', 'true'); await sessionEndpoint(request, response); },
|
|
161
|
-
"edge.sessions-resume": async ({ request, response }) => { response.setHeader('Deprecation', 'true'); await sessionEndpoint(request, response); },
|
|
162
|
-
"edge.sessions-recover": async ({ request, response }) => { response.setHeader('Deprecation', 'true'); await sessionEndpoint(request, response); },
|
|
163
156
|
"edge.team-read": async ({ request, response }) => { response.setHeader('Deprecation', 'true'); await teamEndpoint(request, response); },
|
|
164
157
|
"edge.team-send": async ({ request, response }) => { response.setHeader('Deprecation', 'true'); await teamEndpoint(request, response); },
|
|
165
158
|
"edge.cli": async ({ request, response }) => { await cliEndpoint(request, response); },
|
|
@@ -12,7 +12,7 @@ import { type BuildReviewView } from "../review-switch.js";
|
|
|
12
12
|
import { type AcceptanceCriterion, type approvalOf } from "../scope.js";
|
|
13
13
|
import { type ReportItem } from "../scout-report.js";
|
|
14
14
|
import { type Artifact, type DiffComment, type Publication, type RepairChainRow, type ReviewRetryState, type Run, type Store, type TaskState } from "../store.js";
|
|
15
|
-
import { type PullRequestFact, type TaskStatus } from "../task-status.js";
|
|
15
|
+
import { type ReleaseState, type PullRequestFact, type TaskStatus } from "../task-status.js";
|
|
16
16
|
import { type DisplayStatus, type PublicationFacts, type ReviewFacts } from "../workspace-ui.js";
|
|
17
17
|
import { type Chrome, type Screen } from "./chrome.js";
|
|
18
18
|
import { type Html } from "../html.js";
|
|
@@ -78,6 +78,7 @@ export declare function criterionMatrixHtml(matrix: readonly CriterionMatrixRow[
|
|
|
78
78
|
links?: EvidenceLinkMap;
|
|
79
79
|
fileAnchors?: ReadonlyMap<string, string>;
|
|
80
80
|
verdict?: string | null;
|
|
81
|
+
release?: ReleaseState | undefined;
|
|
81
82
|
}): Html;
|
|
82
83
|
/** One policy summary beneath the requirements. Per-requirement concerns
|
|
83
84
|
* are already visible in the matrix; do not repeat their full text here. */
|
|
@@ -674,6 +675,8 @@ export declare const CHECK_EXCERPT_LINES = 12;
|
|
|
674
675
|
* change's first file, labelled as not cited), the screenshots it cites
|
|
675
676
|
* (none cited: every validated one), and the agent's own note. */
|
|
676
677
|
export declare function personCheckItems(proof: ProofBundleView, patchText: string | null, shots: readonly ResultScreenshot[], runId: number): BrowserCheckItem[];
|
|
678
|
+
/** The completion blocker no acceptance resolves (a failed check, an unresolved HIGH finding), in words, or null. */
|
|
679
|
+
export declare function hardBlockerOf(assignment: AssignmentSnapshot | null | undefined): string | null;
|
|
677
680
|
/** A Needs you result's one action (needs-you.ts): a link to the act that resolves it, or Confirm it
|
|
678
681
|
* stopped behind the password. Null under every other headline. */
|
|
679
682
|
export declare function needActionOf(assignment: AssignmentSnapshot | null, status: TaskStatus | null, csrf: string, returnTo: string, result?: {
|
|
@@ -21,7 +21,7 @@ import {} from "../scope.js";
|
|
|
21
21
|
import { parseReport } from "../scout-report.js";
|
|
22
22
|
import {} from "../store.js";
|
|
23
23
|
import { runCostWords } from "../summary.js";
|
|
24
|
-
import { demoChecksOf, pullRequestFactOf, requirementsOf, requirementWordOf, STATUS_MORE, statusDetailsHtml, statusIconSvg, statusWhyHtml } from "../task-status.js";
|
|
24
|
+
import { checkBackingOf, CHECKED_AT_RELEASE, demoChecksOf, pullRequestFactOf, requirementsOf, requirementWordOf, STATUS_MORE, statusDetailsHtml, statusIconSvg, statusWhyHtml } from "../task-status.js";
|
|
25
25
|
import { ACCEPT_NEEDS_REASON, acceptWordsOf, cantAcceptYetOf, evidenceProblemOf, MISMATCH_HEADLINE, receiptHeadingOf, receiptPublicationWords, reportMismatchesOf, RESULT_DECISION_SENTENCE, resultHeadlineOf, resultStatusOf, REVIEW_TOKENS } from "../workspace-ui.js";
|
|
26
26
|
import { createHash, randomBytes } from "node:crypto";
|
|
27
27
|
import { buildsViews, oneLineOf, projectChip, safePrUrl, screen, sentenceCase, strokeIcon, when, whenTime } from "./chrome.js";
|
|
@@ -173,13 +173,14 @@ export function criterionMatrixHtml(matrix, options = {}) {
|
|
|
173
173
|
const confirmed = row.assessment !== undefined && state === "pass";
|
|
174
174
|
// Plain words (task-status brief): the retired assessment step is not a state a person acts on.
|
|
175
175
|
// The card's Requirements row reads the same words from the same source (task-status.ts), so they agree.
|
|
176
|
-
const label = requirementWordOf(row, options.verdict);
|
|
176
|
+
const label = requirementWordOf(row, options.verdict, options.release);
|
|
177
177
|
const cls = label === "Met" ? "badge-done" : label === "You check" ? "badge-manual-review" : "badge-note";
|
|
178
178
|
const warnings = [];
|
|
179
179
|
// Only replace the known boilerplate. Other recorded failure details
|
|
180
180
|
// remain verbatim, so concision cannot hide a different problem.
|
|
181
181
|
const detail = row.detail.filter(line => !(row.assessment && row.review && line === `${row.review.author} ${row.review.judgement === "contradicts" ? "contradicts" : "needs more evidence for"} criterion "${row.id}": ${row.review.note}`) && !(state === "manual-review" && /^criterion "[^"\n]+" requires manual-review evidence — an operator must accept it before this can verify$/.test(line)));
|
|
182
|
-
|
|
182
|
+
// A requirement a release check covers, or that waits only on one, has nothing wrong to say.
|
|
183
|
+
if (!awaitingAssessment && state !== "pass" && label !== "Met" && label !== CHECKED_AT_RELEASE && detail.length > 0)
|
|
183
184
|
warnings.push(html `<ul class="requirement-issues">${detail.map(line => html `<li>${line}</li>`)}</ul>`);
|
|
184
185
|
const review = row.review;
|
|
185
186
|
if (review !== null && review.judgement !== "upholds") {
|
|
@@ -671,13 +672,16 @@ export function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab,
|
|
|
671
672
|
action: false,
|
|
672
673
|
});
|
|
673
674
|
parts.push(html `<div id="verification" data-cockpit-section="result">${panel.html}</div>`);
|
|
674
|
-
|
|
675
|
+
// What no acceptance resolves (a failed check, a HIGH finding): Complete is refused, so no Accept is offered and
|
|
676
|
+
// the one line says the next step.
|
|
677
|
+
const hardStop = hardBlockerOf(assignment);
|
|
678
|
+
const blocked = hardStop ?? cantAcceptYetOf(proof?.verdict ?? null, proof?.reasons ?? [], accepted);
|
|
675
679
|
const youCheck = panel.panel.youCheck;
|
|
676
680
|
// Accept and finish posts the completion; when an acceptance is owed (the person's own checks, or a reason a
|
|
677
681
|
// report that doesn't match its changes asks for), the same request records it first.
|
|
678
682
|
const owed = accepted ? null : youCheck?.accept != null ? { note: null } : panel.panel.need?.accept != null ? { note: panel.panel.need.accept.note }
|
|
679
683
|
: blocked === ACCEPT_NEEDS_REASON ? { note: "Why is this safe to accept?" } : null;
|
|
680
|
-
const complete = assignment?.state === "ready-to-check" && assignment.receipt !== null && canRetryReview && csrf !== ""
|
|
684
|
+
const complete = hardStop === null && assignment?.state === "ready-to-check" && assignment.receipt !== null && canRetryReview && csrf !== ""
|
|
681
685
|
? { action: `${taskHref(view.taskId)}/complete`, receipt: assignment.receipt.digest, run: run.id, accept: owed } : null;
|
|
682
686
|
if (complete !== null)
|
|
683
687
|
parts.push(completionForm(view.taskId, run.id, complete.receipt, view.detail?.pullRequestTo ?? null, owed));
|
|
@@ -686,8 +690,10 @@ export function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab,
|
|
|
686
690
|
const matrix = proof === null || proof.proofProblem !== null ? [] : proof.matrix;
|
|
687
691
|
const unanswered = accepted || youCheck == null ? [] : youCheck.items.length > 0 ? youCheck.items.map(one => one.statement === "" ? one.words : one.statement) : youCheck.lines;
|
|
688
692
|
const base = !acceptsHere ? null : acceptWordsOf({
|
|
689
|
-
|
|
690
|
-
|
|
693
|
+
// Checks Off that a release check already covers read as passed (release-coverage.ts).
|
|
694
|
+
checks: checks === undefined ? null : checks.running != null ? "running" : checks.level === "off" && checks.status !== "passed" ? checks.release != null ? "passed" : "off" : checks.status,
|
|
695
|
+
// Release-aware words (task-status.ts): a requirement waiting only on a release check, or covered by one, isn't unmet.
|
|
696
|
+
unmet: matrix.filter(row => !["Met", "You check", CHECKED_AT_RELEASE].includes(requirementWordOf(row, null, checks === undefined ? undefined : checkBackingOf(checks)))).length,
|
|
691
697
|
action: complete !== null ? "complete" : "accept",
|
|
692
698
|
publishing: view.detail?.publishing ?? "other",
|
|
693
699
|
proof: proof !== null && proof.proof !== null && proof.proofProblem === null,
|
|
@@ -700,7 +706,7 @@ export function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab,
|
|
|
700
706
|
const followUps = view.detail.followUps ?? null;
|
|
701
707
|
const checksRunning = checks?.running != null || (followUps?.checks.some(one => one.state === "waiting" || one.state === "running") ?? false);
|
|
702
708
|
const runChecks = canRetryReview && csrf !== "" && followUps !== null && (followUps.full || followUps.quick) && !checksRunning &&
|
|
703
|
-
(checks === undefined || checks.status === "not-run" || checks.status === "unavailable")
|
|
709
|
+
(checks === undefined || ((checks.status === "not-run" || checks.status === "unavailable") && checks.release == null))
|
|
704
710
|
// Back on this result's Checks tab, where #follow-ups shows the run it started.
|
|
705
711
|
? { action: `/r/${run.id}/checks`, level: followUps.full ? "full" : "quick", returnTo: `${here}&run=${run.id}&tab=checks` } : null;
|
|
706
712
|
const need = panel.panel.need;
|
|
@@ -1774,7 +1780,8 @@ export function resultPanelParts(detail, o) {
|
|
|
1774
1780
|
checkParts.push(detail.signedCriteria > 0 ? html `<p class="meta">The approved scope has ${detail.signedCriteria} requirement${detail.signedCriteria === 1 ? "" : "s"}, but this build has no requirement-by-requirement verification.</p>` : html `<p class="meta">This scope signed no acceptance checks.</p>`);
|
|
1775
1781
|
}
|
|
1776
1782
|
else {
|
|
1777
|
-
checkParts.push(criterionMatrixHtml(proof.matrix, { runId, links: proof.matrixLinks, fileAnchors: detail.fileAnchors, verdict: proof.verdict
|
|
1783
|
+
checkParts.push(criterionMatrixHtml(proof.matrix, { runId, links: proof.matrixLinks, fileAnchors: detail.fileAnchors, verdict: proof.verdict,
|
|
1784
|
+
release: current?.receipt == null ? undefined : checkBackingOf(current.receipt.checks) }));
|
|
1778
1785
|
}
|
|
1779
1786
|
checkParts.push(semanticCoverageHtml(proof.matrix, proof.qualityMode));
|
|
1780
1787
|
if (reviewConflict(proof.matrix, proof.machineVerdict, proof.verdict))
|
|
@@ -1930,11 +1937,18 @@ export function personCheckItems(proof, patchText, shots, runId) {
|
|
|
1930
1937
|
};
|
|
1931
1938
|
});
|
|
1932
1939
|
}
|
|
1940
|
+
/** The completion blocker no acceptance resolves (a failed check, an unresolved HIGH finding), in words, or null. */
|
|
1941
|
+
export function hardBlockerOf(assignment) {
|
|
1942
|
+
return assignment?.readiness?.blockers.find(one => one.key === "check-failed" || one.key === "high")?.message ?? null;
|
|
1943
|
+
}
|
|
1933
1944
|
/** A Needs you result's one action (needs-you.ts): a link to the act that resolves it, or Confirm it
|
|
1934
1945
|
* stopped behind the password. Null under every other headline. */
|
|
1935
1946
|
export function needActionOf(assignment, status, csrf, returnTo, result) {
|
|
1936
1947
|
if (assignment === null || status === null || status.headline !== "Needs you" || status.need == null)
|
|
1937
1948
|
return null;
|
|
1949
|
+
// Accepting can't resolve it: Request changes is the way on, beside the reason.
|
|
1950
|
+
if (result !== undefined && hardBlockerOf(assignment) !== null)
|
|
1951
|
+
return null;
|
|
1938
1952
|
const action = assignment.primaryAction;
|
|
1939
1953
|
const rebuild = { href: null, confirm: null, rebuild: { action: `${taskHref(assignment.rootId)}/requeue` } };
|
|
1940
1954
|
// Built to an earlier plan: Build again, in place (the task page's requeue).
|
|
@@ -79,9 +79,6 @@ edge("remote", "edge.oauth-authorize", "GET,HEAD", "/oauth/authorize");
|
|
|
79
79
|
edge("remote", "edge.oauth-consent", "POST", "/oauth/authorize", { callers: ["cookie"], scope: "step-up", viewer: true, proof: "adapter" });
|
|
80
80
|
edge("settings", "edge.google-callback", "GET", "/settings/google/callback", { callers: ["service"], proof: "adapter" });
|
|
81
81
|
edge("settings", "edge.connect-callback", "GET", "/settings/tools/connected", { callers: ["service"], proof: "adapter" });
|
|
82
|
-
for (const operation of ["list", "show", "changes", "start", "send", "stop", "resume", "recover"]) {
|
|
83
|
-
edge("remote", `edge.sessions-${operation}`, "POST", `/api/sessions/${operation}`, { callers: ["bearer"], scope: ["list", "show", "changes"].includes(operation) ? "read" : "act", project: "adapter", proof: "adapter" });
|
|
84
|
-
}
|
|
85
82
|
edge("remote", "edge.team-read", "GET", "/api/team", { callers: ["cookie", "bearer"], scope: "read", project: "adapter", proof: "adapter" });
|
|
86
83
|
edge("remote", "edge.team-send", "POST", "/api/team", { callers: ["cookie", "bearer"], scope: "read", project: "adapter", proof: "adapter" });
|
|
87
84
|
edge("remote", "edge.cli", "POST", "/api/cli", { callers: ["bearer"], scope: "read", project: "adapter", proof: "adapter" });
|
package/dist/server/tasks.js
CHANGED
|
@@ -165,8 +165,8 @@ export function createTasksHandlers(runtime) {
|
|
|
165
165
|
// Console v2: results ready to review and work running now, from the same admitted work index.
|
|
166
166
|
...(() => {
|
|
167
167
|
const row = (one) => ({ taskId: one.rootId, title: one.title, detail: one.status.detail, repo: one.repo });
|
|
168
|
-
const needsYou = workIndexPage(store, now, workAccess(), { view: "needs-you", limit: 40, project });
|
|
169
|
-
const runningNow = workIndexPage(store, now, workAccess(), { view: "running", limit: 40, project });
|
|
168
|
+
const needsYou = workIndexPage(store, now, workAccess(), { view: "needs-you", limit: 40, project, root: evidenceRoot });
|
|
169
|
+
const runningNow = workIndexPage(store, now, workAccess(), { view: "running", limit: 40, project, root: evidenceRoot });
|
|
170
170
|
return { ready: needsYou.items.filter(one => one.assignmentState === "ready-to-check").map(row), running: runningNow.items.map(row) };
|
|
171
171
|
})(),
|
|
172
172
|
};
|
|
@@ -195,7 +195,7 @@ export function createTasksHandlers(runtime) {
|
|
|
195
195
|
const rollup = project === null && !unscopedMode;
|
|
196
196
|
let work;
|
|
197
197
|
try {
|
|
198
|
-
work = workIndexPage(store, now, workAccess(), { view, limit: 40, cursor: url.searchParams.get('cursor'), project });
|
|
198
|
+
work = workIndexPage(store, now, workAccess(), { view, limit: 40, cursor: url.searchParams.get('cursor'), project, root: evidenceRoot });
|
|
199
199
|
const facts = requestContext.getStore();
|
|
200
200
|
if (facts !== undefined) {
|
|
201
201
|
// These counts cover the exact admitted project lens, including
|
|
@@ -1382,7 +1382,7 @@ export function createTasksHandlers(runtime) {
|
|
|
1382
1382
|
// CI repair, suggestion-first (M8.18): a red episode never spawns an
|
|
1383
1383
|
// agent by itself — it EARNS a button, and the button creates one
|
|
1384
1384
|
// unapproved task through the same revision machinery as review
|
|
1385
|
-
// comments.
|
|
1385
|
+
// comments. One draft per task/PR, ever: the merge blocker records it.
|
|
1386
1386
|
const id = Number(draftRepair[1]);
|
|
1387
1387
|
const found = store.getRun(id);
|
|
1388
1388
|
if (found === null || !visible(taskRepoOf(found.taskRef))) {
|
|
@@ -1401,10 +1401,6 @@ export function createTasksHandlers(runtime) {
|
|
|
1401
1401
|
// failure was SEEN on and when (audit C-2) — a PR that advanced since
|
|
1402
1402
|
// is a different failure, and the click time is not an observation.
|
|
1403
1403
|
const episode = store.latestOpenCiEpisode(publication.githubRepo, publication.prNumber);
|
|
1404
|
-
// Suffixes survive truncation (audit C-7): the prefix gives way, the
|
|
1405
|
-
// identity-bearing tail never does.
|
|
1406
|
-
const suffix = `-ci-${publication.prNumber}`;
|
|
1407
|
-
const draftId = `${sourceTaskId.slice(0, 64 - suffix.length)}${suffix}`;
|
|
1408
1404
|
const brief = {
|
|
1409
1405
|
schema: 1,
|
|
1410
1406
|
kind: "ci-repair",
|
|
@@ -1425,25 +1421,32 @@ export function createTasksHandlers(runtime) {
|
|
|
1425
1421
|
// repair use (contract handoff task 2): the inherited terms come from
|
|
1426
1422
|
// the source rows inside the seal; the scope read above only names
|
|
1427
1423
|
// the digest this draft was composed against.
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1424
|
+
// One draft per task/PR, ever: the PR's merge blocker names it, lifted or not, and is written in the seal's transaction.
|
|
1425
|
+
const sealed = store.transact(() => {
|
|
1426
|
+
const prior = store.raw().prepare("SELECT task_id FROM merge_blocker WHERE publication = ? AND task_id IS NOT NULL ORDER BY rowid LIMIT 1").get(publication.id);
|
|
1427
|
+
if (prior !== undefined)
|
|
1428
|
+
return { ok: false, reason: "duplicate", detail: String(prior["task_id"]) };
|
|
1429
|
+
const result = store.sealRevision({
|
|
1430
|
+
source: { task: sourceTaskId, run: id, scopeDigest: sourceScope?.digest ?? null },
|
|
1431
|
+
brief: { evidenceRoot, key, sha256: createHash("sha256").update(briefBytes).digest("hex"), bytes: briefBytes.length, capture: "machine-authored ci-repair brief (exit 0)" },
|
|
1432
|
+
child: {
|
|
1433
|
+
title: `repair ${sourceTaskId}: CI failing on PR #${publication.prNumber}`,
|
|
1434
|
+
repair: `repair the failing CI on PR #${publication.prNumber} (failing head ${(episode?.headSha ?? publication.headSha).slice(0, 12)}). ` +
|
|
1435
|
+
`Read the failing checks on GitHub before approving; this draft carries no log content.`,
|
|
1436
|
+
},
|
|
1437
|
+
commentIds: null,
|
|
1438
|
+
requestedBy: who.name,
|
|
1439
|
+
}, now);
|
|
1440
|
+
// The merge blocker rides the SAME breath as the draft (merge grant,
|
|
1441
|
+
// findings 3/12/13): while this repair exists, the source PR merges
|
|
1442
|
+
// NOTHING — sticky until the operator's unblock act or the PR closes.
|
|
1443
|
+
if (result.ok)
|
|
1444
|
+
store.createMergeBlocker(publication.id, result.id, now);
|
|
1445
|
+
return result;
|
|
1446
|
+
});
|
|
1440
1447
|
if (!sealed.ok) {
|
|
1441
|
-
return refuse(response, who, sealed.reason === "duplicate" ? 409 : sealed.reason === "stale-source" || sealed.reason === "comments-taken" ? 409 : 400, sealed.reason === "duplicate" ? `already drafted as ${
|
|
1448
|
+
return refuse(response, who, sealed.reason === "duplicate" ? 409 : sealed.reason === "stale-source" || sealed.reason === "comments-taken" ? 409 : 400, sealed.reason === "duplicate" ? `already drafted as ${sealed.detail}` : `could not draft: ${sealed.detail}`, `/r/${id}`);
|
|
1442
1449
|
}
|
|
1443
|
-
// The merge blocker rides the SAME breath as the draft (merge grant,
|
|
1444
|
-
// findings 3/12/13): while this repair exists, the source PR merges
|
|
1445
|
-
// NOTHING — sticky until the operator's unblock act or the PR closes.
|
|
1446
|
-
store.createMergeBlocker(publication.id, sealed.id, now);
|
|
1447
1450
|
return redirect(response, taskHref(sealed.id));
|
|
1448
1451
|
}
|
|
1449
1452
|
async function resolveIncident(ctx) {
|
package/dist/session-cli.d.ts
CHANGED
|
@@ -1,13 +1,42 @@
|
|
|
1
|
-
import { type SessionDescriptor, type
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { type SessionDescriptor, type SessionRequest, type SessionResponse } from './session-contract.js';
|
|
2
|
+
import { type TeamCliOptions } from './team-cli.js';
|
|
3
|
+
/**
|
|
4
|
+
* `toolroll session …` (D7): native session operations run on the central server over `POST /api/cli`, signed in with
|
|
5
|
+
* the API token `toolroll connect` saved, like every other remote command. The client checks and sends the exact
|
|
6
|
+
* request; the server (session-remote.ts) runs it through the existing session owner and answers one session envelope.
|
|
7
|
+
*/
|
|
8
|
+
export type SessionCliOptions = TeamCliOptions & {
|
|
5
9
|
readStdin?: () => Promise<string>;
|
|
6
10
|
stderr?: (line: string) => void;
|
|
7
11
|
};
|
|
8
|
-
|
|
9
|
-
export declare
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
/** The files key the prompt travels under: the server reads no paths, only what the request carries. */
|
|
13
|
+
export declare const SESSION_PROMPT_FILE = "prompt";
|
|
14
|
+
export declare const SESSION_CLI_ACTIONS: readonly ["capabilities", ...import("./session-contract.js").SessionOperation[]];
|
|
15
|
+
export declare const SESSION_TOKEN_REQUIRED = "Session commands use your saved API token. Create an API token with `toolroll tokens create`, then save it with `toolroll connect <origin> --as <account> --token-stdin`.";
|
|
16
|
+
/** The flags an operation takes: on this computer (where the prompt may come from a file or stdin, and a saved
|
|
17
|
+
* profile is chosen), or on the server (where the prompt is only ever the request's own file). */
|
|
18
|
+
export declare function sessionCliFlags(spec: SessionDescriptor, side?: 'client' | 'server'): Record<string, 'value' | 'flag'>;
|
|
19
|
+
export type SessionArguments = {
|
|
20
|
+
spec: SessionDescriptor;
|
|
21
|
+
flags: Record<string, string | true>;
|
|
22
|
+
positionals: string[];
|
|
23
|
+
};
|
|
24
|
+
/** One parser for both sides of /api/cli: `<operation> [<session-id>] --flag value …`. */
|
|
25
|
+
export declare function parseSessionArguments(argv: readonly string[], side: 'client' | 'server'): SessionArguments | {
|
|
26
|
+
problem: string;
|
|
27
|
+
};
|
|
28
|
+
/** The validated request the parsed arguments and prompt spell. */
|
|
29
|
+
export declare function sessionRequestOf({ spec, flags, positionals }: SessionArguments, prompt: string | undefined): {
|
|
30
|
+
ok: true;
|
|
31
|
+
request: SessionRequest;
|
|
32
|
+
} | {
|
|
33
|
+
ok: false;
|
|
34
|
+
message: string;
|
|
35
|
+
};
|
|
36
|
+
/** The exact argv the server runs for a validated request: always --json, the prompt as the request's one file. */
|
|
37
|
+
export declare function sessionArgv(spec: SessionDescriptor, request: SessionRequest): string[];
|
|
38
|
+
/** The plain-text answer, the same on either side of the transport. */
|
|
39
|
+
export declare function renderSessionResponse(response: SessionResponse): string;
|
|
40
|
+
/** 0 delivered · 1 unknown (inspect first) · 3 refused. */
|
|
41
|
+
export declare const sessionExitCode: (response: SessionResponse) => number;
|
|
13
42
|
export declare function runSessionCommand(argv: readonly string[], write: (line: string) => void, options?: SessionCliOptions): Promise<number>;
|