toolroll 0.9.56 → 0.9.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/action-ledger.js +0 -4
  3. package/dist/actor.d.ts +0 -10
  4. package/dist/actor.js +1 -18
  5. package/dist/api-tokens.d.ts +33 -2
  6. package/dist/api-tokens.js +39 -11
  7. package/dist/assignment-adapters.d.ts +5 -0
  8. package/dist/assignment-delivery.js +1 -1
  9. package/dist/assignment-status.js +1 -1
  10. package/dist/assignment.d.ts +24 -3
  11. package/dist/assignment.js +39 -12
  12. package/dist/browser-workspace.js +1 -1
  13. package/dist/chat-delivery.js +3 -3
  14. package/dist/chat-voice.js +1 -1
  15. package/dist/check-levels.d.ts +3 -0
  16. package/dist/check-levels.js +5 -0
  17. package/dist/cli-http.d.ts +7 -0
  18. package/dist/cli-http.js +30 -4
  19. package/dist/cli.js +2 -1
  20. package/dist/coordinator.d.ts +2 -1
  21. package/dist/coordinator.js +22 -31
  22. package/dist/desktop-update.d.ts +1 -1
  23. package/dist/dispose.js +0 -5
  24. package/dist/export.js +2 -2
  25. package/dist/lead-status.d.ts +1 -1
  26. package/dist/lead-status.js +2 -2
  27. package/dist/mcp-http.js +5 -5
  28. package/dist/operate.js +19 -9
  29. package/dist/password-bearer.d.ts +27 -0
  30. package/dist/password-bearer.js +59 -0
  31. package/dist/proof.d.ts +9 -0
  32. package/dist/proof.js +12 -0
  33. package/dist/release-coverage.d.ts +16 -0
  34. package/dist/release-coverage.js +92 -0
  35. package/dist/remote-command.js +0 -2
  36. package/dist/remote-exec.d.ts +19 -0
  37. package/dist/remote-exec.js +9 -8
  38. package/dist/request-budget.d.ts +1 -1
  39. package/dist/scope.js +2 -1
  40. package/dist/serve.js +15 -0
  41. package/dist/server/chat.js +8 -6
  42. package/dist/server/edge-refusal.d.ts +2 -0
  43. package/dist/server/edge-refusal.js +8 -6
  44. package/dist/server/guards.js +5 -4
  45. package/dist/server/http.d.ts +2 -0
  46. package/dist/server/remote.js +6 -13
  47. package/dist/server/render-results.d.ts +4 -1
  48. package/dist/server/render-results.js +23 -9
  49. package/dist/server/route-table.js +0 -3
  50. package/dist/server/tasks.js +28 -25
  51. package/dist/session-cli.d.ts +38 -9
  52. package/dist/session-cli.js +141 -154
  53. package/dist/session-contract.js +2 -2
  54. package/dist/session-remote.d.ts +19 -0
  55. package/dist/session-remote.js +86 -0
  56. package/dist/store.d.ts +31 -11
  57. package/dist/store.js +276 -95
  58. package/dist/task-outcome-cli.js +3 -1
  59. package/dist/task-status.d.ts +59 -7
  60. package/dist/task-status.js +132 -16
  61. package/dist/team-cli.d.ts +3 -0
  62. package/dist/team-cli.js +23 -3
  63. package/dist/team-runtime.js +1 -1
  64. package/dist/telegram-status.d.ts +11 -4
  65. package/dist/telegram-status.js +17 -10
  66. package/dist/telegram.js +4 -4
  67. package/dist/toolroll-update.js +3 -1
  68. package/dist/work-index.d.ts +6 -1
  69. package/dist/work-index.js +50 -8
  70. package/dist/workspace-ui.js +2 -1
  71. package/package.json +1 -1
  72. package/dist/session-http.d.ts +0 -24
  73. package/dist/session-http.js +0 -170
  74. package/dist/session-server.d.ts +0 -17
  75. package/dist/session-server.js +0 -39
@@ -56,6 +56,24 @@ export type ChecksFact = {
56
56
  running?: "quick" | "full" | null;
57
57
  /** A batch check (batch-checks.ts): waiting for its batch, or how it was checked. */
58
58
  batch?: ChecksBatch | null;
59
+ /** Off: the passing full release check whose commit contains this one (release-coverage.ts), read at read time. */
60
+ release?: {
61
+ run: number;
62
+ head: string;
63
+ } | null;
64
+ };
65
+ /** What checks Off leaves for a release: `pending` until a passing full release check contains the commit, then
66
+ * `covered`. `running`: a check is waiting for its batch or running on this commit. */
67
+ export type ReleaseState = "pending" | "covered" | "running";
68
+ export declare const CHECKED_AT_RELEASE = "Checked at release";
69
+ export declare function releaseStateOf(checks: Pick<ChecksFact, "status" | "level" | "release"> | null | undefined): ReleaseState | undefined;
70
+ /** How the project check stands for requirements that rest on it, read now: `covered` once a check passed on this
71
+ * commit (its own, a follow-up, or a release check containing it), `pending` while checks Off wait for a release. */
72
+ export declare function checkBackingOf(checks: Pick<ChecksFact, "status" | "level" | "release" | "running" | "batch"> | null | undefined): ReleaseState | undefined;
73
+ /** What stands between a finished result and Complete (`completionBlockersOf`): each with the exact next step. */
74
+ export type CompletionBlocker = {
75
+ key: "check-failed" | "check-missing" | "criteria" | "high";
76
+ message: string;
59
77
  };
60
78
  /** `waiting`: the result waits to be checked with others. Otherwise how its check ran: `together` on the
61
79
  * temporary batch commit `tested` with `peers`; `split` (its batch failed), `conflict` or `alone` on its own commit. */
