toolroll 0.9.5 → 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,17 @@
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
+
5
16
  ## 0.9.5 — 2026-10-01
6
17
 
7
18
  - **A demo you can feel in 90 seconds.** In `npx toolroll demo`, Chat has a
@@ -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
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 };
@@ -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
  }
@@ -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,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 => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[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,23 @@ 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 presentation = assignmentPresentationOf(assignment, options);
67
- 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;
68
73
  const href = assignment.primaryAction?.code === 'open-result' && options.resultHref !== undefined ? options.resultHref : assignmentActionHref(assignment);
69
74
  const attention = presentation.attention.map(one => one.detail);
70
75
  const notices = [...attention, ...diagnostics.map(one => one.detail)].filter(one => savedMaterialNotice(one, assignment) !== null);
71
76
  const partial = notices.some(one => savedMaterialNotice(one, assignment) === 'partial');
72
77
  const history = notices.some(one => savedMaterialNotice(one, assignment) === 'history');
73
- const ready = assignment.state === "ready-to-check" || assignment.state === "complete";
74
- 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];
75
79
  return {
76
- token: status.token, tone: status.tone, label: status.label,
80
+ token: status.token, tone: status.tone, label: status.label, status: taskStatus,
77
81
  action: options.hideAction || href === null ? null : { label: assignment.primaryAction.label, href, openResult: options.resultHref !== undefined },
78
- // A good outcome at a glance; the same words stay in the result.
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),
82
+ reasons: [...new Set([...recorded, ...attention.filter(one => savedMaterialNotice(one, assignment) === null).map(plainReasonWords)])].filter(one => one !== taskStatus.sentence),
83
83
  diagnostics: diagnostics.filter(one => savedMaterialNotice(one.detail, assignment) === null)
84
- .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 })),
85
85
  notices: notices.length === 0 ? null : { summary: [partial ? 'Saved output is partial' : '', history ? 'Earlier material unavailable' : ''].filter(Boolean).join(' · '), lines: [...new Set(notices)] },
86
86
  attempts: assignmentAttemptsOf(assignment),
87
87
  lead: assignment.owner === null ? null : { label: assignment.owner.label, active: assignment.owner.active },
@@ -89,14 +89,13 @@ export function assignmentCardOf(assignment, options = {}) {
89
89
  }
90
90
  export function assignmentSummaryHtml(assignment, options = {}) {
91
91
  const card = assignmentCardOf(assignment, options);
92
- 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'}>` +
93
- (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>` +
94
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>`) +
95
- (!options.compact && card.passed !== null
96
- ? `<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>`
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('') +
97
+ (options.compact ? '' : statusDetailsHtml(card.status)) +
98
+ statusWhyHtml(card.status, card.reasons, card.diagnostics) +
100
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>`) +
101
100
  assignmentAttemptsHtml(assignment) + `</section>`;
102
101
  }