toolroll 0.9.4 → 0.9.6

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 CHANGED
@@ -2,6 +2,34 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.9.6 — 2026-10-01
6
+
7
+ - **Every task's status, clear at a glance.** One headline from a fixed set
8
+ (Queued, Planning, Needs you, Building, Ready for review, Complete, Failed,
9
+ Stopped) and one plain sentence, the same words on the Tasks list, the task
10
+ page, the result, Crew, the Telegram/Slack/Discord/Teams card and
11
+ `toolroll status`. Checks, the pull request, requirements and saved
12
+ evidence sit underneath as quiet rows. A pull request that couldn't open
13
+ is an amber note with one action, never a red chip beside Complete: red
14
+ now means only Failed. The exact technical reason stays under Details.
15
+
16
+ ## 0.9.5 — 2026-10-01
17
+
18
+ - **A demo you can feel in 90 seconds.** In `npx toolroll demo`, Chat has a
19
+ scripted lead: ask for anything and it answers with a short plan in a
20
+ sample project; Approve builds it live to Ready with a diff, a passing
21
+ check and a screenshot, then Complete or Request changes. It never calls a
22
+ model, reaches outside or spends. The sandbox folder is now `toolroll-demo-*`.
23
+ - **Calmer first impressions.** A result in Chat shows each fact once, in
24
+ plain words ("You check this one: …" instead of criterion and evidence
25
+ terms), and a first start on Linux without process containment says so in
26
+ one sentence instead of a cgroup error.
27
+ - **Pull requests open reliably.** Each project's worker now publishes only
28
+ its own project's pull requests, under one permission lookup shared by
29
+ Complete, flows and the publisher. Before, another project's worker could
30
+ give up on a pull request that was actually opened, and a flow could file a
31
+ needless rebuild.
32
+
5
33
  ## 0.9.4 — 2026-10-01
6
34
 
7
35
  - **Flow steps act exactly once.** However many workers advance a
package/README.md CHANGED
@@ -35,8 +35,9 @@ npx toolroll up
35
35
 
36
36
  That starts the app and a builder for your projects, then opens the console
37
37
  at http://127.0.0.1:4180. Runs on macOS or Linux with Node.js 22.13+ and git.
38
- Try the seeded sandbox first with `npx toolroll demo` — it never spends and
39
- never reaches outside.
38
+ Try the sandbox first with `npx toolroll demo`: ask its scripted lead for a
39
+ change in Chat, approve the plan, and watch it land Ready. It never calls a
40
+ model, spends or reaches outside.
40
41
 
41
42
  | Install with | Command |
42
43
  |---|---|
@@ -710,8 +711,8 @@ only the reason and the act that opens it (re-file the scope, change the
710
711
  agents, refresh the routine) — the inbox row reads *needs attention* rather
711
712
  than *review & approve*, and the routine's recovery is one labelled, described
712
713
  button with nothing to type; an old approval already on a pre-routing row is
713
- grandfathered, but no new yes lands on it. The demo sandbox shows no seeded
714
- conversation: chat evidence is a real subscription-backed plane. The task page's controls offer,
714
+ grandfathered, but no new yes lands on it. In the demo sandbox, Chat is a
715
+ scripted lead that never calls a model; real chat evidence is a subscription-backed plane. The task page's controls offer,
715
716
  per role, only the agents you configured *for that role* (gemini never
716
717
  reviews; repairs stay on the build provider), a current agent the
717
718
  configuration no longer names is shown for what runs today and never offered
