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 +11 -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 +17 -18
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +21 -20
- package/dist/browser-workspace.d.ts +3 -10
- package/dist/chat-acceptance.js +2 -2
- package/dist/chat-quiet.js +20 -19
- package/dist/demo-chat.d.ts +0 -5
- package/dist/demo-chat.js +10 -2
- package/dist/lead-status.d.ts +7 -0
- package/dist/lead-status.js +11 -2
- package/dist/serve.js +64 -66
- package/dist/task-status.d.ts +179 -0
- package/dist/task-status.js +317 -0
- package/dist/telegram-progress.js +62 -100
- package/dist/telegram-status.js +19 -6
- package/dist/work-index.js +25 -9
- package/dist/workspace-ui.d.ts +7 -0
- package/dist/workspace-ui.js +30 -1
- package/package.json +1 -1
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/** One answer per task, then the details (docs/design/task-status.md). Every
|
|
2
|
+
* surface — the Tasks list, the task page, the build page, Crew, the Chat
|
|
3
|
+
* result, the Telegram/Slack/Discord/Teams cards and `toolroll status` — reads
|
|
4
|
+
* its headline, sentence, tone and detail rows from `taskStatusOf`, so the
|
|
5
|
+
* words cannot drift apart again. Pure: it reads facts the existing
|
|
6
|
+
* projections already recorded and never changes a check, approval or
|
|
7
|
+
* completion.
|
|
8
|
+
*
|
|
9
|
+
* Severity: red belongs to the Failed headline alone. A detail problem that
|
|
10
|
+
* doesn't undo the outcome (a pull request that couldn't open, shortened
|
|
11
|
+
* output) is an amber note on its own row with one action; the exact
|
|
12
|
+
* technical reason stays one tap away in `why`. */
|
|
13
|
+
import type { AssignmentSnapshot } from "./assignment.js";
|
|
14
|
+
export declare const HEADLINES: readonly ["Queued", "Planning", "Needs you", "Building", "Ready for review", "Complete", "Failed", "Stopped"];
|
|
15
|
+
export type Headline = (typeof HEADLINES)[number];
|
|
16
|
+
export type HeadlineTone = "neutral" | "live" | "attention" | "ready" | "success" | "danger";
|
|
17
|
+
export declare const HEADLINE_TONE: Readonly<Record<Headline, HeadlineTone>>;
|
|
18
|
+
export type DetailKey = "checks" | "pull-request" | "requirements" | "evidence";
|
|
19
|
+
/** The row's small icon: the only coloured part of a detail. `failed` is red
|
|
20
|
+
* and only ever appears under the Failed headline; `note` is amber. */
|
|
21
|
+
export type DetailMark = "ok" | "running" | "none" | "note" | "failed";
|
|
22
|
+
export type StatusAction = {
|
|
23
|
+
label: string;
|
|
24
|
+
href: string | null;
|
|
25
|
+
};
|
|
26
|
+
export type StatusDetail = {
|
|
27
|
+
key: DetailKey;
|
|
28
|
+
label: string;
|
|
29
|
+
text: string;
|
|
30
|
+
mark: DetailMark;
|
|
31
|
+
href: string | null;
|
|
32
|
+
action: StatusAction | null;
|
|
33
|
+
why: string | null;
|
|
34
|
+
};
|
|
35
|
+
export type TaskStatus = {
|
|
36
|
+
headline: Headline;
|
|
37
|
+
tone: HeadlineTone;
|
|
38
|
+
sentence: string;
|
|
39
|
+
details: StatusDetail[];
|
|
40
|
+
primaryAction: StatusAction | null;
|
|
41
|
+
why: string[];
|
|
42
|
+
};
|
|
43
|
+
export type TaskStage = "queued" | "planning" | "needs-you" | "building" | "checking" | "finished" | "complete" | "failed" | "stopped";
|
|
44
|
+
export type ChecksFact = {
|
|
45
|
+
status: "passed" | "failed" | "running" | "not-run" | "unavailable";
|
|
46
|
+
exitCode: number | null;
|
|
47
|
+
head: string | null;
|
|
48
|
+
};
|
|
49
|
+
export type PullRequestFact = {
|
|
50
|
+
state: "none" | "opening" | "open" | "merged" | "closed" | "failed";
|
|
51
|
+
number: number | null;
|
|
52
|
+
url: string | null;
|
|
53
|
+
ci: "running" | "passing" | "failing" | null;
|
|
54
|
+
error: string | null;
|
|
55
|
+
/** GitHub's compare page for the pushed branch: where a person opens it by hand. */
|
|
56
|
+
compareUrl?: string | null;
|
|
57
|
+
/** Who merged it and its merge commit, shown on request. */
|
|
58
|
+
note?: string | null;
|
|
59
|
+
};
|
|
60
|
+
export type TaskStatusFacts = {
|
|
61
|
+
stage: TaskStage;
|
|
62
|
+
/** Needs you: what a person is asked for. The sentence names it. */
|
|
63
|
+
need?: "approval" | "answer" | "sign-in" | "other";
|
|
64
|
+
/** The recorded reason in plain words, when the stage has one. */
|
|
65
|
+
reason?: string | null;
|
|
66
|
+
/** A research report rather than a code change. */
|
|
67
|
+
report?: boolean;
|
|
68
|
+
checks?: ChecksFact | null;
|
|
69
|
+
pullRequest?: PullRequestFact | null;
|
|
70
|
+
requirements?: {
|
|
71
|
+
met: number;
|
|
72
|
+
total: number;
|
|
73
|
+
yours: number;
|
|
74
|
+
} | null;
|
|
75
|
+
evidence?: {
|
|
76
|
+
shortened: number;
|
|
77
|
+
missing: number;
|
|
78
|
+
damaged: number;
|
|
79
|
+
} | null;
|
|
80
|
+
completedBy?: string | null;
|
|
81
|
+
action?: StatusAction | null;
|
|
82
|
+
/** Where a row's one action leads: the result (and its Checks tab), the task's pull request. */
|
|
83
|
+
links?: {
|
|
84
|
+
result?: string | null;
|
|
85
|
+
checks?: string | null;
|
|
86
|
+
pullRequest?: string | null;
|
|
87
|
+
};
|
|
88
|
+
/** Exact technical reasons, shown only on request. */
|
|
89
|
+
why?: readonly string[];
|
|
90
|
+
};
|
|
91
|
+
export declare function headlineOf(facts: Pick<TaskStatusFacts, "stage" | "checks" | "report">): Headline;
|
|
92
|
+
export declare function taskStatusOf(facts: TaskStatusFacts): TaskStatus;
|
|
93
|
+
export declare function plainReasonOf(stage: TaskStage, code: string, detail: string | null | undefined): string | null;
|
|
94
|
+
export declare function stageOfCode(code: string, options?: {
|
|
95
|
+
needsPerson?: boolean;
|
|
96
|
+
planning?: boolean;
|
|
97
|
+
operatorHold?: boolean;
|
|
98
|
+
}): {
|
|
99
|
+
stage: TaskStage;
|
|
100
|
+
need?: TaskStatusFacts["need"];
|
|
101
|
+
};
|
|
102
|
+
/** The pull request as the task page's follower reports it, or the bare publication row. */
|
|
103
|
+
export declare function pullRequestFactOf(publication: {
|
|
104
|
+
state: string;
|
|
105
|
+
prNumber?: number | null;
|
|
106
|
+
prUrl: string | null;
|
|
107
|
+
remoteState: string | null;
|
|
108
|
+
lastCheckState?: string | null;
|
|
109
|
+
lastError?: string | null;
|
|
110
|
+
merged?: boolean;
|
|
111
|
+
githubRepo?: string;
|
|
112
|
+
base?: string;
|
|
113
|
+
head?: string;
|
|
114
|
+
} | null): PullRequestFact | null;
|
|
115
|
+
/** The assignment's stage. The work status, when the caller read one, names
|
|
116
|
+
* the exact reason (a running planner, a pause, a failed attempt, a sign-in). */
|
|
117
|
+
export declare function assignmentStageOf(assignment: Pick<AssignmentSnapshot, "state" | "primaryAction">, work?: {
|
|
118
|
+
token: string;
|
|
119
|
+
views?: readonly string[];
|
|
120
|
+
} | null, planning?: boolean): {
|
|
121
|
+
stage: TaskStage;
|
|
122
|
+
need?: TaskStatusFacts["need"];
|
|
123
|
+
};
|
|
124
|
+
/** Requirements from the stored matrix: met, the ones only a person confirms, and the total. */
|
|
125
|
+
export declare function requirementsOf(matrix: readonly {
|
|
126
|
+
state: string;
|
|
127
|
+
assessment?: {
|
|
128
|
+
evidenceState?: string;
|
|
129
|
+
} | undefined;
|
|
130
|
+
review?: unknown;
|
|
131
|
+
}[] | null | undefined): NonNullable<TaskStatusFacts["requirements"]> | null;
|
|
132
|
+
/** Saved evidence health from the receipt's artifact list and its caveats. */
|
|
133
|
+
export declare function evidenceOf(receipt: AssignmentSnapshot["receipt"], problems?: readonly string[]): NonNullable<TaskStatusFacts["evidence"]> | null;
|
|
134
|
+
/** The status facts of one assignment, as every surface reads them. */
|
|
135
|
+
export declare function assignmentStatusFacts(assignment: AssignmentSnapshot, options?: {
|
|
136
|
+
work?: {
|
|
137
|
+
token: string;
|
|
138
|
+
views?: readonly string[];
|
|
139
|
+
detail?: string;
|
|
140
|
+
} | null;
|
|
141
|
+
planning?: boolean;
|
|
142
|
+
pullRequest?: PullRequestFact | null;
|
|
143
|
+
action?: StatusAction | null;
|
|
144
|
+
links?: TaskStatusFacts["links"];
|
|
145
|
+
evidence?: TaskStatusFacts["evidence"];
|
|
146
|
+
why?: readonly string[];
|
|
147
|
+
}): TaskStatusFacts;
|
|
148
|
+
/** The legacy work tone each headline wears where a surface still keys off StatusTone. */
|
|
149
|
+
export declare function workToneOf(headline: Headline): "attention" | "problem" | "live" | "ready" | "done" | "muted";
|
|
150
|
+
/** One plain-text line per detail for chat cards: an icon, the label, the words. */
|
|
151
|
+
export declare function statusDetailLines(status: TaskStatus): string[];
|
|
152
|
+
/** The headline's emoji for chat cards (colour is never the only signal: the words follow). */
|
|
153
|
+
export declare function headlineEmoji(headline: Headline): string;
|
|
154
|
+
export declare const statusIconSvg: (mark: DetailMark) => string;
|
|
155
|
+
/** The detail rows as server HTML: quiet rows, colour only on the icon. */
|
|
156
|
+
export declare function statusDetailsHtml(status: TaskStatus): string;
|
|
157
|
+
/** The technical reasons, one tap away. */
|
|
158
|
+
export declare function statusWhyHtml(status: TaskStatus, extra?: readonly string[], diagnostics?: readonly {
|
|
159
|
+
token: string;
|
|
160
|
+
label: string;
|
|
161
|
+
detail: string;
|
|
162
|
+
}[]): string;
|
|
163
|
+
/** Shared CSS for the server-rendered status: neutral rows, coloured icons. */
|
|
164
|
+
export declare const TASK_STATUS_CSS = ".status-headline{display:flex;align-items:center;gap:.6rem;margin:0;font-size:1.125rem;font-weight:600;color:var(--foreground)}.status-headline i{width:.625rem;height:.625rem;border-radius:999px;flex-shrink:0;background:var(--muted-foreground)}[data-headline-tone=live] .status-headline i{background:var(--so-info,#0d74ce)}[data-headline-tone=attention] .status-headline i{background:var(--so-attention,var(--attention))}[data-headline-tone=ready] .status-headline i{background:transparent;box-shadow:inset 0 0 0 2px var(--so-success,#218358)}[data-headline-tone=success] .status-headline i{background:var(--so-success,#218358)}[data-headline-tone=danger] .status-headline i{background:var(--so-danger,#c4320a)}.status-sentence{margin:.35rem 0 0;color:var(--muted-foreground)}.status-details{list-style:none;margin:.75rem 0 0;padding:.25rem 0 0;border-top:1px solid var(--so-line,var(--border))}.status-detail{display:grid;grid-template-columns:14px 7.5rem minmax(0,1fr);align-items:center;gap:.6rem;padding:.4rem 0;font-size:.8125rem;color:var(--foreground)}.status-detail .status-icon{align-self:center;color:var(--muted-foreground)}.status-detail--ok .status-icon{color:var(--so-success,#218358)}.status-detail--running .status-icon{color:var(--so-info,#0d74ce)}.status-detail--note .status-icon{color:var(--so-warning,#ab6400)}.status-detail--failed .status-icon{color:var(--so-danger,#c4320a)}.status-detail-label{color:var(--muted-foreground)}.status-detail-text{display:flex;flex-wrap:wrap;align-items:baseline;gap:0 .6rem;min-width:0;overflow-wrap:anywhere}.status-detail-text a{color:inherit}.status-detail-act{font-weight:600;white-space:nowrap}.status-detail--note .status-detail-act{color:var(--so-warning,#ab6400)}@media(max-width:480px){.status-detail{grid-template-columns:14px 6.5rem minmax(0,1fr);min-height:2.75rem;padding:.25rem 0}.status-detail-text a{display:inline-block;padding:.75rem 0;margin:-.75rem 0}}.status-why{border:0;padding:0;margin:.25rem 0 0;background:transparent;box-shadow:none}.status-why>summary{cursor:pointer;min-height:2.75rem;display:list-item;align-content:center;font-size:.8125rem;color:var(--muted-foreground)}.status-why p{margin:.25rem 0 .5rem}.receipt-note{display:flex;align-items:center;gap:.45rem;font-size:.8125rem;color:var(--foreground)}.receipt-note .status-icon{color:var(--so-warning,#ab6400)}";
|
|
165
|
+
/** A dispatch diagnosis as its stage, for the phone and CLI reads that hold
|
|
166
|
+
* no assignment. A done task is Complete once a person marked its result;
|
|
167
|
+
* a refuted result whose project check failed is Failed. */
|
|
168
|
+
export declare function stageOfDispatch(d: {
|
|
169
|
+
code: string;
|
|
170
|
+
condition: string;
|
|
171
|
+
action: string | null;
|
|
172
|
+
role?: string | null;
|
|
173
|
+
detail?: string;
|
|
174
|
+
}, options?: {
|
|
175
|
+
completed?: boolean;
|
|
176
|
+
}): {
|
|
177
|
+
stage: TaskStage;
|
|
178
|
+
need?: TaskStatusFacts["need"];
|
|
179
|
+
};
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
export const HEADLINES = ["Queued", "Planning", "Needs you", "Building", "Ready for review", "Complete", "Failed", "Stopped"];
|
|
2
|
+
export const HEADLINE_TONE = {
|
|
3
|
+
Queued: "neutral", Planning: "live", "Needs you": "attention", Building: "live",
|
|
4
|
+
"Ready for review": "ready", Complete: "success", Failed: "danger", Stopped: "neutral",
|
|
5
|
+
};
|
|
6
|
+
const short = (sha) => sha !== null && /^[a-f0-9]{7,40}$/.test(sha) ? sha.slice(0, 7) : null;
|
|
7
|
+
export function headlineOf(facts) {
|
|
8
|
+
switch (facts.stage) {
|
|
9
|
+
case "queued": return "Queued";
|
|
10
|
+
case "planning": return "Planning";
|
|
11
|
+
case "needs-you": return "Needs you";
|
|
12
|
+
case "building":
|
|
13
|
+
case "checking": return "Building";
|
|
14
|
+
case "finished": return !facts.report && facts.checks?.status === "failed" ? "Failed" : "Ready for review";
|
|
15
|
+
case "complete": return "Complete";
|
|
16
|
+
case "failed": return "Failed";
|
|
17
|
+
case "stopped": return "Stopped";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function sentenceOf(headline, facts) {
|
|
21
|
+
const reason = facts.reason?.trim() || null;
|
|
22
|
+
const sha = short(facts.checks?.head ?? null);
|
|
23
|
+
switch (headline) {
|
|
24
|
+
case "Queued": return reason ?? "Waiting for a worker.";
|
|
25
|
+
case "Planning": return reason ?? "The lead is writing the plan.";
|
|
26
|
+
// An approval always reads the same way; the plan itself names the work.
|
|
27
|
+
case "Needs you": return facts.need === "approval" ? "Review the plan and approve it to start." : reason ?? (facts.need === "answer" ? "Answer the question so the work can continue."
|
|
28
|
+
: facts.need === "sign-in" ? "Sign in again; the task starts on its own after."
|
|
29
|
+
: "Something needs your decision before the work can continue.");
|
|
30
|
+
case "Building": return facts.stage === "checking" ? "Checks are running on the change." : reason ?? "An agent is working on it.";
|
|
31
|
+
case "Ready for review":
|
|
32
|
+
if (facts.report)
|
|
33
|
+
return "The report is ready to read. Read it, then mark it complete.";
|
|
34
|
+
if (facts.checks?.status === "passed")
|
|
35
|
+
return `Checks passed${sha === null ? "" : ` on ${sha}`}. Review the change, then mark it complete.`;
|
|
36
|
+
// Never claim a check that isn't known to have passed.
|
|
37
|
+
if (facts.checks == null)
|
|
38
|
+
return "Review the change, then mark it complete.";
|
|
39
|
+
return "Built without a passing project check. Review the change, then mark it complete.";
|
|
40
|
+
case "Complete": {
|
|
41
|
+
const by = facts.completedBy ? `Marked complete by ${facts.completedBy}.` : "Marked complete.";
|
|
42
|
+
const pr = facts.pullRequest;
|
|
43
|
+
return pr?.state === "merged" ? `${by} ${pr.number === null ? "Its pull request" : `Pull request #${pr.number}`} merged.` : by;
|
|
44
|
+
}
|
|
45
|
+
case "Failed":
|
|
46
|
+
if (facts.checks?.status === "failed")
|
|
47
|
+
return `Checks failed${sha === null ? "" : ` on ${sha}`}. See what broke, then retry or ask for changes.`;
|
|
48
|
+
return reason ?? "The last attempt stopped before it finished. Review it, then retry.";
|
|
49
|
+
case "Stopped": return reason ?? "Stopped by a person. The work so far is kept.";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** A detail problem: red only under Failed, otherwise an amber note. */
|
|
53
|
+
const problem = (headline) => headline === "Failed" ? "failed" : "note";
|
|
54
|
+
function detailsOf(headline, facts) {
|
|
55
|
+
const rows = [];
|
|
56
|
+
const row = (key, label, text, mark, extra = {}) => rows.push({ key, label, text, mark, href: extra.href ?? null, action: extra.action ?? null, why: extra.why ?? null });
|
|
57
|
+
const checks = facts.checks;
|
|
58
|
+
if (checks != null && !facts.report) {
|
|
59
|
+
const sha = short(checks.head);
|
|
60
|
+
const checksHref = facts.links?.checks ?? facts.links?.result ?? null;
|
|
61
|
+
if (checks.status === "passed")
|
|
62
|
+
row("checks", "Checks", `Passed${sha === null ? "" : ` on ${sha}`}`, "ok", { href: checksHref });
|
|
63
|
+
else if (checks.status === "failed")
|
|
64
|
+
row("checks", "Checks", `Failed${checks.exitCode === null ? "" : ` (exit ${checks.exitCode})`}`, problem(headline), headline === "Failed" ? { href: checksHref } : { action: { label: "See what failed", href: checksHref } });
|
|
65
|
+
else if (checks.status === "running")
|
|
66
|
+
row("checks", "Checks", "Running", "running");
|
|
67
|
+
else if (checks.status === "not-run")
|
|
68
|
+
row("checks", "Checks", "Didn't run", "none");
|
|
69
|
+
else
|
|
70
|
+
row("checks", "Checks", "Couldn't be read", "note", { action: { label: "Open the result", href: facts.links?.result ?? null } });
|
|
71
|
+
}
|
|
72
|
+
const pr = facts.pullRequest;
|
|
73
|
+
if (pr != null) {
|
|
74
|
+
const name = pr.number === null ? "Pull request" : `#${pr.number}`;
|
|
75
|
+
const github = pr.url !== null && /^https:\/\/github\.com\//.test(pr.url) ? pr.url : null;
|
|
76
|
+
if (pr.state === "none")
|
|
77
|
+
row("pull-request", "Pull request", "None", "none");
|
|
78
|
+
else if (pr.state === "opening")
|
|
79
|
+
row("pull-request", "Pull request", "Opening…", "running", { why: pr.error });
|
|
80
|
+
else if (pr.state === "merged")
|
|
81
|
+
row("pull-request", "Pull request", `${name} merged`, "ok", { href: github, why: pr.note ?? null });
|
|
82
|
+
else if (pr.state === "closed")
|
|
83
|
+
row("pull-request", "Pull request", `${name} closed without merging`, "none", { href: github });
|
|
84
|
+
else if (pr.state === "failed")
|
|
85
|
+
row("pull-request", "Pull request", "Couldn't open", "note", { action: pr.compareUrl ? { label: "Open it on GitHub", href: pr.compareUrl } : { label: "See why", href: facts.links?.pullRequest ?? null }, why: pr.error === null ? "Publishing gave up. The commit is safe locally." : `${pr.error} The commit is safe locally.` });
|
|
86
|
+
else if (pr.ci === "running")
|
|
87
|
+
row("pull-request", "Pull request", `${name} open, CI running`, "running", { href: github });
|
|
88
|
+
else if (pr.ci === "passing")
|
|
89
|
+
row("pull-request", "Pull request", `${name} open, CI passed`, "ok", { href: github });
|
|
90
|
+
else if (pr.ci === "failing")
|
|
91
|
+
row("pull-request", "Pull request", `${name} open, CI failing`, "note", { action: { label: "Open it on GitHub", href: github } });
|
|
92
|
+
else
|
|
93
|
+
row("pull-request", "Pull request", `${name} open`, "none", { href: github });
|
|
94
|
+
}
|
|
95
|
+
const req = facts.requirements;
|
|
96
|
+
if (req != null && req.total > 0) {
|
|
97
|
+
const unmet = req.total - req.met - req.yours;
|
|
98
|
+
const text = `${req.met} of ${req.total} met${req.yours > 0 ? ` · You check ${req.yours}` : ""}`;
|
|
99
|
+
row("requirements", "Requirements", text, unmet > 0 ? problem(headline) : req.met === req.total ? "ok" : "none", unmet > 0 && headline !== "Failed" ? { action: { label: "See which", href: facts.links?.checks ?? facts.links?.result ?? null } } : {});
|
|
100
|
+
}
|
|
101
|
+
const evidence = facts.evidence;
|
|
102
|
+
if (evidence != null) {
|
|
103
|
+
const action = { label: "See what was kept", href: facts.links?.result ?? null };
|
|
104
|
+
if (evidence.damaged > 0)
|
|
105
|
+
row("evidence", "Saved evidence", "Some saved files changed", problem(headline), headline === "Failed" ? {} : { action });
|
|
106
|
+
else if (evidence.missing > 0)
|
|
107
|
+
row("evidence", "Saved evidence", "Some saved files are missing", problem(headline), headline === "Failed" ? {} : { action });
|
|
108
|
+
else if (evidence.shortened > 0)
|
|
109
|
+
row("evidence", "Saved evidence", "Some output shortened", "note", { action });
|
|
110
|
+
else
|
|
111
|
+
row("evidence", "Saved evidence", "Complete", "ok");
|
|
112
|
+
}
|
|
113
|
+
return rows;
|
|
114
|
+
}
|
|
115
|
+
export function taskStatusOf(facts) {
|
|
116
|
+
const headline = headlineOf(facts);
|
|
117
|
+
return { headline, tone: HEADLINE_TONE[headline], sentence: sentenceOf(headline, facts), details: detailsOf(headline, facts),
|
|
118
|
+
primaryAction: facts.action ?? null, why: [...new Set(facts.why ?? [])].filter(one => one.trim() !== "") };
|
|
119
|
+
}
|
|
120
|
+
// ---- reading the existing projections ----------------------------------------
|
|
121
|
+
/** Every dispatch or work-index code, as the stage it means. Unknown codes that
|
|
122
|
+
* wait on a person stay Needs you; everything else waits in the queue. */
|
|
123
|
+
const CODE_STAGE = {
|
|
124
|
+
queued: ["queued"], ready: ["queued"], "worker-at-capacity": ["queued"], "retry-scheduled": ["queued"], updating: ["queued"],
|
|
125
|
+
"waiting-dependency": ["queued"], "scouting-ready": ["queued"], "planning-ready": ["queued"], "provider-quota": ["queued"], "planner-source": ["queued"],
|
|
126
|
+
running: ["building"], reviewing: ["building"], "review-pending": ["building"],
|
|
127
|
+
"signed-out": ["needs-you", "sign-in"],
|
|
128
|
+
"needs-approval": ["needs-you", "approval"],
|
|
129
|
+
"waiting-decision": ["needs-you", "answer"], "decision-queue": ["needs-you", "answer"],
|
|
130
|
+
stopping: ["stopped"], stopped: ["stopped"], paused: ["stopped"], cancelled: ["stopped"],
|
|
131
|
+
failed: ["failed"], "waiting-incident": ["failed"], "checks-failed": ["failed"],
|
|
132
|
+
"ready-to-check": ["finished"], "ready-to-review": ["finished"], "report-ready": ["finished"], "no-change": ["finished"],
|
|
133
|
+
"pr-opened": ["finished"], "merge-observed": ["finished"], "pr-closed": ["finished"], "agent-attested": ["finished"], "accepted-exception": ["finished"],
|
|
134
|
+
complete: ["complete"],
|
|
135
|
+
};
|
|
136
|
+
/** Queue reasons worth a sentence of their own; any other queued or
|
|
137
|
+
* building reason is scheduler wording, and the default sentence is plainer. */
|
|
138
|
+
const QUEUED_REASONS = new Set(["worker-at-capacity", "waiting-dependency", "retry-scheduled", "updating", "planning-ready", "scouting-ready", "provider-quota"]);
|
|
139
|
+
export function plainReasonOf(stage, code, detail) {
|
|
140
|
+
if (stage === "building" || stage === "checking" || stage === "planning")
|
|
141
|
+
return null;
|
|
142
|
+
if (stage === "queued")
|
|
143
|
+
return QUEUED_REASONS.has(code) ? detail ?? null : null;
|
|
144
|
+
return detail ?? null;
|
|
145
|
+
}
|
|
146
|
+
export function stageOfCode(code, options = {}) {
|
|
147
|
+
if (code === "running" && options.planning)
|
|
148
|
+
return { stage: "planning" };
|
|
149
|
+
if (code === "held")
|
|
150
|
+
return options.operatorHold === false ? { stage: "needs-you", need: "other" } : { stage: "stopped" };
|
|
151
|
+
const known = CODE_STAGE[code];
|
|
152
|
+
if (known !== undefined)
|
|
153
|
+
return known[1] === undefined ? { stage: known[0] } : { stage: known[0], need: known[1] };
|
|
154
|
+
return options.needsPerson === false ? { stage: "queued" } : { stage: "needs-you", need: "other" };
|
|
155
|
+
}
|
|
156
|
+
/** The pull request as the task page's follower reports it, or the bare publication row. */
|
|
157
|
+
export function pullRequestFactOf(publication) {
|
|
158
|
+
if (publication === null)
|
|
159
|
+
return null;
|
|
160
|
+
const safe = (part) => part !== undefined && /^[A-Za-z0-9_.\/-]+$/.test(part) && !part.includes("..") ? part : null;
|
|
161
|
+
const repo = safe(publication.githubRepo), from = safe(publication.base), to = safe(publication.head);
|
|
162
|
+
const compareUrl = repo !== null && /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repo) && from !== null && to !== null ? `https://github.com/${repo}/compare/${from}...${to}?expand=1` : null;
|
|
163
|
+
const seen = publication.lastCheckState;
|
|
164
|
+
const ci = seen === "running" || seen === "passing" || seen === "failing" ? seen : null;
|
|
165
|
+
const fromUrl = /\/pull\/(\d+)$/.exec(publication.prUrl ?? "");
|
|
166
|
+
const number = publication.prNumber ?? (fromUrl === null ? null : Number(fromUrl[1]));
|
|
167
|
+
const base = { number, url: publication.prUrl, ci, error: publication.lastError ?? null, compareUrl };
|
|
168
|
+
if (publication.merged === true || publication.remoteState === "MERGED")
|
|
169
|
+
return { ...base, state: "merged" };
|
|
170
|
+
if (publication.remoteState === "CLOSED")
|
|
171
|
+
return { ...base, state: "closed" };
|
|
172
|
+
if (publication.state === "failed")
|
|
173
|
+
return { ...base, state: "failed" };
|
|
174
|
+
if (publication.state === "opened")
|
|
175
|
+
return { ...base, state: "open" };
|
|
176
|
+
return { ...base, state: "opening" };
|
|
177
|
+
}
|
|
178
|
+
/** The assignment's stage. The work status, when the caller read one, names
|
|
179
|
+
* the exact reason (a running planner, a pause, a failed attempt, a sign-in). */
|
|
180
|
+
export function assignmentStageOf(assignment, work, planning = false) {
|
|
181
|
+
switch (assignment.state) {
|
|
182
|
+
case "cancelled": return { stage: "stopped" };
|
|
183
|
+
case "complete": return { stage: "complete" };
|
|
184
|
+
case "ready-to-check": return { stage: "finished" };
|
|
185
|
+
case "checking": return { stage: "checking" };
|
|
186
|
+
case "working": {
|
|
187
|
+
const token = work?.token ?? "running";
|
|
188
|
+
const read = stageOfCode(token, { needsPerson: work?.views?.includes("needs-you") ?? false, planning });
|
|
189
|
+
return read.stage === "finished" || read.stage === "complete" ? { stage: "building" } : read;
|
|
190
|
+
}
|
|
191
|
+
case "needs-decision": {
|
|
192
|
+
const action = assignment.primaryAction?.code;
|
|
193
|
+
if (action === "approve-scope")
|
|
194
|
+
return { stage: "needs-you", need: "approval" };
|
|
195
|
+
if (action === "answer-decision" || action === "inspect-decisions")
|
|
196
|
+
return { stage: "needs-you", need: "answer" };
|
|
197
|
+
const token = work?.token ?? "";
|
|
198
|
+
// A failed attempt is Failed. A saved result that still needs a decision
|
|
199
|
+
// (its scope, a hold, a process exit) is Needs you, even if its checks
|
|
200
|
+
// failed: the decision is the next step, and the Checks row says the rest.
|
|
201
|
+
if (token === "failed" || token === "waiting-incident")
|
|
202
|
+
return { stage: "failed" };
|
|
203
|
+
const read = stageOfCode(token, { needsPerson: true, operatorHold: action === "unhold" });
|
|
204
|
+
return read.stage === "stopped" ? read : { stage: "needs-you", need: read.need ?? "other" };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/** Requirements from the stored matrix: met, the ones only a person confirms, and the total. */
|
|
209
|
+
export function requirementsOf(matrix) {
|
|
210
|
+
if (!matrix || matrix.length === 0)
|
|
211
|
+
return null;
|
|
212
|
+
// Evidence that passed and only awaits the retired assessment step is met.
|
|
213
|
+
const met = (row) => row.state === "pass" || (row.assessment?.evidenceState === "pass" && (row.review ?? null) === null);
|
|
214
|
+
return { met: matrix.filter(met).length, total: matrix.length, yours: matrix.filter(row => !met(row) && row.state === "manual-review").length };
|
|
215
|
+
}
|
|
216
|
+
/** Saved evidence health from the receipt's artifact list and its caveats. */
|
|
217
|
+
export function evidenceOf(receipt, problems = receipt?.caveats ?? []) {
|
|
218
|
+
if (receipt === null)
|
|
219
|
+
return null;
|
|
220
|
+
const shortened = receipt.artifacts.filter(one => !one.complete).length + problems.filter(one => /shortened|cut short/.test(one)).length;
|
|
221
|
+
const damaged = problems.filter(one => /unavailable or changed|no longer verif|cannot be read|could not be read|failed capture|altered|damaged/i.test(one)).length;
|
|
222
|
+
const missing = problems.filter(one => /missing|ambiguous|no longer exists/i.test(one) && !/unavailable or changed/.test(one)).length;
|
|
223
|
+
return { shortened, missing, damaged };
|
|
224
|
+
}
|
|
225
|
+
/** The status facts of one assignment, as every surface reads them. */
|
|
226
|
+
export function assignmentStatusFacts(assignment, options = {}) {
|
|
227
|
+
const { stage, need } = assignmentStageOf(assignment, options.work ?? null, options.planning ?? false);
|
|
228
|
+
const receipt = assignment.receipt;
|
|
229
|
+
const finished = stage === "finished" || stage === "complete";
|
|
230
|
+
// A saved result's rows show whenever it is the reason a person is needed, too.
|
|
231
|
+
const withResult = finished || stage === "failed" || (stage === "needs-you" && receipt !== null);
|
|
232
|
+
const report = receipt?.completionKind === "research-report";
|
|
233
|
+
const checks = receipt === null || !withResult ? null
|
|
234
|
+
: { status: receipt.checks.status, exitCode: receipt.checks.exitCode, head: receipt.head };
|
|
235
|
+
const publication = assignment.publication;
|
|
236
|
+
const pullRequest = options.pullRequest !== undefined ? options.pullRequest
|
|
237
|
+
: publication === null ? (withResult && !report ? { state: "none", number: null, url: null, ci: null, error: null } : null)
|
|
238
|
+
: pullRequestFactOf(publication);
|
|
239
|
+
// Ready, Complete and Failed speak for themselves; every other stage keeps its recorded reason.
|
|
240
|
+
const reason = stage === "needs-you" || stage === "stopped" || stage === "queued" || (stage === "failed" && checks?.status !== "failed")
|
|
241
|
+
? stage === "stopped" && assignment.state === "cancelled" ? "Cancelled. Nothing else will run." : plainReasonOf(stage, options.work?.token ?? "", options.work?.detail ?? assignment.detail)
|
|
242
|
+
: null;
|
|
243
|
+
return {
|
|
244
|
+
stage, ...(need === undefined ? {} : { need }), reason, report,
|
|
245
|
+
checks,
|
|
246
|
+
pullRequest: withResult ? pullRequest : null,
|
|
247
|
+
requirements: withResult ? requirementsOf(receipt?.proof?.matrix) : null,
|
|
248
|
+
evidence: withResult ? options.evidence ?? evidenceOf(receipt, [...(receipt?.caveats ?? []), ...assignment.attention]) : null,
|
|
249
|
+
completedBy: assignment.completion === null ? null : assignment.completion.actor.replace(/^(?:operator|coordinator|lead):/, ""),
|
|
250
|
+
action: options.action ?? null,
|
|
251
|
+
...(options.links === undefined ? {} : { links: options.links }),
|
|
252
|
+
why: options.why ?? [],
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
// ---- rendering helpers shared by the server pages and chat cards ------------
|
|
256
|
+
/** The legacy work tone each headline wears where a surface still keys off StatusTone. */
|
|
257
|
+
export function workToneOf(headline) {
|
|
258
|
+
switch (headline) {
|
|
259
|
+
case "Needs you": return "attention";
|
|
260
|
+
case "Failed": return "problem";
|
|
261
|
+
case "Building":
|
|
262
|
+
case "Planning": return "live";
|
|
263
|
+
case "Ready for review": return "ready";
|
|
264
|
+
case "Complete": return "done";
|
|
265
|
+
default: return "muted";
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/** One plain-text line per detail for chat cards: an icon, the label, the words. */
|
|
269
|
+
export function statusDetailLines(status) {
|
|
270
|
+
const icon = { ok: "✓", running: "●", none: "○", note: "⚠", failed: "✕" };
|
|
271
|
+
return status.details.map(one => `${icon[one.mark]} ${one.label} · ${one.text}${one.action === null ? "" : ` — ${one.action.label}`}`);
|
|
272
|
+
}
|
|
273
|
+
/** The headline's emoji for chat cards (colour is never the only signal: the words follow). */
|
|
274
|
+
export function headlineEmoji(headline) {
|
|
275
|
+
return { Queued: "🕓", Planning: "📝", "Needs you": "👋", Building: "⏳", "Ready for review": "✅", Complete: "✅", Failed: "❌", Stopped: "⏹" }[headline];
|
|
276
|
+
}
|
|
277
|
+
const escapeHtml = (value) => value.replace(/[&<>"']/g, char => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[char]);
|
|
278
|
+
const ICON_PATHS = {
|
|
279
|
+
ok: `<path d="M20 6 9 17l-5-5"/>`,
|
|
280
|
+
running: `<circle cx="12" cy="12" r="4"/>`,
|
|
281
|
+
none: `<circle cx="12" cy="12" r="3"/>`,
|
|
282
|
+
note: `<path d="M12 8v5"/><path d="M12 16.5h.01"/><path d="M10.3 3.9 2.4 18a2 2 0 0 0 1.7 3h15.8a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/>`,
|
|
283
|
+
failed: `<path d="M18 6 6 18M6 6l12 12"/>`,
|
|
284
|
+
};
|
|
285
|
+
export const statusIconSvg = (mark) => `<svg class="status-icon status-icon--${mark}" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${ICON_PATHS[mark]}</svg>`;
|
|
286
|
+
/** The detail rows as server HTML: quiet rows, colour only on the icon. */
|
|
287
|
+
export function statusDetailsHtml(status) {
|
|
288
|
+
if (status.details.length === 0)
|
|
289
|
+
return "";
|
|
290
|
+
return `<ul class="status-details" aria-label="Details">` + status.details.map(one => {
|
|
291
|
+
const text = one.href === null ? escapeHtml(one.text) : `<a href="${escapeHtml(one.href)}">${escapeHtml(one.text)}</a>`;
|
|
292
|
+
const action = one.action === null ? "" : one.action.href === null ? `<span class="status-detail-act">${escapeHtml(one.action.label)}</span>` : `<a class="status-detail-act" href="${escapeHtml(one.action.href)}">${escapeHtml(one.action.label)}</a>`;
|
|
293
|
+
return `<li class="status-detail status-detail--${one.mark}" data-status-detail="${one.key}" data-mark="${one.mark}">${statusIconSvg(one.mark)}<span class="status-detail-label">${escapeHtml(one.label)}</span><span class="status-detail-text"><span>${text}</span>${action}</span></li>`;
|
|
294
|
+
}).join("") + `</ul>`;
|
|
295
|
+
}
|
|
296
|
+
/** The technical reasons, one tap away. */
|
|
297
|
+
export function statusWhyHtml(status, extra = [], diagnostics = []) {
|
|
298
|
+
const lines = [...new Set([...status.details.flatMap(one => one.why === null ? [] : [`${one.label}: ${one.why}`]), ...status.why, ...extra])];
|
|
299
|
+
if (lines.length === 0 && diagnostics.length === 0)
|
|
300
|
+
return "";
|
|
301
|
+
return `<details class="status-why"><summary>Details</summary>${lines.map(one => `<p class="meta">${escapeHtml(one)}</p>`).join("")}` +
|
|
302
|
+
diagnostics.map(one => `<p class="meta" data-work-diagnostic="${escapeHtml(one.token)}">${escapeHtml(one.label)} · ${escapeHtml(one.detail)}</p>`).join("") + `</details>`;
|
|
303
|
+
}
|
|
304
|
+
/** Shared CSS for the server-rendered status: neutral rows, coloured icons. */
|
|
305
|
+
export const TASK_STATUS_CSS = `.status-headline{display:flex;align-items:center;gap:.6rem;margin:0;font-size:1.125rem;font-weight:600;color:var(--foreground)}.status-headline i{width:.625rem;height:.625rem;border-radius:999px;flex-shrink:0;background:var(--muted-foreground)}[data-headline-tone=live] .status-headline i{background:var(--so-info,#0d74ce)}[data-headline-tone=attention] .status-headline i{background:var(--so-attention,var(--attention))}[data-headline-tone=ready] .status-headline i{background:transparent;box-shadow:inset 0 0 0 2px var(--so-success,#218358)}[data-headline-tone=success] .status-headline i{background:var(--so-success,#218358)}[data-headline-tone=danger] .status-headline i{background:var(--so-danger,#c4320a)}.status-sentence{margin:.35rem 0 0;color:var(--muted-foreground)}.status-details{list-style:none;margin:.75rem 0 0;padding:.25rem 0 0;border-top:1px solid var(--so-line,var(--border))}.status-detail{display:grid;grid-template-columns:14px 7.5rem minmax(0,1fr);align-items:center;gap:.6rem;padding:.4rem 0;font-size:.8125rem;color:var(--foreground)}.status-detail .status-icon{align-self:center;color:var(--muted-foreground)}.status-detail--ok .status-icon{color:var(--so-success,#218358)}.status-detail--running .status-icon{color:var(--so-info,#0d74ce)}.status-detail--note .status-icon{color:var(--so-warning,#ab6400)}.status-detail--failed .status-icon{color:var(--so-danger,#c4320a)}.status-detail-label{color:var(--muted-foreground)}.status-detail-text{display:flex;flex-wrap:wrap;align-items:baseline;gap:0 .6rem;min-width:0;overflow-wrap:anywhere}.status-detail-text a{color:inherit}.status-detail-act{font-weight:600;white-space:nowrap}.status-detail--note .status-detail-act{color:var(--so-warning,#ab6400)}@media(max-width:480px){.status-detail{grid-template-columns:14px 6.5rem minmax(0,1fr);min-height:2.75rem;padding:.25rem 0}.status-detail-text a{display:inline-block;padding:.75rem 0;margin:-.75rem 0}}.status-why{border:0;padding:0;margin:.25rem 0 0;background:transparent;box-shadow:none}.status-why>summary{cursor:pointer;min-height:2.75rem;display:list-item;align-content:center;font-size:.8125rem;color:var(--muted-foreground)}.status-why p{margin:.25rem 0 .5rem}.receipt-note{display:flex;align-items:center;gap:.45rem;font-size:.8125rem;color:var(--foreground)}.receipt-note .status-icon{color:var(--so-warning,#ab6400)}`;
|
|
306
|
+
/** A dispatch diagnosis as its stage, for the phone and CLI reads that hold
|
|
307
|
+
* no assignment. A done task is Complete once a person marked its result;
|
|
308
|
+
* a refuted result whose project check failed is Failed. */
|
|
309
|
+
export function stageOfDispatch(d, options = {}) {
|
|
310
|
+
if (d.code === "complete" || d.code.startsWith("review-") || d.code === "reviewing")
|
|
311
|
+
return { stage: options.completed ? "complete" : "finished" };
|
|
312
|
+
if (d.code === "proof-refuted")
|
|
313
|
+
return /approved check failed|checks failed/i.test(d.detail ?? "") ? { stage: "failed" } : { stage: "needs-you", need: "other" };
|
|
314
|
+
if (d.code === "needs-verification")
|
|
315
|
+
return { stage: "needs-you", need: "other" };
|
|
316
|
+
return stageOfCode(d.code, { needsPerson: d.condition === "waiting" && d.action !== null, planning: d.condition === "running" && d.role === "planner", operatorHold: d.action === "unhold" });
|
|
317
|
+
}
|