@@ -94,14 +112,22 @@ export type TaskStatusFacts = {
94
112
  checks?: ChecksFact | null;
95
113
  pullRequest?: PullRequestFact | null;
96
114
  /** `unverified`: the report is refuted, so no requirement counts as met however it was marked. `missed`: the ones it
97
- * failed or left unanswered. */
115
+ * failed or left unanswered. `atRelease`: checks Off, waiting for a release check. `unshown`: a required screenshot
116
+ * is missing. `ask`: what a person is asked to look at first (a requirement they check, or a missing screenshot). */
98
117
  requirements?: {
99
118
  met: number;
100
119
  total: number;
101
120
  yours: number;
102
121
  missed?: number;
103
122
  unverified?: boolean;
123
+ atRelease?: number;
124
+ unshown?: number;
125
+ ask?: string | null;
104
126
  } | null;
127
+ /** A person accepted this result's proof: their own check and a recorded exception need nothing more. */
128
+ accepted?: boolean;
129
+ /** What refuses Complete (`completionBlockersOf`); the first one is the sentence when it holds the result back. */
130
+ blockers?: readonly CompletionBlocker[];
105
131
  evidence?: {
106
132
  shortened: number;
107
133
  missing: number;
@@ -124,11 +150,14 @@ export type TaskStatusFacts = {
124
150
  /** What the person calls their lead (Settings → Lead); "Lead" when unnamed. */
125
151
  leadName?: string;
126
152
  };
127
- export declare function headlineOf(facts: Pick<TaskStatusFacts, "stage" | "checks" | "report">): Headline;
153
+ export declare function headlineOf(facts: Pick<TaskStatusFacts, "stage" | "checks" | "report" | "requirements" | "accepted" | "blockers">): Headline;
128
154
  /** What the lead's claim says, in place of "waits for you" (lead-voice.ts). */
129
155
  export declare const leadOnIt: (name?: string) => string;
130
156
  export declare const leadLapsed: (name?: string) => string;
131
- export declare function taskStatusOf(facts: TaskStatusFacts): TaskStatus;
157
+ /** The one status every surface shows (console list, task and result pages, CLI, chat, phone, `status`). */
158
+ export declare function statusOf(facts: TaskStatusFacts): TaskStatus;
159
+ /** The name most surfaces already import. */
160
+ export declare const taskStatusOf: typeof statusOf;
132
161
  export declare function plainReasonOf(stage: TaskStage, code: string, detail: string | null | undefined): string | null;
133
162
  export declare function stageOfCode(code: string, options?: {
134
163
  needsPerson?: boolean;
@@ -165,18 +194,41 @@ export declare function assignmentStageOf(assignment: Pick<AssignmentSnapshot, "
165
194
  wait?: WaitKey;
166
195
  };
167
196
  type RequirementRow = {
197
+ id?: string;
198
+ statement?: string;
168
199
  state: string;
200
+ requiredEvidence?: readonly string[];
201
+ answered?: readonly {
202
+ kind: string;
203
+ }[];
204
+ detail?: readonly string[];
169
205
  assessment?: {
170
206
  evidenceState?: string;
207
+ detail?: readonly string[];
171
208
  } | undefined;
172
209
  review?: unknown;
173
210
  };
174
211
  /** One requirement's state in words: the card's Requirements row and the Checks tab read this one source, so they
175
- * can't disagree. A refuted report verifies none of its requirements, so each reads Unverified, as the card does. */
176
- export type RequirementWord = "Met" | "You check" | "Not shown yet" | "Not met" | "Unverified";
177
- export declare function requirementWordOf(row: RequirementRow, verdict?: string | null): RequirementWord;
212
+ * can't disagree. A refuted report verifies none of its requirements, so each reads Unverified, as the card does.
213
+ * With checks Off, a requirement that waits only on the check reads Checked at release, then Met once a passing full
214
+ * release check contains the commit. */
215
+ export type RequirementWord = "Met" | "You check" | "Not shown yet" | "Checked at release" | "Waiting for checks" | "Not met" | "Unverified";
216
+ export declare function requirementWordOf(row: RequirementRow, verdict?: string | null, release?: ReleaseState): RequirementWord;
178
217
  /** Requirements from the stored matrix: met, the ones only a person confirms, and the total. */
179
- export declare function requirementsOf(matrix: readonly RequirementRow[] | null | undefined): NonNullable<TaskStatusFacts["requirements"]> | null;
218
+ export declare function requirementsOf(matrix: readonly RequirementRow[] | null | undefined, release?: ReleaseState): NonNullable<TaskStatusFacts["requirements"]> | null;
219
+ /** What refuses Complete, for `task complete`, the console and chat completion and deploy-candidate alike: a failed
220
+ * check; a required check that is missing (checks deliberately Off are not missing: they are checked at release);
221
+ * an unresolved requirement (a person's own check is theirs to give by completing; a recorded acceptance resolves
222
+ * the rest); an unresolved HIGH review finding. A release check that covers the commit counts as passed. */
223
+ export declare function completionBlockersOf(f: {
224
+ report: boolean;
225
+ checks: Pick<ChecksFact, "status" | "exitCode" | "level" | "release" | "running" | "batch"> | null;
226
+ checkRequired: boolean;
227
+ matrix: readonly RequirementRow[] | null | undefined;
228
+ verdict: string | null | undefined;
229
+ accepted: boolean;
230
+ high: number;
231
+ }): CompletionBlocker[];
180
232
  /** A refuted report verifies none of its requirements, whatever it marked met (a person's acceptance doesn't change that). */
181
233
  export declare function unverifiedWhenRefuted(requirements: NonNullable<TaskStatusFacts["requirements"]> | null, verdict: string | null | undefined): NonNullable<TaskStatusFacts["requirements"]> | null;
182
234
  /** Saved evidence health from the receipt's artifact list and its caveats. */
@@ -27,6 +27,17 @@ export const CHECKS_LABEL = "Project checks";
27
27
  export const PR_CI_LABEL = "PR CI";
28
28
  /** The one verb for opening a saved result, on every surface. */
29
29
  export const OPEN_RESULT = "Open result";
30
+ export const CHECKED_AT_RELEASE = "Checked at release";
31
+ export function releaseStateOf(checks) {
32
+ if (checks?.level !== "off" || checks.status === "passed" || checks.status === "failed")
33
+ return undefined;
34
+ return checks.release != null ? "covered" : "pending";
35
+ }
36
+ /** How the project check stands for requirements that rest on it, read now: `covered` once a check passed on this
37
+ * commit (its own, a follow-up, or a release check containing it), `pending` while checks Off wait for a release. */
38
+ export function checkBackingOf(checks) {
39
+ return checks?.status === "passed" ? "covered" : checksUnderway(checks) ? "running" : releaseStateOf(checks);
40
+ }
30
41
  /** "T2", "T2 and T3", "T2, T3 and 2 more": short enough for one line. The full list is on the result. */
31
42
  export function peerWords(peers, shown = 2) {
32
43
  if (peers.length <= shown)
@@ -43,6 +54,23 @@ const BATCH_WHY = {
43
54
  conflict: "It conflicted with another result when merged for a batch check, so it was checked on its own.",
44
55
  };
45
56
  const short = (sha) => sha !== null && /^[a-f0-9]{7,40}$/.test(sha) ? sha.slice(0, 7) : null;
57
+ /** A check waiting for its batch or running on this commit: the result reads Ready meanwhile, not Needs you. */
58
+ const checksUnderway = (checks) => checks?.batch?.state === "waiting" || checks?.running != null;
59
+ /** A finished result a person still has to act on before it can be complete: an unresolved HIGH finding or unmet
60
+ * requirement, a missing check, or a requirement only they can confirm. Failed checks read Failed instead. */
61
+ function resultAsk(facts) {
62
+ if (facts.report || facts.checks?.status === "failed")
63
+ return null;
64
+ const blocker = facts.blockers?.find(one => one.key !== "check-failed" && !(one.key === "check-missing" && checksUnderway(facts.checks)));
65
+ if (blocker !== undefined)
66
+ return blocker.message;
67
+ const req = facts.requirements;
68
+ if (req == null || facts.accepted === true || req.unverified === true)
69
+ return null;
70
+ if ((req.yours > 0 || (req.unshown ?? 0) > 0) && req.ask != null)
71
+ return req.ask;
72
+ return null;
73
+ }
46
74
  export function headlineOf(facts) {
47
75
  switch (facts.stage) {
48
76
  case "queued": return "Queued";
@@ -57,7 +85,10 @@ export function headlineOf(facts) {
57
85
  return "Ready for review";
58
86
  if (facts.checks?.status === "failed")
59
87
  return "Failed";
60
- // Off: ready like any build; the sentence and the Checks row say no check ran.
88
+ // A requirement a person checks, a missing screenshot, an unmet requirement or a HIGH finding: Needs you, with the ask.
89
+ if (resultAsk(facts) !== null)
90
+ return "Needs you";
91
+ // Off: ready like any build; the sentence and the Checks row say it is checked at release.
61
92
  return "Ready for review";
62
93
  case "complete": return "Complete";
63
94
  case "failed": return "Failed";
@@ -71,15 +102,17 @@ function sentenceOf(headline, facts) {
71
102
  case "Queued": return reason ?? "Waiting for a worker.";
72
103
  case "Planning": return reason ?? "The lead is writing the plan.";
73
104
  // One plain sentence per need (needs-you.ts); a recorded reason only where the need keeps one.
74
- case "Needs you": return needSentence(facts.need ?? "other", reason, facts.needContext);
105
+ case "Needs you": return facts.stage === "finished" ? resultAsk(facts) ?? needSentence("review-result", reason, facts.needContext) : needSentence(facts.need ?? "other", reason, facts.needContext);
75
106
  case "Waiting": return waitSentence(facts.wait, reason, facts.needContext);
76
107
  case "Building": return facts.stage === "checking" ? "Checks are running on the change."
77
108
  : facts.stage === "reviewing" ? "Reviewing the change before it reaches you." : reason ?? "An agent is working on it.";
78
109
  case "Ready for review":
79
110
  if (facts.report)
80
111
  return "The report is ready to read. Read it, then mark it complete.";
112
+ if (releaseStateOf(facts.checks) === "covered")
113
+ return `Checked at release on ${short(facts.checks.release.head) ?? "its release"}. Review the change, then mark it complete.`;
81
114
  if (facts.checks?.level === "off" && facts.checks.status !== "passed")
82
- return "Checks are off for this project. Review the change, then mark it complete.";
115
+ return "Checked at release. Review the change, then mark it complete.";
83
116
  if (facts.checks?.status === "passed") {
84
117
  // The Checks row names the others and the batch commit; the sentence doesn't repeat them.
85
118
  const peers = facts.checks.batch?.state === "together" ? facts.checks.batch.peers.length : 0;
@@ -135,8 +168,10 @@ function detailsOf(headline, facts) {
135
168
  row("checks", CHECKS_LABEL, `${quick ? "Quick checks failed" : "Failed"}${checks.exitCode === null ? "" : ` (exit ${checks.exitCode})`}`, problem(headline), headline === "Failed" ? { href: checksHref } : { action: { label: "See what failed", href: checksHref } });
136
169
  else if (checks.status === "running")
137
170
  row("checks", CHECKS_LABEL, "Running", "running");
171
+ else if (checks.level === "off" && checks.release != null)
172
+ row("checks", CHECKS_LABEL, `Passed at release on ${short(checks.release.head) ?? `build #${checks.release.run}`}`, "ok", { href: checksHref });
138
173
  else if (checks.level === "off")
139
- row("checks", CHECKS_LABEL, "Off for this project", "none", runChecks === null ? {} : { action: runChecks });
174
+ row("checks", CHECKS_LABEL, CHECKED_AT_RELEASE, "none", runChecks === null ? {} : { action: runChecks });
140
175
  else if (checks.status === "not-run")
141
176
  row("checks", CHECKS_LABEL, "Didn't run", "none", runChecks === null ? {} : { action: runChecks });
142
177
  // A saved check that can't be read is run again where that is possible.
@@ -173,8 +208,9 @@ function detailsOf(headline, facts) {
173
208
  else if (req != null && req.total > 0 && req.unverified === true)
174
209
  row("requirements", "Requirements", "Unverified", "none");
175
210
  else if (req != null && req.total > 0) {
176
- const unmet = req.total - req.met - req.yours;
177
- const text = `${req.met} of ${req.total} met${req.yours > 0 ? ` · You check ${req.yours}` : ""}`;
211
+ const atRelease = req.atRelease ?? 0;
212
+ const unmet = req.total - req.met - req.yours - atRelease;
213
+ const text = `${req.met} of ${req.total} met${atRelease > 0 ? ` · ${atRelease} at release` : ""}${req.yours > 0 ? ` · You check ${req.yours}` : ""}`;
178
214
  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 } } : {});
179
215
  }
180
216
  const evidence = facts.evidence;
@@ -203,19 +239,23 @@ function leadSentence(headline, facts) {
203
239
  return `${facts.checks.level === "quick" ? "Quick checks" : "Checks"} failed${sha === null ? "" : ` on ${sha}`}. ${leadOnIt(facts.leadName)}`;
204
240
  return leadOnIt(facts.leadName);
205
241
  }
206
- export function taskStatusOf(facts) {
242
+ /** The one status every surface shows (console list, task and result pages, CLI, chat, phone, `status`). */
243
+ export function statusOf(facts) {
207
244
  const read = headlineOf(facts);
208
245
  const lead = facts.lead != null && LEAD_HEADLINES.has(read) ? facts.lead : null;
209
246
  // The lead has it: nothing waits on the person, so a Needs you reads Waiting.
210
247
  const headline = lead === "on-it" && read === "Needs you" ? "Waiting" : read;
211
- // Every Needs you carries the one action that resolves it, worded the same everywhere.
212
- const need = headline === "Needs you" ? { key: facts.need ?? "other", action: NEEDS[facts.need ?? "other"].action } : null;
248
+ // Every Needs you carries the one action that resolves it, worded the same everywhere; a finished result's is Open result.
249
+ const needKey = facts.stage === "finished" ? "review-result" : facts.need ?? "other";
250
+ const need = headline === "Needs you" ? { key: needKey, action: NEEDS[needKey].action } : null;
213
251
  const ownWords = need !== null && (need.key === "other" || need.key === "review-result") && facts.action != null;
214
252
  const primaryAction = need === null || ownWords ? facts.action ?? null : { label: need.action.label, href: facts.action?.href ?? null };
215
253
  const sentence = lead === "on-it" ? leadSentence(headline, facts) : lead === "lapsed" ? `${leadLapsed(facts.leadName)} ${sentenceOf(headline, facts)}` : sentenceOf(headline, facts);
216
254
  return { headline, tone: HEADLINE_TONE[headline], sentence, details: detailsOf(headline, facts),
217
255
  primaryAction, why: [...new Set(facts.why ?? [])].filter(one => one.trim() !== ""), need };
218
256
  }
257
+ /** The name most surfaces already import. */
258
+ export const taskStatusOf = statusOf;
219
259
  // ---- reading the existing projections ----------------------------------------
220
260
  /** Every dispatch or work-index code, as the stage it means. Unknown codes that
221
261
  * wait on a person stay Needs you; everything else waits in the queue. */
@@ -316,19 +356,93 @@ export function assignmentStageOf(assignment, work, planning = false) {
316
356
  }
317
357
  /** Evidence that passed and only awaits the retired assessment step is met. */
318
358
  const requirementMet = (row) => row.state === "pass" || (row.assessment?.evidenceState === "pass" && (row.review ?? null) === null);
319
- export function requirementWordOf(row, verdict) {
359
+ /** A requirement whose only gap is the project check: every other kind it needs was shown, none is a person's check. */
360
+ const onlyCheckMissing = (row) => {
361
+ const needs = row.requiredEvidence ?? [];
362
+ const answered = new Set((row.answered ?? []).map(one => one.kind));
363
+ if (row.state !== "missing" || row.review != null || row.assessment?.evidenceState === "failed" ||
364
+ !needs.includes("check") || needs.includes("manual-review") || !needs.every(kind => kind === "check" || answered.has(kind)))
365
+ return false;
366
+ // `answered` contains claims, not validated evidence. In legacy matrices a missing
367
+ // check takes precedence over an invalid screenshot/changed path in `state`.
368
+ // Only the stored validator's check-only gaps establish that every other
369
+ // reference resolved. Direct assessment keeps these outcomes in its own detail.
370
+ const detail = row.assessment?.detail ?? row.detail ?? [];
371
+ const checkGaps = new Set([
372
+ `criterion "${row.id}" requires check evidence, which the proof does not reference`,
373
+ `criterion "${row.id}" is waiting for the final check`,
374
+ `criterion "${row.id}" needs a passing approved project check`,
375
+ ]);
376
+ return (needs.every(kind => kind === "check") || detail.length > 0) && detail.every(one => checkGaps.has(one));
377
+ };
378
+ /** A required screenshot nobody captured. */
379
+ const screenshotMissing = (row) => (row.requiredEvidence ?? []).includes("screenshot") && !(row.answered ?? []).some(one => one.kind === "screenshot");
380
+ export function requirementWordOf(row, verdict, release) {
320
381
  if (verdict === "refuted")
321
382
  return "Unverified";
322
383
  if (requirementMet(row))
323
384
  return "Met";
324
- return row.state === "manual-review" ? "You check" : row.state === "missing" ? "Not shown yet" : "Not met";
385
+ if (release !== undefined && onlyCheckMissing(row))
386
+ return release === "covered" ? "Met" : release === "running" ? "Waiting for checks" : CHECKED_AT_RELEASE;
387
+ if (row.state === "manual-review")
388
+ return "You check";
389
+ return row.state === "missing" || screenshotMissing(row) ? "Not shown yet" : "Not met";
390
+ }
391
+ const ASK_STATEMENT = 140;
392
+ const quoted = (statement) => {
393
+ const one = (statement ?? "").replace(/\s+/g, " ").trim();
394
+ return one.length > ASK_STATEMENT ? `${one.slice(0, ASK_STATEMENT - 1)}…` : one;
395
+ };
396
+ /** The ask for one unresolved requirement: what is wrong with it, and the two ways on. */
397
+ function unresolvedAsk(row, word) {
398
+ const what = word === "Not shown yet" && screenshotMissing(row) ? "A screenshot is missing for"
399
+ : word === "Not shown yet" ? "Nothing shows this requirement is met" : word === "Unverified" ? "This requirement couldn't be verified" : "This requirement isn't met";
400
+ return `${what}: ${quoted(row.statement) || row.id || "a requirement"}. Ask for changes, or accept the result with a reason.`;
325
401
  }
326
402
  /** Requirements from the stored matrix: met, the ones only a person confirms, and the total. */
327
- export function requirementsOf(matrix) {
403
+ export function requirementsOf(matrix, release) {
328
404
  if (!matrix || matrix.length === 0)
329
405
  return null;
330
- return { met: matrix.filter(requirementMet).length, total: matrix.length, yours: matrix.filter(row => requirementWordOf(row) === "You check").length,
331
- missed: matrix.filter(row => !requirementMet(row) && (row.state === "failed" || row.state === "missing")).length };
406
+ const words = matrix.map(row => requirementWordOf(row, null, release));
407
+ const yours = matrix.find((_, index) => words[index] === "You check");
408
+ const unshown = matrix.filter((row, index) => words[index] === "Not shown yet" && screenshotMissing(row));
409
+ const ask = yours !== undefined ? `Check this requirement yourself, then mark it complete: ${quoted(yours.statement)}`
410
+ : unshown.length > 0 ? unresolvedAsk(unshown[0], "Not shown yet") : null;
411
+ return { met: words.filter(word => word === "Met").length, total: matrix.length, yours: words.filter(word => word === "You check").length,
412
+ missed: words.filter(word => word === "Not met" || word === "Not shown yet").length,
413
+ ...(release !== "pending" ? {} : { atRelease: words.filter(word => word === CHECKED_AT_RELEASE).length }),
414
+ ...(unshown.length === 0 ? {} : { unshown: unshown.length }),
415
+ ...(ask === null ? {} : { ask }) };
416
+ }
417
+ /** What refuses Complete, for `task complete`, the console and chat completion and deploy-candidate alike: a failed
418
+ * check; a required check that is missing (checks deliberately Off are not missing: they are checked at release);
419
+ * an unresolved requirement (a person's own check is theirs to give by completing; a recorded acceptance resolves
420
+ * the rest); an unresolved HIGH review finding. A release check that covers the commit counts as passed. */
421
+ export function completionBlockersOf(f) {
422
+ const blockers = [];
423
+ const release = releaseStateOf(f.checks);
424
+ const backing = checkBackingOf(f.checks);
425
+ if (!f.report && f.checks?.status === "failed") {
426
+ blockers.push({ key: "check-failed", message: `Checks failed${f.checks.exitCode === null ? "" : ` (exit ${f.checks.exitCode})`}. Fix them and run checks again, or ask for changes.` });
427
+ }
428
+ else if (!f.report && f.checkRequired && f.checks?.status !== "passed" && release === undefined) {
429
+ blockers.push({ key: "check-missing", message: checksUnderway(f.checks) ? "Its project check hasn't finished yet. Mark it complete once it passes."
430
+ : "No passing project check is recorded for this result. Run checks on it, then mark it complete." });
431
+ }
432
+ if (!f.accepted && f.checks?.status !== "failed") {
433
+ const refuted = f.verdict === "refuted";
434
+ const unresolved = (f.matrix ?? []).map(row => ({ row, word: requirementWordOf(row, refuted ? "refuted" : null, backing) }))
435
+ .filter(one => one.word === "Not met" || one.word === "Not shown yet" || one.word === "Unverified");
436
+ if (unresolved.length === 1)
437
+ blockers.push({ key: "criteria", message: unresolvedAsk(unresolved[0].row, unresolved[0].word) });
438
+ else if (unresolved.length > 1) {
439
+ const ids = unresolved.map(one => one.row.id ?? "?");
440
+ blockers.push({ key: "criteria", message: `${unresolved.length} requirements ${unresolved.every(one => one.word === "Unverified") ? "couldn't be verified" : "aren't met yet"} (${ids.slice(0, 6).join(", ")}${ids.length > 6 ? ", …" : ""}). Ask for changes, or accept the result with a reason.` });
441
+ }
442
+ }
443
+ if (f.high > 0)
444
+ blockers.push({ key: "high", message: `The automatic review found ${f.high === 1 ? "a high-severity problem" : `${f.high} high-severity problems`}. Ask for changes before marking it complete.` });
445
+ return blockers;
332
446
  }
333
447
  /** A refuted report verifies none of its requirements, whatever it marked met (a person's acceptance doesn't change that). */
334
448
  export function unverifiedWhenRefuted(requirements, verdict) {
@@ -355,7 +469,7 @@ export function assignmentStatusFacts(assignment, options = {}) {
355
469
  const checks = receipt === null || !withResult ? null
356
470
  : { status: receipt.checks.status, exitCode: receipt.checks.exitCode, head: receipt.head,
357
471
  ...(receipt.checks.level == null ? {} : { level: receipt.checks.level }), ...(receipt.checks.running == null ? {} : { running: receipt.checks.running }),
358
- ...(receipt.checks.batch == null ? {} : { batch: receipt.checks.batch }) };
472
+ ...(receipt.checks.batch == null ? {} : { batch: receipt.checks.batch }), ...(receipt.checks.release == null ? {} : { release: receipt.checks.release }) };
359
473
  const publication = assignment.publication;
360
474
  const pullRequest = options.pullRequest !== undefined ? options.pullRequest
361
475
  : publication === null ? (withResult && !report ? { state: "none", number: null, url: null, ci: null, error: null } : null)
@@ -368,7 +482,9 @@ export function assignmentStatusFacts(assignment, options = {}) {
368
482
  stage, ...(need === undefined ? {} : { need }), ...(wait === undefined ? {} : { wait }), ...(build === null ? {} : { needContext: { build } }), reason, report,
369
483
  checks,
370
484
  pullRequest: withResult ? pullRequest : null,
371
- requirements: withResult ? unverifiedWhenRefuted(requirementsOf(receipt?.proof?.matrix), receipt?.proof?.verdict) : null,
485
+ requirements: withResult ? unverifiedWhenRefuted(requirementsOf(receipt?.proof?.matrix, checkBackingOf(checks)), assignment.readiness?.verdict ?? receipt?.proof?.verdict) : null,
486
+ ...(withResult && receipt?.proofAcceptance != null ? { accepted: true } : {}),
487
+ ...(withResult && (assignment.readiness?.blockers.length ?? 0) > 0 ? { blockers: assignment.readiness.blockers } : {}),
372
488
  evidence: withResult ? options.evidence ?? evidenceOf(receipt, [...(receipt?.caveats ?? []), ...assignment.attention]) : null,
373
489
  completedBy: assignment.completion === null ? null : assignment.completion.lead === true ? "the lead" : assignment.completion.actor.replace(/^(?:operator|coordinator|lead):/, ""),
374
490
  action: options.action ?? null,
@@ -14,6 +14,9 @@ export type TeamCliOptions = {
14
14
  export declare const TEAM_CLI_ACTIONS: readonly ['connect', 'lead list', 'lead create', 'lead update', 'lead member', 'lead transfer', 'conversation list', 'conversation create', 'conversation show', 'conversation member', 'conversation edit', 'conversation withdraw', 'conversation read', 'conversation follow', 'conversation stop'];
15
15
  export declare class UsageError extends Error {
16
16
  }
17
+ /** Credentials are never sent to a URL with embedded authority, an API path, query, fragment or an insecure remote
18
+ * host. Fetch cannot follow redirects. */
19
+ export declare function serviceOrigin(raw: string): string;
17
20
  type Profile = {
18
21
  origin: string;
19
22
  account: string;
package/dist/team-cli.js CHANGED
@@ -5,11 +5,11 @@ import { randomBytes } from 'node:crypto';
5
5
  import { createInterface } from 'node:readline/promises';
6
6
  import { setTimeout as delay } from 'node:timers/promises';
7
7
  import { envelopeJson } from './envelope.js';
8
- import { sessionServiceOrigin } from './session-cli.js';
9
8
  import { databasePath } from './store.js';
10
9
  import { configBase, namedPath } from './names.js';
11
10
  import { parseApiToken } from './api-tokens.js';
12
11
  import { deprecationWarning } from './deprecations.js';
12
+ import { PASSWORD_PROFILE_WARNING } from './password-bearer.js';
13
13
  /** The central service's own lead verbs; every other `lead …` (token, say, or none) is the local lead's. */
14
14
  const CENTRAL_LEAD_ACTIONS = ['list', 'create', 'update', 'member', 'transfer'];
15
15
  /** D5: deprecated (deprecations.ts). Each still runs this release, hidden from help and the contract, with one warning on stderr. */
@@ -45,6 +45,22 @@ existing local commands. Credentials stay in a private file, never in URLs or ar
45
45
  Reads and attachment do not start model work. Server permissions remain authoritative.`;
46
46
  export class UsageError extends Error {
47
47
  }
48
+ /** Credentials are never sent to a URL with embedded authority, an API path, query, fragment or an insecure remote
49
+ * host. Fetch cannot follow redirects. */
50
+ export function serviceOrigin(raw) {
51
+ let url;
52
+ try {
53
+ url = new URL(raw);
54
+ }
55
+ catch {
56
+ throw new UsageError('The service origin must be HTTPS or loopback HTTP.');
57
+ }
58
+ if (url.username || url.password || url.search || url.hash || url.pathname !== '/')
59
+ throw new UsageError('The service origin must contain only the origin, with no credentials, path, query or fragment.');
60
+ if (url.protocol !== 'https:' && !(url.protocol === 'http:' && ['127.0.0.1', '[::1]', 'localhost'].includes(url.hostname)))
61
+ throw new UsageError('The service origin requires HTTPS except for loopback HTTP.');
62
+ return url.origin;
63
+ }
48
64
  /** Transport metadata stays local to the CLI; only an actual HTTP 429 permits a follower read retry. */
49
65
  const RETRY_AFTER_MS = Symbol('retry-after-ms');
50
66
  function retryDelay(header) {
@@ -104,7 +120,7 @@ function profiles(options) {
104
120
  if (!record(parsed) || parsed.version !== 1 || typeof parsed.active !== 'string' || !record(parsed.profiles))
105
121
  throw new Error();
106
122
  for (const [name, one] of Object.entries(parsed.profiles)) {
107
- if (!/^[A-Za-z0-9_-]{1,64}$/.test(name) || !record(one) || typeof one.origin !== 'string' || sessionServiceOrigin(one.origin) !== one.origin || typeof one.account !== 'string' || !accountValid(one.account) || typeof one.token !== 'string' || !tokenValid(one.token))
123
+ if (!/^[A-Za-z0-9_-]{1,64}$/.test(name) || !record(one) || typeof one.origin !== 'string' || serviceOrigin(one.origin) !== one.origin || typeof one.account !== 'string' || !accountValid(one.account) || typeof one.token !== 'string' || !tokenValid(one.token))
108
124
  throw new Error();
109
125
  }
110
126
  if (!Object.hasOwn(parsed.profiles, parsed.active))
@@ -387,7 +403,7 @@ export async function maybeRunTeamCommand(argv, write, options = {}) {
387
403
  if (command === 'connect') {
388
404
  let origin;
389
405
  try {
390
- origin = sessionServiceOrigin(parsed.positions[0]);
406
+ origin = serviceOrigin(parsed.positions[0]);
391
407
  }
392
408
  catch {
393
409
  throw new UsageError('Use an HTTPS service origin (loopback HTTP is allowed), without credentials, paths, query or fragment.');
@@ -408,6 +424,8 @@ export async function maybeRunTeamCommand(argv, write, options = {}) {
408
424
  secret = (flags['token-file'] ? privateFile(value(flags, 'token-file'), 8192) : await (options.readStdin ?? stdinSecret)()).trim();
409
425
  if (!accountValid(account) || !tokenValid(secret))
410
426
  throw new UsageError('Provide an account name and a valid sign-in secret from your selected source.');
427
+ if (parseApiToken(secret) === null)
428
+ stderr(PASSWORD_PROFILE_WARNING);
411
429
  const profile = { origin, account, token: secret }, checked = await perform(profile, { operation: 'list', args: {} }, options);
412
430
  if (!checked.ok) {
413
431
  emit(checked);
@@ -421,6 +439,8 @@ export async function maybeRunTeamCommand(argv, write, options = {}) {
421
439
  if (!profile || !Object.hasOwn(saved.profiles, profileName))
422
440
  throw new UsageError('No saved connection. Use connect with your individual sign-in first.');
423
441
  secret = profile.token;
442
+ if (parseApiToken(secret) === null)
443
+ stderr(PASSWORD_PROFILE_WARNING);
424
444
  const call = (operation, args, signal) => perform(profile, { operation, args }, { ...options, ...(signal === undefined ? {} : { signal }) });
425
445
  if (command === 'chat' || command === 'brief')
426
446
  return await chat(command, flags, profile.account, call, emit, options, stderr);
@@ -58,7 +58,7 @@ export function createTeamRuntime(options) {
58
58
  const previous = pages.get(key);
59
59
  if (revision && previous?.revision === revision && previous.expires > now.getTime())
60
60
  return previous.page;
61
- const page = workIndexPage(store, now, { principal: 'operator', repos: selected.projects }, { leadId: selected.leadId, limit: 40 });
61
+ const page = workIndexPage(store, now, { principal: 'operator', repos: selected.projects }, { leadId: selected.leadId, limit: 40, root: options.evidenceRoot });
62
62
  if (revision && options.workspaceRevision) {
63
63
  if (pages.size >= 64)
64
64
  pages.delete(pages.keys().next().value);
@@ -17,9 +17,16 @@ export declare const PHONE_HELP: string;
17
17
  /** The same display scrub for every phone-bound string a person or a model authored. */
18
18
  export declare const phoneText: typeof publicChatText;
19
19
  export declare function projectLabel(repo: string): string;
20
+ /** The phone's reading of saved results: `root` lets a finished result read its saved checks, requirements and
21
+ * review exactly as the console does. Without it, the dispatch diagnosis speaks. */
22
+ export type PhoneRead = {
23
+ root?: string | undefined;
24
+ now?: Date;
25
+ repos?: readonly string[];
26
+ };
20
27
  /** repos is the transport's explicit enrollment ceiling, never opened-project
21
28
  * history. This snapshot is intentionally bounded and advertises that bound. */
22
- export declare function phoneStatus(store: Store, repos: readonly string[], now: Date, focused?: string | null): string;
29
+ export declare function phoneStatus(store: Store, repos: readonly string[], now: Date, focused?: string | null, root?: string): string;
23
30
  /** One task a person can pick to talk about from a chat app. */
24
31
  export type PhoneTaskChoice = {
25
32
  id: string;
@@ -31,7 +38,7 @@ export type PhoneTaskChoice = {
31
38
  * then work in progress, waiting work and recent results; one entry per
32
39
  * task (its revisions share it), optionally narrowed by words in the id or
33
40
  * title. Bounded, within the chat's own project ceiling. */
34
- export declare function phoneTaskChoices(store: Store, repos: readonly string[], now: Date, query?: string | null, limit?: number): PhoneTaskChoice[];
41
+ export declare function phoneTaskChoices(store: Store, repos: readonly string[], now: Date, query?: string | null, limit?: number, root?: string): PhoneTaskChoice[];
35
42
  /** `id` keys the chat's choice (the task's first version, where its chat
36
43
  * lives); `view` is what to show: the exact version named, else the current one. */
37
44
  export type PhoneTaskPick = {
@@ -63,10 +70,10 @@ export type PhoneTaskLink = {
63
70
  };
64
71
  /** Where the rest lives when no button can say so: the closing line of an unlinked `/task`. */
65
72
  export declare const PHONE_CONSOLE_FOOTER = "Read-only status. Saved files and full actions are in the console.";
66
- export declare function phoneTask(store: Store, repos: readonly string[], id: string, now: Date): string;
73
+ export declare function phoneTask(store: Store, repos: readonly string[], id: string, now: Date, root?: string): string;
67
74
  /** One task's status for a chat app: the words, its one console link, and
68
75
  * the saved result it shows (a reply to it is about that result). */
69
- export declare function phoneTaskView(store: Store, repos: readonly string[], id: string, now: Date): {
76
+ export declare function phoneTaskView(store: Store, repos: readonly string[], id: string, now: Date, root?: string): {
70
77
  text: string;
71
78
  link: PhoneTaskLink | null;
72
79
  run: number | null;
@@ -4,7 +4,8 @@ import { manualReviewOnly } from "./proof.js";
4
4
  * No provider calls, repository access, new workflow state, or inferred success. */
5
5
  import { diagnoseTaskDispatch, withDispatchDiagnoses } from "./dispatch.js";
6
6
  import { familyCompleted } from "./result-completion.js";
7
- import { plainReasonOf, stageOfDispatch, taskStatusOf } from "./task-status.js";
7
+ import { assignmentStatusFacts, plainReasonOf, stageOfDispatch, taskStatusOf } from "./task-status.js";
8
+ import { assignmentOf } from "./assignment.js";
8
9
  import { CHAT_CONTROLS, chatControlHref, chatResultHref } from "./chat-controls.js";
9
10
  export function phoneCommand(text) {
10
11
  // Slash forms everywhere; the bare words too, because Slack and Discord
@@ -53,10 +54,16 @@ export function projectLabel(repo) {
53
54
  return plain(repo.split(/[\\/]/).filter(Boolean).pop() ?? "project", 48);
54
55
  }
55
56
  /** The shared headline (task-status.ts) for a dispatch diagnosis on the phone. */
56
- function headlineFor(store, id, d) {
57
+ function headlineFor(store, id, d, read_ = {}) {
57
58
  // Any version of a completed family reads Complete, as taskWaitSnapshot says.
58
59
  const completed = d.code === "complete" && familyCompleted(store, id);
59
60
  const read = stageOfDispatch(d, { completed });
61
+ // A finished result reads the console's own facts: its requirements, checks Off at release, and what holds it back.
62
+ if (read.stage === "finished" && read_.root !== undefined && read_.now !== undefined && read_.repos !== undefined) {
63
+ const assignment = assignmentOf(store, id, read_.now, { principal: "operator", repos: read_.repos }, read_.root);
64
+ if (assignment !== null && assignment.state === "ready-to-check")
65
+ return taskStatusOf(assignmentStatusFacts(assignment));
66
+ }
60
67
  const finished = read.stage === "finished" || read.stage === "complete" || read.stage === "failed";
61
68
  return taskStatusOf({ stage: read.stage, ...(read.need === undefined ? {} : { need: read.need }), reason: finished ? null : plainReasonOf(read.stage, d.code, d.detail),
62
69
  checks: read.stage === "failed" && d.code === "proof-refuted" ? { status: "failed", exitCode: null, head: null } : null });
@@ -95,7 +102,7 @@ function nextStep(d) {
95
102
  }
96
103
  /** repos is the transport's explicit enrollment ceiling, never opened-project
97
104
  * history. This snapshot is intentionally bounded and advertises that bound. */
98
- export function phoneStatus(store, repos, now, focused = null) {
105
+ export function phoneStatus(store, repos, now, focused = null, root) {
99
106
  if (repos.length === 0)
100
107
  return "No connected projects are available to this bridge. Add a project in Toolroll, then send /status again.";
101
108
  return store.transact(() => {
@@ -111,7 +118,7 @@ export function phoneStatus(store, repos, now, focused = null) {
111
118
  continue;
112
119
  lines.push(`${group} · ${rows.length}${snapshot.tasksSaturated ? " in this snapshot" : ""}`);
113
120
  for (const task of rows.slice(0, 2)) {
114
- lines.push(`${plain(task.id, 64)} · ${projectLabel(repos[task.repoIndex])}`, ` ${plain(task.title, 64)} — ${headlineFor(store, task.id, task.dispatch).headline}`);
121
+ lines.push(`${plain(task.id, 64)} · ${projectLabel(repos[task.repoIndex])}`, ` ${plain(task.title, 64)} — ${headlineFor(store, task.id, task.dispatch, { root, now, repos }).headline}`);
115
122
  }
116
123
  if (rows.length > 2)
117
124
  lines.push(` +${rows.length - 2} more in the console`);
@@ -126,7 +133,7 @@ const PICK_GROUPS = ["Needs attention", "Working", "Waiting / next up", "Finishe
126
133
  * then work in progress, waiting work and recent results; one entry per
127
134
  * task (its revisions share it), optionally narrowed by words in the id or
128
135
  * title. Bounded, within the chat's own project ceiling. */
129
- export function phoneTaskChoices(store, repos, now, query = null, limit = 8) {
136
+ export function phoneTaskChoices(store, repos, now, query = null, limit = 8, root) {
130
137
  if (repos.length === 0)
131
138
  return [];
132
139
  return store.transact(() => {
@@ -142,7 +149,7 @@ export function phoneTaskChoices(store, repos, now, query = null, limit = 8) {
142
149
  if (seen.has(id) || !words.every(word => `${id} ${row.title}`.toLowerCase().includes(word)))
143
150
  continue;
144
151
  seen.add(id);
145
- choices.push({ id, title: plain(row.title, 64), label: headlineFor(store, row.id, row.dispatch).headline, group: groupOf(row.dispatch) });
152
+ choices.push({ id, title: plain(row.title, 64), label: headlineFor(store, row.id, row.dispatch, { root, now, repos }).headline, group: groupOf(row.dispatch) });
146
153
  if (choices.length >= limit)
147
154
  break;
148
155
  }
@@ -210,12 +217,12 @@ function taskLinkFor(id, d, result) {
210
217
  default: return control("task");
211
218
  }
212
219
  }
213
- export function phoneTask(store, repos, id, now) {
214
- return phoneTaskView(store, repos, id, now).text;
220
+ export function phoneTask(store, repos, id, now, root) {
221
+ return phoneTaskView(store, repos, id, now, root).text;
215
222
  }
216
223
  /** One task's status for a chat app: the words, its one console link, and
217
224
  * the saved result it shows (a reply to it is about that result). */
218
- export function phoneTaskView(store, repos, id, now) {
225
+ export function phoneTaskView(store, repos, id, now, root) {
219
226
  return store.transact(() => {
220
227
  const ref = store.lookupRef(id);
221
228
  // Check admission before reading a title, run, proof, or diagnosis.
@@ -225,7 +232,7 @@ export function phoneTaskView(store, repos, id, now) {
225
232
  const d = diagnoseTaskDispatch(store, id, now);
226
233
  if (d === null)
227
234
  return { text: "This task's status is unavailable. Open it in the console before retrying.", link: null, run: null };
228
- const shared = headlineFor(store, id, d);
235
+ const shared = headlineFor(store, id, d, { root, now, repos });
229
236
  const lines = [plain(task.title, 140), `${plain(id, 64)} · ${projectLabel(ref.repo)}`, `As of ${now.toISOString().replace("T", " ").slice(0, 19)} UTC`, "", shared.headline];
230
237
  const blocker = d.blockerTaskId === null ? null : store.lookupRef(d.blockerTaskId);
231
238
  const hiddenDependency = blocker !== null && (blocker.repo === null || !repos.includes(blocker.repo));
package/dist/telegram.js CHANGED
@@ -1884,9 +1884,9 @@ function applyPhoneRead(context, update, effects) {
1884
1884
  const repos = telegramConversationRepos(store, binding.approver, registry);
1885
1885
  const focused = focusedTitle(store, binding);
1886
1886
  if (command.kind === "status")
1887
- response = phoneStatus(store, repos, clock(), focused?.title ?? null);
1887
+ response = phoneStatus(store, repos, clock(), focused?.title ?? null, context.conversation?.evidenceRoot);
1888
1888
  else if (command.kind === "tasks") {
1889
- const choices = phoneTaskChoices(store, repos, clock());
1889
+ const choices = phoneTaskChoices(store, repos, clock(), null, 8, context.conversation?.evidenceRoot);
1890
1890
  response = choices.length === 0 ? phoneTaskListText([], null) : `${focused === null ? "" : `Talking about: ${focused.title}\n\n`}Pick a task to talk about:`;
1891
1891
  if (choices.length > 0)
1892
1892
  keyboard = pickKeyboard(store, choices, focused !== null);
@@ -1901,7 +1901,7 @@ function applyPhoneRead(context, update, effects) {
1901
1901
  response = PHONE_NO_MATCH;
1902
1902
  else {
1903
1903
  store.setChatFocus("telegram", binding.id, pick.id, clock());
1904
- const view = phoneTaskView(store, repos, pick.view, clock());
1904
+ const view = phoneTaskView(store, repos, pick.view, clock(), context.conversation?.evidenceRoot);
1905
1905
  shown = { task: pick.view, run: view.run };
1906
1906
  button = phoneLinkButton(context.conversation?.phoneOrigin?.() ?? null, view.link);
1907
1907
  // A destination with no trusted origin to carry it: the words say where instead.
@@ -2124,7 +2124,7 @@ function applyCallback(context, update, effects) {
2124
2124
  store.setChatFocus("telegram", binding.id, id, clock());
2125
2125
  ack(`Talking about: ${title}`.slice(0, 190));
2126
2126
  const current = store.taskFamilyOf(taskId, repos, false)?.current.id ?? id;
2127
- const view = phoneTaskView(store, repos, current, clock());
2127
+ const view = phoneTaskView(store, repos, current, clock(), context.conversation?.evidenceRoot);
2128
2128
  store.recordTelegramTaskMessage(binding, String(message.message_id), current, view.run, clock());
2129
2129
  editText(phoneFocusText(view.text), [[telegramButton("Back to the lead", "pick:lead")]]);
2130
2130
  return;