@@ -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 declare function assignmentPresentationOf(assignment: AssignmentSnapshot, options?: {
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
- import { GOAL_ASSESSMENT_PENDING } from './proof.js';
1
+ import { GOAL_ASSESSMENT_PENDING, manualReviewCriterionOf } from './proof.js';
2
2
  import { failedCheckExit } from './workspace-ui.js';
3
- const LABELS = {
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
- ? { ...(work ?? { label: LABELS[state], tone: 'muted' }), token: `assignment-${state}`,
26
- detail: assignment.detail, action: null, views: work?.views?.filter(view => view !== 'completed') ?? ['all'], rank: work?.rank ?? 2 }
27
- : { token: `assignment-${state}`, label: state === 'checking' ? work?.label ?? LABELS[state] : LABELS[state], detail: assignment.detail,
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 };
@@ -51,7 +64,9 @@ export function assignmentPresentationOf(assignment, options = {}) {
51
64
  };
52
65
  const attention = new Map();
53
66
  for (const detail of [...assignment.attention, ...(options.additionalAttention ?? [])]) {
54
- if (historicalAssessmentReason(detail) || detail === assignment.detail)
67
+ // A requirement only a person can confirm is not a problem: the result
68
+ // names it in plain words beside its Accept action.
69
+ if (historicalAssessmentReason(detail) || detail === assignment.detail || manualReviewCriterionOf(detail) !== null)
55
70
  continue;
56
71
  const material = materialReason(detail);
57
72
  const id = checks !== undefined && checkReason(detail) ? 'checks' : material === null ? `detail:${detail}` : `material:${material}`;
@@ -76,5 +91,5 @@ export function assignmentPresentationOf(assignment, options = {}) {
76
91
  return false;
77
92
  return true;
78
93
  });
79
- return { status, primaryAction: assignment.primaryAction, attention: [...attention.values()], diagnostics };
94
+ return { status, taskStatus, primaryAction: assignment.primaryAction, attention: [...attention.values()], diagnostics };
80
95
  }
@@ -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
- /** Ready with passing checks: a verdict chip replaces the detail sentence. */
41
- passed: {
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;
@@ -1,4 +1,7 @@
1
1
  import { assignmentPresentationOf, shortenedMaterialReason } from './assignment-presentation.js';
2
+ import { plainReasonWords } from './proof.js';
3
+ import { chatResultHref } from './chat-controls.js';
4
+ import { statusDetailsHtml, statusWhyHtml } from './task-status.js';
2
5
  const escape = (value) => value.replace(/[&<>"']/g, char => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[char]);
3
6
  /** The existing verified receipt reader may discover damage after a verdict
4
7
  * was saved. Keep the limitation visible without inventing another work stage. */
@@ -62,25 +65,23 @@ function savedMaterialNotice(detail, assignment) {
62
65
  !assignment.receipt.artifacts.some(one => one.id === Number(earlier[1])) ? 'history' : null;
63
66
  }
64
67
  export function assignmentCardOf(assignment, options = {}) {
65
- const presentation = assignmentPresentationOf(assignment, options);
66
- const { status, diagnostics } = presentation;
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
+ const presentation = assignmentPresentationOf(assignment, { ...options, links });
72
+ const { status, diagnostics, taskStatus } = presentation;
67
73
  const href = assignment.primaryAction?.code === 'open-result' && options.resultHref !== undefined ? options.resultHref : assignmentActionHref(assignment);
68
74
  const attention = presentation.attention.map(one => one.detail);
69
75
  const notices = [...attention, ...diagnostics.map(one => one.detail)].filter(one => savedMaterialNotice(one, assignment) !== null);
70
76
  const partial = notices.some(one => savedMaterialNotice(one, assignment) === 'partial');
71
77
  const history = notices.some(one => savedMaterialNotice(one, assignment) === 'history');
72
- const ready = assignment.state === "ready-to-check" || assignment.state === "complete";
73
- const checkProblem = ready && assignment.receipt !== null && assignment.receipt.completionKind !== 'research-report' && assignment.receipt.checks.status !== 'passed';
78
+ const recorded = assignment.detail === taskStatus.sentence || taskStatus.headline === 'Ready for review' || taskStatus.headline === 'Complete' ? [] : [assignment.detail];
74
79
  return {
75
- token: status.token, tone: status.tone, label: status.label,
80
+ token: status.token, tone: status.tone, label: status.label, status: taskStatus,
76
81
  action: options.hideAction || href === null ? null : { label: assignment.primaryAction.label, href, openResult: options.resultHref !== undefined },
77
- // A good outcome at a glance; the same words stay in the result.
78
- passed: ready && !checkProblem && assignment.receipt !== null && assignment.receipt.checks.status === 'passed'
79
- ? { by: assignment.completion === null ? null : assignment.completion.actor.replace(/^(?:operator|coordinator|lead):/, '') } : null,
80
- detail: { text: assignment.detail, problem: checkProblem || options.problem === true && !ready },
81
- problems: attention.filter(one => savedMaterialNotice(one, assignment) === null),
82
+ reasons: [...new Set([...recorded, ...attention.filter(one => savedMaterialNotice(one, assignment) === null).map(plainReasonWords)])].filter(one => one !== taskStatus.sentence),
82
83
  diagnostics: diagnostics.filter(one => savedMaterialNotice(one.detail, assignment) === null)
83
- .map(one => ({ token: one.token, label: one.label, detail: one.detail, problem: one.tone === 'problem' || one.tone === 'attention' })),
84
+ .map(one => ({ token: one.token, label: one.label, detail: one.detail })),
84
85
  notices: notices.length === 0 ? null : { summary: [partial ? 'Saved output is partial' : '', history ? 'Earlier material unavailable' : ''].filter(Boolean).join(' · '), lines: [...new Set(notices)] },
85
86
  attempts: assignmentAttemptsOf(assignment),
86
87
  lead: assignment.owner === null ? null : { label: assignment.owner.label, active: assignment.owner.active },
@@ -88,14 +89,13 @@ export function assignmentCardOf(assignment, options = {}) {
88
89
  }
89
90
  export function assignmentSummaryHtml(assignment, options = {}) {
90
91
  const card = assignmentCardOf(assignment, options);
91
- 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}"${options.compact ? '' : ' data-task-status'}>` +
92
- (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">${card.label}</h2>`) +
92
+ const tone = card.status.tone;
93
+ 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'}>` +
94
+ (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>`) +
95
+ `<p class="meta assignment-detail status-sentence">${escape(card.status.sentence)}</p>` +
93
96
  (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>`) +
94
- (!options.compact && card.passed !== null
95
- ? `<p class="meta assignment-detail assignment-verdict"><span class="verdict-chip verdict-chip--success">Checks passed</span>${card.passed.by === null ? '' : ` Completed by ${escape(card.passed.by)}`}</p>`
96
- : `<p class="${card.detail.problem ? 'problem' : 'meta'} assignment-detail">${escape(card.detail.text)}</p>`) +
97
- card.problems.map(one => `<p class="problem">${escape(one)}</p>`).join('') +
98
- card.diagnostics.map(one => `<p class="${one.problem ? 'problem' : 'meta'}" data-work-diagnostic="${escape(one.token)}">${escape(one.label)} · ${escape(one.detail)}</p>`).join('') +
97
+ (options.compact ? '' : statusDetailsHtml(card.status)) +
98
+ statusWhyHtml(card.status, card.reasons, card.diagnostics) +
99
99
  (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>`) +
100
100
  assignmentAttemptsHtml(assignment) + `</section>`;
101
101
  }