toolroll 0.9.5 → 0.9.7
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 +49 -0
- package/dist/actor.d.ts +21 -0
- package/dist/actor.js +48 -0
- package/dist/assignment-adapters.d.ts +1 -0
- package/dist/assignment-presentation.d.ts +13 -2
- package/dist/assignment-presentation.js +22 -9
- package/dist/assignment-ui.d.ts +10 -15
- package/dist/assignment-ui.js +19 -18
- package/dist/assignment.d.ts +9 -0
- package/dist/assignment.js +62 -13
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +29 -24
- package/dist/browser-workspace.d.ts +120 -12
- package/dist/builder.d.ts +1 -0
- package/dist/builder.js +33 -13
- package/dist/chat-acceptance.js +2 -2
- package/dist/chat-actions.js +2 -1
- package/dist/chat-delivery.js +2 -0
- package/dist/chat-quiet.js +30 -19
- package/dist/chat-task-actions.d.ts +13 -0
- package/dist/chat-task-actions.js +19 -1
- package/dist/check-levels-ui.d.ts +21 -0
- package/dist/check-levels-ui.js +70 -0
- package/dist/check-levels.d.ts +67 -0
- package/dist/check-levels.js +171 -0
- package/dist/claim.d.ts +22 -0
- package/dist/claim.js +43 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +4 -0
- package/dist/demo-chat.d.ts +0 -5
- package/dist/demo-chat.js +14 -3
- package/dist/dispatch.js +7 -2
- package/dist/dispose.d.ts +8 -0
- package/dist/dispose.js +7 -1
- package/dist/export.js +3 -2
- package/dist/first-run.d.ts +34 -0
- package/dist/first-run.js +37 -1
- package/dist/flow-gallery-ui.d.ts +30 -0
- package/dist/flow-gallery-ui.js +85 -0
- package/dist/flow-gallery.d.ts +111 -0
- package/dist/flow-gallery.js +445 -0
- package/dist/flow-share.d.ts +102 -0
- package/dist/flow-share.js +614 -0
- package/dist/flow-starters.d.ts +2 -0
- package/dist/flow-starters.js +1 -1
- package/dist/flow-steps.js +7 -0
- package/dist/flow-triggers.js +2 -0
- package/dist/flows-cli.d.ts +56 -0
- package/dist/flows-cli.js +109 -3
- package/dist/flows-ui.d.ts +15 -1
- package/dist/flows-ui.js +31 -6
- package/dist/guides.js +27 -2
- package/dist/lead-context.js +6 -2
- package/dist/lead-status.d.ts +7 -0
- package/dist/lead-status.js +11 -2
- package/dist/mate-doors.d.ts +0 -15
- package/dist/mate-doors.js +5 -1
- package/dist/mate-tools.js +9 -4
- package/dist/models-ui.js +1 -1
- package/dist/operate.d.ts +4 -1
- package/dist/operate.js +601 -85
- package/dist/project-concurrency.d.ts +62 -0
- package/dist/project-concurrency.js +116 -0
- package/dist/project-delete-ui.d.ts +7 -0
- package/dist/project-delete-ui.js +19 -2
- package/dist/project-delete.js +4 -0
- package/dist/pull-request-flow.d.ts +9 -0
- package/dist/pull-request-flow.js +21 -3
- package/dist/result-follow-ups.d.ts +101 -0
- package/dist/result-follow-ups.js +333 -0
- package/dist/scope.js +11 -0
- package/dist/serve.d.ts +211 -2
- package/dist/serve.js +1070 -204
- package/dist/store.d.ts +76 -3
- package/dist/store.js +311 -22
- package/dist/surface.js +31 -8
- package/dist/tailnet.d.ts +11 -0
- package/dist/tailnet.js +47 -0
- package/dist/task-outcome-cli.js +2 -2
- package/dist/task-status.d.ts +186 -0
- package/dist/task-status.js +335 -0
- package/dist/team-cli.js +3 -0
- package/dist/telegram-progress.js +62 -100
- package/dist/telegram-status.js +21 -8
- package/dist/verification-evidence.js +21 -1
- package/dist/webhooks.js +5 -0
- package/dist/work-index.js +60 -11
- package/dist/work-summary.d.ts +0 -4
- package/dist/work-summary.js +2 -0
- package/dist/workspace-ui.d.ts +10 -0
- package/dist/workspace-ui.js +33 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,55 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.9.7 — 2026-10-01
|
|
6
|
+
|
|
7
|
+
- **Onboarding without a password or a form.** `toolroll up` prints three
|
|
8
|
+
lines and opens the console already signed in, through a one-time link that
|
|
9
|
+
works once, for ten minutes, from this computer only. The lead turns on by
|
|
10
|
+
itself with the agent that's signed in (Claude Code, else Codex); the old
|
|
11
|
+
setup form moved to Settings → Lead → Advanced. With no agent signed in,
|
|
12
|
+
Chat shows the command to install and sign in, and checks again on its own.
|
|
13
|
+
A short "how it works", a live Plan → Ready timeline on your first task, a
|
|
14
|
+
card for pairing your phone, and a "wrong host" page that says what to run.
|
|
15
|
+
- **The task page is one conversation.** The status stays on top; below it
|
|
16
|
+
the plan, the agent's notes and results, questions and your replies, in
|
|
17
|
+
order, ending in a box to message the agent (Build, Plan only, or Just
|
|
18
|
+
answer). Details (scope, attempts, audit) moved to a side panel, or a sheet
|
|
19
|
+
on phones. The Chat home shows who is working now and four counts, and
|
|
20
|
+
Catch up and the Inbox have tabs: Needs you, Ready, Running, All.
|
|
21
|
+
- **Checks: quick while building, easy to turn off.** Each project has a
|
|
22
|
+
check level: Quick (the default for new projects), Full or Off, and a task
|
|
23
|
+
can override it ("skip the tests"). With checks Off a result is still
|
|
24
|
+
Ready for review, and says no check ran. Any result can run its checks again or file a task to add tests.
|
|
25
|
+
- **A flow gallery, and flows as files.** Flows → New is a gallery of
|
|
26
|
+
templates grouped by what they're for, with new ones for developers
|
|
27
|
+
(Overnight bug bash, Flaky test hunter, Release notes writer, Docs follow
|
|
28
|
+
the code, Error to fix, PR second opinion and more). Export any flow as a
|
|
29
|
+
`.toolroll-flow.json` file and import one from a file or a link, with a
|
|
30
|
+
preview; secrets never leave, and imported triggers start switched off.
|
|
31
|
+
- **Several builds at once on one project.** The service builds up to two
|
|
32
|
+
tasks of a project at a time (Settings → Projects, `toolroll project
|
|
33
|
+
concurrency`), within the machine's limit. Stopping the service mid-build
|
|
34
|
+
saves the work and resumes it next time.
|
|
35
|
+
- **The lead's housekeeping stays quiet.** An agent acting as your lead gets
|
|
36
|
+
its own credential (`toolroll lead token`); its work pings nobody unless it
|
|
37
|
+
hands something to you. Your own actions never ping you, a replaced task
|
|
38
|
+
reads "Replaced by", and a whole project can be muted.
|
|
39
|
+
- **A faster release check.** Flow journeys run in parallel groups, browser
|
|
40
|
+
journeys are rebalanced, and a version-only bump no longer forces every
|
|
41
|
+
test.
|
|
42
|
+
|
|
43
|
+
## 0.9.6 — 2026-10-01
|
|
44
|
+
|
|
45
|
+
- **Every task's status, clear at a glance.** One headline from a fixed set
|
|
46
|
+
(Queued, Planning, Needs you, Building, Ready for review, Complete, Failed,
|
|
47
|
+
Stopped) and one plain sentence, the same words on the Tasks list, the task
|
|
48
|
+
page, the result, Crew, the Telegram/Slack/Discord/Teams card and
|
|
49
|
+
`toolroll status`. Checks, the pull request, requirements and saved
|
|
50
|
+
evidence sit underneath as quiet rows. A pull request that couldn't open
|
|
51
|
+
is an amber note with one action, never a red chip beside Complete: red
|
|
52
|
+
now means only Failed. The exact technical reason stays under Details.
|
|
53
|
+
|
|
5
54
|
## 0.9.5 — 2026-10-01
|
|
6
55
|
|
|
7
56
|
- **A demo you can feel in 90 seconds.** In `npx toolroll demo`, Chat has a
|
package/dist/actor.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type Actor = {
|
|
2
|
+
account: string;
|
|
3
|
+
lead: boolean;
|
|
4
|
+
};
|
|
5
|
+
/** Run `fn` with this actor (or an empty slot a verified sign-in may fill). */
|
|
6
|
+
export declare function withActor<T>(actor: Actor | null, fn: () => T): T;
|
|
7
|
+
export declare function currentActor(): Actor | null;
|
|
8
|
+
/** A person verified their password inside this command: their acts are now theirs. A lead keeps its own label. */
|
|
9
|
+
export declare function claimActor(account: string): void;
|
|
10
|
+
/** How the ledger names an actor. */
|
|
11
|
+
export declare function actorLabel(actor: Actor): string;
|
|
12
|
+
export declare function mintLeadToken(): {
|
|
13
|
+
id: string;
|
|
14
|
+
token: string;
|
|
15
|
+
hash: string;
|
|
16
|
+
};
|
|
17
|
+
export declare function parseLeadToken(presented: string): {
|
|
18
|
+
id: string;
|
|
19
|
+
secret: string;
|
|
20
|
+
} | null;
|
|
21
|
+
export declare function leadSecretMatches(secret: string, keptHash: string): boolean;
|
package/dist/actor.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who is acting right now, for pings that follow responsibility.
|
|
3
|
+
*
|
|
4
|
+
* A lead is an agent acting for one person under a lead token (`toolroll lead
|
|
5
|
+
* token`): it signs in as that person, and its acts are recorded as "lead for
|
|
6
|
+
* <owner>". A person acting for themselves is recorded by name. Work whose
|
|
7
|
+
* actor is the lead pings nobody, and nobody is pinged about their own act.
|
|
8
|
+
*
|
|
9
|
+
* The actor is held for one command or one console request, never for a
|
|
10
|
+
* long-running service: a worker's facts are nobody's act.
|
|
11
|
+
*/
|
|
12
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
13
|
+
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
14
|
+
/** The slot is mutable so a command can name its person after it has verified their password. */
|
|
15
|
+
const slot = new AsyncLocalStorage();
|
|
16
|
+
/** Run `fn` with this actor (or an empty slot a verified sign-in may fill). */
|
|
17
|
+
export function withActor(actor, fn) {
|
|
18
|
+
return slot.run({ actor }, fn);
|
|
19
|
+
}
|
|
20
|
+
export function currentActor() {
|
|
21
|
+
return slot.getStore()?.actor ?? null;
|
|
22
|
+
}
|
|
23
|
+
/** A person verified their password inside this command: their acts are now theirs. A lead keeps its own label. */
|
|
24
|
+
export function claimActor(account) {
|
|
25
|
+
const held = slot.getStore();
|
|
26
|
+
if (held !== undefined && held.actor === null)
|
|
27
|
+
held.actor = { account, lead: false };
|
|
28
|
+
}
|
|
29
|
+
/** How the ledger names an actor. */
|
|
30
|
+
export function actorLabel(actor) {
|
|
31
|
+
return actor.lead ? `lead for ${actor.account}` : actor.account;
|
|
32
|
+
}
|
|
33
|
+
// ---- the lead token ---------------------------------------------------------
|
|
34
|
+
/** `lt_<id>_<secret>`: shown once, only its hash kept. */
|
|
35
|
+
const SHAPE = /^lt_([a-f0-9]{12})_([A-Za-z0-9_-]{43})$/;
|
|
36
|
+
export function mintLeadToken() {
|
|
37
|
+
const id = randomBytes(6).toString("hex"), secret = randomBytes(32).toString("base64url");
|
|
38
|
+
return { id, token: `lt_${id}_${secret}`, hash: hashLeadSecret(secret) };
|
|
39
|
+
}
|
|
40
|
+
const hashLeadSecret = (secret) => createHash("sha256").update(secret, "utf8").digest("hex");
|
|
41
|
+
export function parseLeadToken(presented) {
|
|
42
|
+
const match = SHAPE.exec(presented.trim());
|
|
43
|
+
return match === null ? null : { id: match[1], secret: match[2] };
|
|
44
|
+
}
|
|
45
|
+
export function leadSecretMatches(secret, keptHash) {
|
|
46
|
+
const a = Buffer.from(hashLeadSecret(secret), "hex"), b = Buffer.from(keptHash, "hex");
|
|
47
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
48
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* checks, approvals, ownership, or the exact completion identity. */
|
|
4
4
|
import type { AssignmentSnapshot } from './assignment.js';
|
|
5
5
|
import { type DisplayStatus, type WorkStatus } from './workspace-ui.js';
|
|
6
|
+
import { type PullRequestFact, type StatusAction, type TaskStatus, type TaskStatusFacts } from './task-status.js';
|
|
6
7
|
export type AssignmentWorkStatus = DisplayStatus & Partial<Pick<WorkStatus, 'views' | 'rank'>>;
|
|
7
8
|
export type AssignmentAttention = {
|
|
8
9
|
id: string;
|
|
@@ -14,12 +15,22 @@ export type AssignmentAttention = {
|
|
|
14
15
|
export declare function historicalAssessmentReason(reason: string): boolean;
|
|
15
16
|
/** Known storage-cap notices do not mean the retained bytes are damaged. */
|
|
16
17
|
export declare function shortenedMaterialReason(detail: string): boolean;
|
|
17
|
-
export
|
|
18
|
+
export type AssignmentStatusOptions = {
|
|
18
19
|
workStatus?: AssignmentWorkStatus;
|
|
19
20
|
diagnostics?: WorkStatus['diagnostics'];
|
|
20
21
|
additionalAttention?: readonly string[];
|
|
21
|
-
|
|
22
|
+
/** A live planner rather than a builder holds the work. */
|
|
23
|
+
planning?: boolean;
|
|
24
|
+
pullRequest?: PullRequestFact | null;
|
|
25
|
+
action?: StatusAction | null;
|
|
26
|
+
links?: TaskStatusFacts['links'];
|
|
27
|
+
evidence?: TaskStatusFacts['evidence'];
|
|
28
|
+
};
|
|
29
|
+
/** The one shared status (task-status.ts) for this assignment. */
|
|
30
|
+
export declare function assignmentTaskStatusOf(assignment: AssignmentSnapshot, options?: AssignmentStatusOptions): TaskStatus;
|
|
31
|
+
export declare function assignmentPresentationOf(assignment: AssignmentSnapshot, options?: AssignmentStatusOptions): {
|
|
22
32
|
status: WorkStatus;
|
|
33
|
+
taskStatus: TaskStatus;
|
|
23
34
|
primaryAction: import("./work-summary.js").WorkAction | null;
|
|
24
35
|
attention: AssignmentAttention[];
|
|
25
36
|
diagnostics: Pick<DisplayStatus, "detail" | "label" | "token" | "tone">[];
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { GOAL_ASSESSMENT_PENDING, manualReviewCriterionOf } from './proof.js';
|
|
2
2
|
import { failedCheckExit } from './workspace-ui.js';
|
|
3
|
-
|
|
4
|
-
working: 'Working', checking: 'Checking', 'needs-decision': 'Needs your decision',
|
|
5
|
-
'ready-to-check': 'Ready', complete: 'Complete', cancelled: 'Cancelled',
|
|
6
|
-
};
|
|
3
|
+
import { assignmentStatusFacts, taskStatusOf, workToneOf } from './task-status.js';
|
|
7
4
|
/** Only the retired assessment requirement is history-only. Specific missing
|
|
8
5
|
* material, defects and failed checks remain visible, including reviewer findings. */
|
|
9
6
|
export function historicalAssessmentReason(reason) {
|
|
@@ -19,13 +16,29 @@ export function shortenedMaterialReason(detail) {
|
|
|
19
16
|
detail === 'The changed-file list was cut short; the counts are complete.' ||
|
|
20
17
|
/^Saved (?:terminal-diff|report) #\d+ is incomplete\.$/.test(detail);
|
|
21
18
|
}
|
|
19
|
+
/** The one shared status (task-status.ts) for this assignment. */
|
|
20
|
+
export function assignmentTaskStatusOf(assignment, options = {}) {
|
|
21
|
+
const work = options.workStatus;
|
|
22
|
+
return taskStatusOf(assignmentStatusFacts(assignment, {
|
|
23
|
+
work: work === undefined ? null : { token: work.token, ...(work.views === undefined ? {} : { views: work.views }), detail: work.detail },
|
|
24
|
+
...(options.planning === undefined ? {} : { planning: options.planning }),
|
|
25
|
+
...(options.pullRequest === undefined ? {} : { pullRequest: options.pullRequest }),
|
|
26
|
+
...(options.action === undefined ? {} : { action: options.action }),
|
|
27
|
+
...(options.links === undefined ? {} : { links: options.links }),
|
|
28
|
+
...(options.evidence === undefined ? {} : { evidence: options.evidence }),
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
22
31
|
export function assignmentPresentationOf(assignment, options = {}) {
|
|
23
32
|
const { state } = assignment, work = options.workStatus;
|
|
33
|
+
const taskStatus = assignmentTaskStatusOf(assignment, options);
|
|
34
|
+
// The headline and its sentence are the shared words; the token, views and
|
|
35
|
+
// rank stay the existing projection's (pages and tests key off them).
|
|
36
|
+
const shared = { label: taskStatus.headline, tone: workToneOf(taskStatus.headline), detail: taskStatus.sentence, action: null };
|
|
37
|
+
const needsYou = taskStatus.headline === 'Needs you';
|
|
24
38
|
const status = state === 'working'
|
|
25
|
-
? { ...
|
|
26
|
-
|
|
27
|
-
: { token: `assignment-${state}`,
|
|
28
|
-
action: null, tone: state === 'needs-decision' ? 'attention' : state === 'checking' ? work?.tone ?? 'live' : state === 'ready-to-check' ? 'ready' : state === 'complete' ? 'done' : 'muted',
|
|
39
|
+
? { ...shared, token: `assignment-${state}`,
|
|
40
|
+
views: [...new Set([...(work?.views?.filter(view => view !== 'completed') ?? ['all']), ...(needsYou ? ['needs-you'] : [])])], rank: needsYou ? 0 : work?.rank ?? 2 }
|
|
41
|
+
: { ...shared, token: `assignment-${state}`,
|
|
29
42
|
views: state === 'ready-to-check' || state === 'needs-decision' ? ['all', 'needs-you'] : state === 'complete' ? ['all', 'completed']
|
|
30
43
|
: state === 'checking' && work?.views?.includes('running') ? ['all', 'running'] : ['all'],
|
|
31
44
|
rank: state === 'needs-decision' || state === 'ready-to-check' ? 0 : state === 'checking' ? 1 : state === 'complete' ? 3 : 4 };
|
|
@@ -78,5 +91,5 @@ export function assignmentPresentationOf(assignment, options = {}) {
|
|
|
78
91
|
return false;
|
|
79
92
|
return true;
|
|
80
93
|
});
|
|
81
|
-
return { status, primaryAction: assignment.primaryAction, attention: [...attention.values()], diagnostics };
|
|
94
|
+
return { status, taskStatus, primaryAction: assignment.primaryAction, attention: [...attention.values()], diagnostics };
|
|
82
95
|
}
|
package/dist/assignment-ui.d.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* existing projections and signed operating-mode terms, never this renderer. */
|
|
3
3
|
import type { AssignmentSnapshot } from './assignment.js';
|
|
4
4
|
import type { DisplayStatus, StatusTone, WorkStatus } from './workspace-ui.js';
|
|
5
|
-
import { type AssignmentWorkStatus } from './assignment-presentation.js';
|
|
5
|
+
import { type AssignmentStatusOptions, type AssignmentWorkStatus } from './assignment-presentation.js';
|
|
6
|
+
import { type PullRequestFact, type TaskStatus } from './task-status.js';
|
|
6
7
|
/** The existing verified receipt reader may discover damage after a verdict
|
|
7
8
|
* was saved. Keep the limitation visible without inventing another work stage. */
|
|
8
9
|
export declare function assignmentWithEvidence(assignment: AssignmentSnapshot, resultStatus: DisplayStatus | null, resultRunId?: number | null): AssignmentSnapshot;
|
|
@@ -19,38 +20,32 @@ export type AssignmentAttemptRow = {
|
|
|
19
20
|
/** Every version of the assignment, oldest first, as the card lists them. */
|
|
20
21
|
export declare function assignmentAttemptsOf(assignment: AssignmentSnapshot): AssignmentAttemptRow[];
|
|
21
22
|
export declare function assignmentAttemptsHtml(assignment: AssignmentSnapshot): string;
|
|
22
|
-
type AssignmentCardOptions = {
|
|
23
|
+
type AssignmentCardOptions = Omit<AssignmentStatusOptions, 'links'> & {
|
|
23
24
|
hideAction?: boolean;
|
|
24
25
|
problem?: boolean;
|
|
25
|
-
diagnostics?: WorkStatus['diagnostics'];
|
|
26
|
-
workStatus?: AssignmentWorkStatus;
|
|
27
26
|
resultHref?: string;
|
|
27
|
+
pullRequest?: PullRequestFact | null;
|
|
28
28
|
};
|
|
29
29
|
/** The status card's content: one projection for the server HTML and the
|
|
30
|
-
* rebuilt task page, so the two never word the same state differently.
|
|
30
|
+
* rebuilt task page, so the two never word the same state differently. The
|
|
31
|
+
* headline, sentence and detail rows are the shared task status; the exact
|
|
32
|
+
* recorded reasons stay one tap away under Details, never as red text. */
|
|
31
33
|
export type AssignmentCard = {
|
|
32
34
|
token: string;
|
|
33
35
|
tone: StatusTone;
|
|
34
36
|
label: string;
|
|
37
|
+
status: TaskStatus;
|
|
35
38
|
action: {
|
|
36
39
|
label: string;
|
|
37
40
|
href: string;
|
|
38
41
|
openResult: boolean;
|
|
39
42
|
} | null;
|
|
40
|
-
/**
|
|
41
|
-
|
|
42
|
-
by: string | null;
|
|
43
|
-
} | null;
|
|
44
|
-
detail: {
|
|
45
|
-
text: string;
|
|
46
|
-
problem: boolean;
|
|
47
|
-
};
|
|
48
|
-
problems: string[];
|
|
43
|
+
/** Exact technical reasons and diagnostics, shown on request. */
|
|
44
|
+
reasons: string[];
|
|
49
45
|
diagnostics: {
|
|
50
46
|
token: string;
|
|
51
47
|
label: string;
|
|
52
48
|
detail: string;
|
|
53
|
-
problem: boolean;
|
|
54
49
|
}[];
|
|
55
50
|
notices: {
|
|
56
51
|
summary: string;
|
package/dist/assignment-ui.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { assignmentPresentationOf, shortenedMaterialReason } from './assignment-presentation.js';
|
|
2
2
|
import { plainReasonWords } from './proof.js';
|
|
3
|
+
import { chatResultHref } from './chat-controls.js';
|
|
4
|
+
import { statusDetailsHtml, statusWhyHtml } from './task-status.js';
|
|
3
5
|
const escape = (value) => value.replace(/[&<>"']/g, char => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[char]);
|
|
4
6
|
/** The existing verified receipt reader may discover damage after a verdict
|
|
5
7
|
* was saved. Keep the limitation visible without inventing another work stage. */
|
|
@@ -63,25 +65,25 @@ function savedMaterialNotice(detail, assignment) {
|
|
|
63
65
|
!assignment.receipt.artifacts.some(one => one.id === Number(earlier[1])) ? 'history' : null;
|
|
64
66
|
}
|
|
65
67
|
export function assignmentCardOf(assignment, options = {}) {
|
|
66
|
-
const
|
|
67
|
-
const {
|
|
68
|
+
const receipt = assignment.receipt;
|
|
69
|
+
const links = receipt === null ? {} : { result: options.resultHref ?? chatResultHref(receipt.taskId, receipt.runId), checks: chatResultHref(receipt.taskId, receipt.runId, 'checks'),
|
|
70
|
+
pullRequest: `/t/${encodeURIComponent(assignment.rootId)}#merge`,
|
|
71
|
+
// Run checks lives with the result's checks; the row's action leads there.
|
|
72
|
+
...(receipt.completionKind === 'research-report' ? {} : { runChecks: `${chatResultHref(receipt.taskId, receipt.runId, 'checks')}#follow-ups` }) };
|
|
73
|
+
const presentation = assignmentPresentationOf(assignment, { ...options, links });
|
|
74
|
+
const { status, diagnostics, taskStatus } = presentation;
|
|
68
75
|
const href = assignment.primaryAction?.code === 'open-result' && options.resultHref !== undefined ? options.resultHref : assignmentActionHref(assignment);
|
|
69
76
|
const attention = presentation.attention.map(one => one.detail);
|
|
70
77
|
const notices = [...attention, ...diagnostics.map(one => one.detail)].filter(one => savedMaterialNotice(one, assignment) !== null);
|
|
71
78
|
const partial = notices.some(one => savedMaterialNotice(one, assignment) === 'partial');
|
|
72
79
|
const history = notices.some(one => savedMaterialNotice(one, assignment) === 'history');
|
|
73
|
-
const
|
|
74
|
-
const checkProblem = ready && assignment.receipt !== null && assignment.receipt.completionKind !== 'research-report' && assignment.receipt.checks.status !== 'passed';
|
|
80
|
+
const recorded = assignment.detail === taskStatus.sentence || taskStatus.headline === 'Ready for review' || taskStatus.headline === 'Complete' ? [] : [assignment.detail];
|
|
75
81
|
return {
|
|
76
|
-
token: status.token, tone: status.tone, label: status.label,
|
|
82
|
+
token: status.token, tone: status.tone, label: status.label, status: taskStatus,
|
|
77
83
|
action: options.hideAction || href === null ? null : { label: assignment.primaryAction.label, href, openResult: options.resultHref !== undefined },
|
|
78
|
-
|
|
79
|
-
passed: ready && !checkProblem && assignment.receipt !== null && assignment.receipt.checks.status === 'passed'
|
|
80
|
-
? { by: assignment.completion === null ? null : assignment.completion.actor.replace(/^(?:operator|coordinator|lead):/, '') } : null,
|
|
81
|
-
detail: { text: assignment.detail, problem: checkProblem || options.problem === true && !ready },
|
|
82
|
-
problems: attention.filter(one => savedMaterialNotice(one, assignment) === null).map(plainReasonWords),
|
|
84
|
+
reasons: [...new Set([...recorded, ...attention.filter(one => savedMaterialNotice(one, assignment) === null).map(plainReasonWords)])].filter(one => one !== taskStatus.sentence),
|
|
83
85
|
diagnostics: diagnostics.filter(one => savedMaterialNotice(one.detail, assignment) === null)
|
|
84
|
-
.map(one => ({ token: one.token, label: one.label, detail: one.detail
|
|
86
|
+
.map(one => ({ token: one.token, label: one.label, detail: one.detail })),
|
|
85
87
|
notices: notices.length === 0 ? null : { summary: [partial ? 'Saved output is partial' : '', history ? 'Earlier material unavailable' : ''].filter(Boolean).join(' · '), lines: [...new Set(notices)] },
|
|
86
88
|
attempts: assignmentAttemptsOf(assignment),
|
|
87
89
|
lead: assignment.owner === null ? null : { label: assignment.owner.label, active: assignment.owner.active },
|
|
@@ -89,14 +91,13 @@ export function assignmentCardOf(assignment, options = {}) {
|
|
|
89
91
|
}
|
|
90
92
|
export function assignmentSummaryHtml(assignment, options = {}) {
|
|
91
93
|
const card = assignmentCardOf(assignment, options);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
+
const tone = card.status.tone;
|
|
95
|
+
return `<section class="${options.compact ? 'assignment-summary' : 'card assignment-summary'}" aria-label="assignment progress" data-assignment="${escape(assignment.rootId)}" data-work-status="${card.token}" data-tone="${card.tone}" data-headline="${escape(card.status.headline)}" data-headline-tone="${tone}"${options.compact ? '' : ' data-task-status'}>` +
|
|
96
|
+
(options.compact ? `<span class="status-line" data-work-status="${card.token}" data-tone="${card.tone}"><i class="status-dot" aria-hidden="true"></i><span class="status-label">${card.label}</span></span>` : `<h2 class="assignment-state status-headline"><i aria-hidden="true"></i>${escape(card.label)}</h2>`) +
|
|
97
|
+
`<p class="meta assignment-detail status-sentence">${escape(card.status.sentence)}</p>` +
|
|
94
98
|
(card.action === null ? '' : `<a class="${options.compact ? 'work-action' : 'button-link'}" href="${escape(card.action.href)}"${card.action.openResult ? ' data-open-result' : ''}${options.compact ? '' : ' data-primary-action'}>${escape(card.action.label)}${options.compact ? ' →' : ''}</a>`) +
|
|
95
|
-
(
|
|
96
|
-
|
|
97
|
-
: `<p class="${card.detail.problem ? 'problem' : 'meta'} assignment-detail">${escape(card.detail.text)}</p>`) +
|
|
98
|
-
card.problems.map(one => `<p class="problem">${escape(one)}</p>`).join('') +
|
|
99
|
-
card.diagnostics.map(one => `<p class="${one.problem ? 'problem' : 'meta'}" data-work-diagnostic="${escape(one.token)}">${escape(one.label)} · ${escape(one.detail)}</p>`).join('') +
|
|
99
|
+
(options.compact ? '' : statusDetailsHtml(card.status)) +
|
|
100
|
+
statusWhyHtml(card.status, card.reasons, card.diagnostics) +
|
|
100
101
|
(card.notices === null ? '' : `<details class="assignment-notices"><summary>${card.notices.summary}</summary>${card.notices.lines.map(one => `<p class="meta">${escape(one)}</p>`).join('')}</details>`) +
|
|
101
102
|
assignmentAttemptsHtml(assignment) + `</section>`;
|
|
102
103
|
}
|
package/dist/assignment.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type Store, type ProofVerdictRow, type ProofAcceptanceRow } from "./store.js";
|
|
2
2
|
import { type WorkAction, type WorkSummaryAccess } from "./work-summary.js";
|
|
3
3
|
import { type VerifiedApprover } from "./principal.js";
|
|
4
|
+
import { type CheckLevel } from "./check-levels.js";
|
|
4
5
|
export type AssignmentAccess = WorkSummaryAccess;
|
|
5
6
|
export type AssignmentOwner = {
|
|
6
7
|
kind: "coordinator" | "lead";
|
|
@@ -13,6 +14,10 @@ export type AssignmentChecks = {
|
|
|
13
14
|
command: string | null;
|
|
14
15
|
logArtifactId: number | null;
|
|
15
16
|
detail: string;
|
|
17
|
+
/** Which check this is (check-levels.ts): quick, full, or off; null before levels. */
|
|
18
|
+
level?: CheckLevel | null;
|
|
19
|
+
/** A follow-up check waiting or running on this commit. */
|
|
20
|
+
running?: "quick" | "full" | null;
|
|
16
21
|
};
|
|
17
22
|
export type AssignmentReceipt = {
|
|
18
23
|
digest: string;
|
|
@@ -70,10 +75,12 @@ export type AssignmentSnapshot = {
|
|
|
70
75
|
problem: string | null;
|
|
71
76
|
}[];
|
|
72
77
|
};
|
|
78
|
+
/** `lead`: marked complete by the person's lead (`toolroll lead token`), shown as "by the lead". */
|
|
73
79
|
completion: {
|
|
74
80
|
actor: string;
|
|
75
81
|
at: string;
|
|
76
82
|
digest: string;
|
|
83
|
+
lead?: true;
|
|
77
84
|
} | null;
|
|
78
85
|
handoff: {
|
|
79
86
|
kind: "result" | "decision" | "attention";
|
|
@@ -112,6 +119,7 @@ export declare function assignmentBrief(assignment: AssignmentSnapshot | null):
|
|
|
112
119
|
actor: string;
|
|
113
120
|
at: string;
|
|
114
121
|
digest: string;
|
|
122
|
+
lead?: true;
|
|
115
123
|
} | null;
|
|
116
124
|
result: {
|
|
117
125
|
digest: string;
|
|
@@ -189,6 +197,7 @@ export declare function assignmentUpdates(store: Store, now: Date, access: Assig
|
|
|
189
197
|
actor: string;
|
|
190
198
|
at: string;
|
|
191
199
|
digest: string;
|
|
200
|
+
lead?: true;
|
|
192
201
|
} | null;
|
|
193
202
|
result: {
|
|
194
203
|
digest: string;
|
package/dist/assignment.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* approval or proof state. Lead ownership and exact receipt acknowledgments
|
|
3
3
|
* are append-only actions; handoffs use the existing durable outbox. */
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
|
+
import { currentActor, withActor } from "./actor.js";
|
|
6
|
+
import { replacedWords } from "./task-status.js";
|
|
5
7
|
import { COMPLETION_ACTION, familyChangedFiles } from "./result-completion.js";
|
|
6
8
|
import { homedir } from "node:os";
|
|
7
9
|
import { readSchemaVersion } from "./store.js";
|
|
@@ -12,6 +14,8 @@ import { verificationEvidence } from "./verification-evidence.js";
|
|
|
12
14
|
import { reproveApprover } from "./principal.js";
|
|
13
15
|
import { noteAssignmentStatus } from "./assignment-status.js";
|
|
14
16
|
import { historicalAssessmentReason } from "./assignment-presentation.js";
|
|
17
|
+
import { runCheckLevel } from "./check-levels.js";
|
|
18
|
+
import { followUpChecksOf, withFollowUps } from "./result-follow-ups.js";
|
|
15
19
|
/** Status-first handoff for routine reads. Fetch get_assignment only when
|
|
16
20
|
* inspecting its exact evidence; do not repeat a full proof in every poll. */
|
|
17
21
|
export function assignmentBrief(assignment) {
|
|
@@ -28,6 +32,20 @@ export function assignmentBrief(assignment) {
|
|
|
28
32
|
publication: assignment.publication, deployment: assignment.deployment };
|
|
29
33
|
}
|
|
30
34
|
const digest = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
35
|
+
/** A lookup added with lead-quiet, read safely: a deploy proves completion with this code over the INSTALLED runtime's
|
|
36
|
+
* store, before this version's migration, so the method (or its table) may not exist yet. Absent reads as none. */
|
|
37
|
+
function olderStoreSafe(read, absent) {
|
|
38
|
+
try {
|
|
39
|
+
return read();
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (error instanceof TypeError || /no such (table|column)/.test(String(error)))
|
|
43
|
+
return absent;
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** A check as the receipt digest seals it: presentation-only fields (check-levels.ts) left out. */
|
|
48
|
+
const sealedChecks = ({ level: _level, running: _running, ...proof }) => proof;
|
|
31
49
|
const OWNER_ACTION = "assignment claimed";
|
|
32
50
|
const CHECK_ACTION = COMPLETION_ACTION;
|
|
33
51
|
const actorOf = (owner) => `${owner.kind}:${owner.id}`;
|
|
@@ -58,8 +76,24 @@ function ownerOf(store, rootId, repo) {
|
|
|
58
76
|
active: credential !== undefined && credential["revoked_at"] === null && repo !== null && Array.isArray(repos) && repos.includes(repo) };
|
|
59
77
|
}
|
|
60
78
|
// Report the recorded check outcome, never turn a model verdict into a check.
|
|
61
|
-
|
|
62
|
-
|
|
79
|
+
// Follow-up checks on the same commit (Run checks) upgrade a pass and show a failure.
|
|
80
|
+
function nativeChecks(store, root, runId, now) {
|
|
81
|
+
const own = buildChecks(store, root, runId);
|
|
82
|
+
const run = store.getRun(runId);
|
|
83
|
+
const followUps = followUpChecksOf(store, runId, now, root);
|
|
84
|
+
if (followUps.length === 0)
|
|
85
|
+
return own;
|
|
86
|
+
const read = withFollowUps({ status: own.status, level: own.level ?? null, exitCode: own.exitCode, head: run?.headRevision ?? null }, followUps);
|
|
87
|
+
const latest = [...followUps].reverse().find(one => one.state === "passed" || one.state === "failed");
|
|
88
|
+
const changed = read.status !== own.status || read.level !== (own.level ?? null) || read.exitCode !== own.exitCode;
|
|
89
|
+
const words = read.level === "quick" ? "Quick checks" : "Checks";
|
|
90
|
+
return { ...own, running: read.running,
|
|
91
|
+
...(changed ? { status: read.status, level: read.level, exitCode: read.exitCode, logArtifactId: latest?.logArtifactId ?? own.logArtifactId,
|
|
92
|
+
detail: read.status === "passed" ? `${words} passed.` : `${words} failed (exit ${read.exitCode}).` } : {}) };
|
|
93
|
+
}
|
|
94
|
+
function buildChecks(store, root, runId) {
|
|
95
|
+
const level = runCheckLevel(store, runId);
|
|
96
|
+
const unavailable = (detail) => ({ status: "unavailable", exitCode: null, command: null, logArtifactId: null, detail, level });
|
|
63
97
|
if (root === undefined)
|
|
64
98
|
return unavailable("Saved checks are unavailable.");
|
|
65
99
|
try {
|
|
@@ -68,11 +102,14 @@ function nativeChecks(store, root, runId) {
|
|
|
68
102
|
return unavailable(gate.problem);
|
|
69
103
|
const receipt = gate.bytes === null ? null : JSON.parse(gate.bytes);
|
|
70
104
|
if (receipt === null)
|
|
71
|
-
return { ...unavailable("No machine check is recorded."), status: "not-run" };
|
|
105
|
+
return { ...unavailable(level === "off" ? "Checks were off for this build." : "No machine check is recorded."), status: "not-run" };
|
|
72
106
|
const ran = receipt.result.ran === true, exitCode = ran ? receipt.result.exitCode : null;
|
|
107
|
+
// The sealed receipt names the grant that ran: the quick one, or the full one.
|
|
108
|
+
const sealedLevel = typeof receipt.command?.repo === "string" && receipt.command.repo.startsWith("quick:") ? "quick" : "full";
|
|
109
|
+
const words = sealedLevel === "quick" ? "Quick checks" : "Checks";
|
|
73
110
|
return { status: ran ? exitCode === 0 ? "passed" : "failed" : "not-run", exitCode,
|
|
74
|
-
command: receipt.command.command, logArtifactId: receipt.log.artifactId,
|
|
75
|
-
detail: ran ? exitCode === 0 ?
|
|
111
|
+
command: receipt.command.command, logArtifactId: receipt.log.artifactId, level: level === null ? null : sealedLevel,
|
|
112
|
+
detail: ran ? exitCode === 0 ? `${words} passed.` : `${words} failed (exit ${exitCode}).` : `${words} did not finish.` };
|
|
76
113
|
}
|
|
77
114
|
catch {
|
|
78
115
|
return unavailable("Saved checks could not be read.");
|
|
@@ -112,7 +149,7 @@ export function assignmentOf(store, taskId, now, access, root) {
|
|
|
112
149
|
const reports = artifacts.filter(a => a.kind === "report");
|
|
113
150
|
const unavailable = family.versions.flatMap(version => store.runsFor(version.refId).flatMap(run => store.artifactsFor(run.id).filter(artifact => root === undefined || !readVerifiedArtifact(root, artifact).ok)
|
|
114
151
|
.map(artifact => `Saved ${artifact.kind} #${artifact.id} (run ${run.id}) is unavailable or changed.`)));
|
|
115
|
-
const checks = result === null ? null : nativeChecks(store, root, result.id);
|
|
152
|
+
const checks = result === null ? null : nativeChecks(store, root, result.id, now);
|
|
116
153
|
const finishedBuild = result?.role === "builder" && (result.outcome === "built" || result.outcome === "no-change") &&
|
|
117
154
|
result.finishedAt !== null && /^[a-f0-9]{40}$/.test(result.headRevision ?? "");
|
|
118
155
|
// Finished work returns to its lead or user. Strict terms and previous model
|
|
@@ -133,7 +170,11 @@ export function assignmentOf(store, taskId, now, access, root) {
|
|
|
133
170
|
...artifacts.filter(a => a.kind !== "check-log" && (a.truncated || a.captureStatus === "failed")).map(a => `Saved ${a.kind} #${a.id} is ${a.captureStatus === "failed" ? "a failed capture" : "incomplete"}.`)],
|
|
134
171
|
agentReport: result.handoff, evidence: "recorded",
|
|
135
172
|
};
|
|
136
|
-
|
|
173
|
+
// The digest seals the proof, not how it is shown: the check's `level` (read from its sealed command) and `running`
|
|
174
|
+
// (a follow-up in flight) stay out of it, so a result completed before check levels keeps its completion, and a
|
|
175
|
+
// follow-up check that is still running never unseals one.
|
|
176
|
+
const sealedBody = receiptBody === null ? null : { ...receiptBody, checks: sealedChecks(receiptBody.checks) };
|
|
177
|
+
const receipt = receiptBody === null ? null : { ...receiptBody, digest: digest({ receipt: sealedBody,
|
|
137
178
|
scope: scope === null ? null : { digest: scope.digest, approved: approvalOf(scope).approved, termsProblem: scope.termsProblem ?? null },
|
|
138
179
|
versions: family.versions.map(v => ({ id: v.id, state: v.state })) }) };
|
|
139
180
|
let completion = null;
|
|
@@ -154,8 +195,10 @@ export function assignmentOf(store, taskId, now, access, root) {
|
|
|
154
195
|
state = "needs-decision";
|
|
155
196
|
else if (current.state === "cancelled") {
|
|
156
197
|
state = "cancelled";
|
|
157
|
-
|
|
158
|
-
|
|
198
|
+
const successor = olderStoreSafe(() => store.replacementOf(current.id) ?? store.replacementOf(family.root.id), null);
|
|
199
|
+
detail = successor === null ? "This assignment was cancelled." : `${replacedWords(successor)}.`;
|
|
200
|
+
primaryAction = successor === null ? { code: "inspect-task", label: "View assignment", target: { taskId: current.id, runId: null, decisionId: null }, access: "read", retry: "read-again" }
|
|
201
|
+
: { code: "inspect-task", label: `Open ${successor}`, target: { taskId: successor, runId: null, decisionId: null }, access: "read", retry: "read-again" };
|
|
159
202
|
}
|
|
160
203
|
else if (!live && processProblem !== null && result !== null) {
|
|
161
204
|
state = "needs-decision";
|
|
@@ -182,10 +225,12 @@ export function assignmentOf(store, taskId, now, access, root) {
|
|
|
182
225
|
// membership changes and ownership transfer cannot revoke past completion.
|
|
183
226
|
// The exact result digest still fences changes to work, scope and history.
|
|
184
227
|
if (checked !== undefined) {
|
|
185
|
-
|
|
228
|
+
const rootRef = store.lookupRef(family.root.id);
|
|
229
|
+
const byLead = rootRef !== null && olderStoreSafe(() => store.taskActs(rootRef.id), []).filter(one => one.act === "completed").at(-1)?.lead === true;
|
|
230
|
+
completion = { actor: String(checked["actor"]), at: String(checked["at"]), digest: receipt.digest, ...(byLead ? { lead: true } : {}) };
|
|
186
231
|
state = "complete";
|
|
187
232
|
primaryAction = { ...primaryAction, label: completionKind === "research-report" ? "Read report" : "Open result" };
|
|
188
|
-
const label = completion.actor.startsWith("operator:") ? completion.actor.slice(9) : owner && completion.actor === actorOf(owner) ? owner.label : "the previous lead";
|
|
233
|
+
const label = byLead ? "the lead" : completion.actor.startsWith("operator:") ? completion.actor.slice(9) : owner && completion.actor === actorOf(owner) ? owner.label : "the previous lead";
|
|
189
234
|
detail = completionKind === "research-report" ? `Research report checked by ${label}. No deployment is implied.`
|
|
190
235
|
: `Handled by ${label}. ${receipt.checks.detail} Publication and deployment are separate.`;
|
|
191
236
|
}
|
|
@@ -316,6 +361,9 @@ function acknowledgeCurrent(store, current, receiptDigest, actor, now, root, acc
|
|
|
316
361
|
return { ok: false, reason: "approval-rules", message: problem };
|
|
317
362
|
store.recordAction({ at: now.toISOString(), actor, repo: current.repo,
|
|
318
363
|
taskId: current.rootId, runId: receipt.runId, action: CHECK_ACTION, outcome: receiptDigest, source: "work" });
|
|
364
|
+
const ref = store.lookupRef(current.rootId);
|
|
365
|
+
if (ref !== null)
|
|
366
|
+
store.noteTaskAct(ref.id, "completed", now);
|
|
319
367
|
store.bumpWake();
|
|
320
368
|
}
|
|
321
369
|
const assignment = assignmentOf(store, current.rootId, now, access, root);
|
|
@@ -335,7 +383,8 @@ export function checkAssignment(store, taskId, receiptDigest, owner, now, root =
|
|
|
335
383
|
/** The signed-in user can mark the same exact receipt handled without taking
|
|
336
384
|
* lead ownership. This records review, never check success or new authority. */
|
|
337
385
|
export function checkAssignmentAsOperator(store, taskId, receiptDigest, who, now, root = evidenceRoot(homedir())) {
|
|
338
|
-
|
|
386
|
+
// Completing is this person's act (or their lead's): it never pings them.
|
|
387
|
+
return withActor(currentActor() ?? { account: who.name, lead: false }, () => store.transact(() => {
|
|
339
388
|
if (!reproveApprover(store, who).ok)
|
|
340
389
|
return { ok: false, reason: "unauthenticated", message: "Sign in again before marking this result complete." };
|
|
341
390
|
const access = { principal: "operator", repos: who.repos };
|
|
@@ -343,7 +392,7 @@ export function checkAssignmentAsOperator(store, taskId, receiptDigest, who, now
|
|
|
343
392
|
if (current === null || !store.accountCanAccess(who.name, current.repo))
|
|
344
393
|
return { ok: false, reason: "not-found", message: "No assignment is available in your projects." };
|
|
345
394
|
return acknowledgeCurrent(store, current, receiptDigest, `operator:${who.name}`, now, root, access);
|
|
346
|
-
});
|
|
395
|
+
}));
|
|
347
396
|
}
|
|
348
397
|
function noteAssignmentHandoff(store, assignment, now) {
|
|
349
398
|
if (!assignment.owner?.active || assignment.handoff === null || assignment.handoff.acknowledged)
|