toolroll 0.9.4 → 0.9.5

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/dist/serve.js CHANGED
@@ -58,6 +58,7 @@ import { prepareWorkspaceRevision, WorkspaceValidatorCache } from "./workspace-r
58
58
  import { workIndexPage, workCountsByProject, WorkIndexCursorError } from "./work-index.js";
59
59
  import { openWorkDecisionOf } from "./work-summary.js";
60
60
  import { assignmentOf, checkAssignmentAsOperator } from './assignment.js';
61
+ import { demoChatHtml, demoThreadHtml, DEMO_CHAT_SCRIPT } from "./demo-chat.js";
61
62
  import { pullRequestSettingsHtml, PULL_REQUEST_SETTINGS_CSS } from './pull-request-ui.js';
62
63
  import { checkPublishing, completeAndOpenPullRequest, mergeAsPerson, pullRequestBlocker, publishingOf, saveMergeSettings, savePublishing, newestPullRequestOf, MERGE_METHODS } from './pull-request-flow.js';
63
64
  import { leadBriefHtml, LEAD_CONTEXT_CSS } from './lead-context.js';
@@ -150,7 +151,7 @@ import { TEMPLATES, templateByName } from "./templates.js";
150
151
  import { starterRecipes, savedRecipes, findRecipe, importRecipe, exportRecipe, createWorkflowPreview, workflowPreview, launchWorkflow, saveWorkflowRecipe, RecipeError, prepareRecipeRun } from "./recipes.js";
151
152
  import { recipeFromForm, recipeLibraryHtml, recipeEditorHtml, workflowPreviewHtml, recipeScript, RECIPE_CSS, recipeDefinitionPreviewHtml, recipeRunHtml, recipeAnswersFromForm } from "./recipe-ui.js";
152
153
  import { EVIDENCE_CAPS, readVerifiedArtifact, readVerifiedReport, readVerifiedProofForRun, storeEvidence, writeEvidenceFile, scanForSecrets } from "./evidence.js";
153
- import { GOAL_ASSESSMENT_PENDING, reviewConflict, manualReviewOnly, dispatchStatusToken, passFraction, semanticCoverage, coverageWords, coverageStateWords } from "./proof.js";
154
+ import { GOAL_ASSESSMENT_PENDING, reviewConflict, manualReviewOnly, manualReviewCriterionOf, personCheckWords, plainReasonWords, dispatchStatusToken, passFraction, semanticCoverage, coverageWords, coverageStateWords } from "./proof.js";
154
155
  import { WORK_VIEWS, REVIEW_TOKENS, parseWorkView, resultStatusOf, receiptHeadingOf, receiptPublicationWords, reviewFactsOf, workStatusOf, primaryDestinationOf, needsPerson, dispatchActionLabel, } from "./workspace-ui.js";
155
156
  import { PRICED_BUILD_MODELS } from "./pricing.js";
156
157
  import { RESULT_TABS, RESULT_REVIEW_SCRIPT, isRevisionFeedback, evidenceResultStatusOf, evidenceShortenedWords, parseResultTab, resultLeadOf, evidenceProblemDetailsOf, evidenceHealthOf, resultFactsAttributes, resultFactsAttributeMap, resultReturnTarget, commentSourceKey, REQUEST_TOKEN, revisionBatchOf, parseRevisionBatch, revisionSourceOf, } from "./result-review.js";
@@ -1727,6 +1728,7 @@ export function createDecisionServer(options) {
1727
1728
  url.pathname !== "/projects/browse" && url.pathname !== "/projects/github" && url.pathname !== "/workbench" &&
1728
1729
  url.pathname !== "/fleet" &&
1729
1730
  url.pathname !== "/chat" && url.pathname !== "/chat/mate/status" && url.pathname !== "/chat/task-status" && url.pathname !== "/chat/stream" &&
1731
+ !url.pathname.startsWith("/chat/demo/") &&
1730
1732
  !/^\/chat\/action\/[0-9]{1,15}$/.test(url.pathname) &&
1731
1733
  !url.pathname.startsWith("/settings") && url.pathname !== "/logout" && url.pathname !== "/people" && url.pathname !== "/ledger" && url.pathname !== "/ledger/export" && url.pathname !== "/metrics" && url.pathname !== "/spend" && url.pathname !== "/spend/budget" &&
1732
1734
  !(url.pathname === "/board" && url.searchParams.get("scope") === "all");
@@ -3237,6 +3239,26 @@ export function createDecisionServer(options) {
3237
3239
  ...(fragments === null ? {} : { fragments }),
3238
3240
  }));
3239
3241
  }
3242
+ // `toolroll demo`: Chat is the scripted lead, never a model.
3243
+ if (url.pathname === "/chat/demo/live") {
3244
+ const lead = demoLeadHere();
3245
+ if (lead === null || who.via !== "cookie")
3246
+ return respond(response, 404, "application/json", JSON.stringify({ ok: false }));
3247
+ const exchanges = lead.exchanges();
3248
+ return respond(response, 200, "application/json", JSON.stringify({
3249
+ version: lead.version(), working: exchanges.some(one => one.state === "working"),
3250
+ html: demoThreadHtml(exchanges, who.session.csrf, demoResultView),
3251
+ }));
3252
+ }
3253
+ if (url.pathname === "/chat" && demoLeadHere() !== null && who.via === "cookie" &&
3254
+ !["task", "result", "project", "conversation", "team", "proposal"].some(name => url.searchParams.has(name))) {
3255
+ const lead = demoLeadHere();
3256
+ const said = url.searchParams.get("said")?.slice(0, 300) ?? null;
3257
+ return sendScreen(response, 200, screen("Chat", demoChatHtml({ exchanges: lead.exchanges(), csrf: who.session.csrf, version: lead.version(), problem: said, resultOf: demoResultView }), {
3258
+ chrome: chromeFor(null, "chat", undefined, "all"),
3259
+ functional: { script: DEMO_CHAT_SCRIPT, fetches: true },
3260
+ }));
3261
+ }
3240
3262
  if (url.pathname === "/chat") {
3241
3263
  // Cookie sessions only (Codex v3 review, change 7): drafts live in
3242
3264
  // THIS session's memory; a bearer caller has nowhere to keep them.
@@ -4146,6 +4168,42 @@ export function createDecisionServer(options) {
4146
4168
  /** The frozen explicit repo list, digested canonically (sorted) — every
4147
4169
  * candidate binds to it and filing re-proves it (v2 new finding 5). */
4148
4170
  const chatCeilingDigest = (repos = managedRepos()) => createHash("sha256").update([...repos].sort().join("\n")).digest("hex");
4171
+ /** The scripted lead answers only inside a demo database. */
4172
+ function demoLeadHere() {
4173
+ return options.demoLead !== undefined && store.isDemo() ? options.demoLead : null;
4174
+ }
4175
+ /** A finished demo exchange's result, read back from the evidence its run actually stored. */
4176
+ function demoResultView(exchange) {
4177
+ if (exchange.runId === null || exchange.taskId === null)
4178
+ return null;
4179
+ const artifacts = store.artifactsFor(exchange.runId);
4180
+ const text = (kind) => {
4181
+ const artifact = artifacts.find(one => one.kind === kind);
4182
+ if (artifact === undefined)
4183
+ return null;
4184
+ const read = readVerifiedArtifact(evidenceRoot, artifact);
4185
+ return read.ok ? read.content.toString("utf8") : null;
4186
+ };
4187
+ const shot = artifacts.find(one => one.kind === "screenshot");
4188
+ const stat = (() => { try {
4189
+ return JSON.parse(text("diff-stat") ?? "null");
4190
+ }
4191
+ catch {
4192
+ return null;
4193
+ } })();
4194
+ const assignment = assignmentOf(store, exchange.taskId, clock(), { principal: "operator", repos: managedRepos() }, evidenceRoot);
4195
+ const log = text("check-log");
4196
+ return {
4197
+ diff: text("terminal-diff"),
4198
+ checkLog: log === null ? null : log.slice(log.indexOf("$ ") === -1 ? 0 : log.indexOf("$ ")),
4199
+ checks: { status: assignment?.receipt?.checks.status ?? "unavailable", detail: assignment?.receipt?.checks.detail ?? "Saved checks are unavailable." },
4200
+ screenshot: shot === undefined ? null : { href: `/r/${exchange.runId}/evidence/${shot.id}`, caption: exchange.plan.screenshot.caption },
4201
+ additions: stat?.additions ?? 0,
4202
+ deletions: stat?.deletions ?? 0,
4203
+ files: stat?.fileCount ?? 0,
4204
+ taskHref: taskHref(exchange.taskId),
4205
+ };
4206
+ }
4149
4207
  /** Every condition re-proved per request — the render and the POST each
4150
4208
  * ask again; nothing is cached into authority. */
4151
4209
  function chatEnablement() {
@@ -4503,7 +4561,7 @@ export function createDecisionServer(options) {
4503
4561
  s = { ...s, workspace: { ...s.workspace, view: { ...s.workspace.view, stepUp: { label: ssoFacts.label, fresh: ssoFacts.fresh, confirmHref: `/login/sso?reauth=1&return=${encodeURIComponent(back)}` } } } };
4504
4562
  }
4505
4563
  const teamEntry = requestContext.getStore()?.returnTo;
4506
- if (teamEntry?.startsWith('/chat') && !s.workspace?.team && !teamEntry.includes('task=') && !teamEntry.includes('proposal=')) {
4564
+ if (teamEntry?.startsWith('/chat') && !s.workspace?.team && !teamEntry.includes('task=') && !teamEntry.includes('proposal=') && demoLeadHere() === null) {
4507
4565
  s.body = '<p class="team-entry"><a href="/chat?team=1">Open team chat</a></p>' + s.body;
4508
4566
  if (s.workspace?.conversation)
4509
4567
  s.workspace.controlsHtml = '<p><a href="/chat?team=1">Open team chat</a></p>' + (s.workspace.controlsHtml ?? '');
@@ -4524,7 +4582,7 @@ export function createDecisionServer(options) {
4524
4582
  const extras = s.workspace ?? {};
4525
4583
  const notices = [...(extras.notices ?? [])];
4526
4584
  if (s.chrome.demo)
4527
- notices.unshift('Demo workspace — synthetic tasks. External work is disabled.');
4585
+ notices.unshift(DEMO_BANNER);
4528
4586
  if (s.chrome.modeBanner)
4529
4587
  notices.push(s.chrome.modeBanner.words);
4530
4588
  let crew = { crew: [], crewTruncated: false };
@@ -5922,6 +5980,43 @@ export function createDecisionServer(options) {
5922
5980
  const denied = authorizeMutation(request, who, body);
5923
5981
  if (denied !== null)
5924
5982
  return refuse(response, who, denied.status, denied.message);
5983
+ // The scripted demo lead's acts: ask, approve, change a plan, complete or send back a result.
5984
+ const demoAct = /^\/chat\/demo\/(?:ask|([0-9]{1,9})\/(approve|change|revise|complete))$/.exec(url.pathname);
5985
+ if (demoAct !== null) {
5986
+ const lead = demoLeadHere();
5987
+ if (lead === null || who.via !== "cookie")
5988
+ return refuse(response, who, 404, "There's no page at this address.", "/chat");
5989
+ if (who.role !== "approver")
5990
+ return redirect(response, `/chat?said=${encodeURIComponent("Your login can watch. Approving and completing is an approver's act.")}`);
5991
+ const now = clock();
5992
+ if (demoAct[1] === undefined) {
5993
+ const message = (body.get("message") ?? "").trim();
5994
+ if (message === "")
5995
+ return redirect(response, `/chat?said=${encodeURIComponent("Type a request first.")}`);
5996
+ const asked = lead.ask(message, now);
5997
+ return redirect(response, `/chat#demo-${asked.id}`);
5998
+ }
5999
+ const id = Number(demoAct[1]);
6000
+ const note = body.get("note") ?? "";
6001
+ const done = demoAct[2] === "approve" ? lead.approve(id, now)
6002
+ : demoAct[2] === "change" ? lead.change(id, note, now)
6003
+ : demoAct[2] === "revise" ? lead.requestChanges(id, note, now)
6004
+ : lead.complete(id, now, (taskId, runId) => {
6005
+ const principal = matePrincipal(who);
6006
+ if (principal === null)
6007
+ return { ok: false, message: "Your access changed. Sign in again." };
6008
+ const current = assignmentOf(store, taskId, now, { principal: "operator", repos: principal.repos }, evidenceRoot);
6009
+ if (current?.receipt?.runId !== runId)
6010
+ return { ok: false, message: "This result changed. Reload before marking it complete." };
6011
+ const completed = checkAssignmentAsOperator(store, taskId, current.receipt.digest, principal, now, evidenceRoot);
6012
+ return completed.ok ? { ok: true } : { ok: false, message: completed.message };
6013
+ });
6014
+ bustBadge();
6015
+ if (!done.ok)
6016
+ return redirect(response, `/chat?said=${encodeURIComponent(done.message)}`);
6017
+ const latest = lead.exchanges().at(-1);
6018
+ return redirect(response, demoAct[2] === "change" || demoAct[2] === "revise" ? `/chat#demo-${latest?.id ?? id}` : `/chat#demo-${id}-work`);
6019
+ }
5925
6020
  // A person says a provider's sign-in works again: its sign-in pause
5926
6021
  // lifts, its tasks may start, and one short message says so.
5927
6022
  const resumeProvider = /^\/providers\/([a-z]+)\/resume$/.exec(url.pathname);
@@ -10220,7 +10315,7 @@ export function createDecisionServer(options) {
10220
10315
  note = validated.note;
10221
10316
  }
10222
10317
  store.acceptProof(latest.id, verifiedAuthor(who.name), note, now);
10223
- return redirect(response, taskHref(taskId));
10318
+ return redirect(response, body.get("return") === null ? taskHref(taskId) : safeReturn(body.get("return")));
10224
10319
  }
10225
10320
  case "complete": {
10226
10321
  if (who.via !== "cookie" || who.role !== "approver")
@@ -11249,7 +11344,7 @@ function criterionMatrixHtml(matrix, options = {}) {
11249
11344
  const state = row.state;
11250
11345
  const awaitingAssessment = row.assessment?.evidenceState === "pass" && row.review === null;
11251
11346
  const confirmed = row.assessment !== undefined && state === "pass";
11252
- const label = awaitingAssessment ? "Not assessed" : confirmed ? "Confirmed" : state === "pass" ? "Evidence checks passed" : state === "manual-review" ? "Human review" : state === "missing" ? "Evidence missing" : "Evidence failed";
11347
+ const label = awaitingAssessment ? "Not assessed" : confirmed ? "Confirmed" : state === "pass" ? "Met" : state === "manual-review" ? "You check" : state === "missing" ? "Not shown yet" : "Not met";
11253
11348
  const cls = awaitingAssessment ? "" : state === "pass" ? "badge-done" : state === "manual-review" ? "badge-manual-review" : "badge-failed";
11254
11349
  const warnings = [];
11255
11350
  // Only replace the known boilerplate. Other recorded failure details
@@ -11648,6 +11743,8 @@ function agentsStripHtml(view, taskId) {
11648
11743
  return `<p class="task-chat-agents">${escape(agentsSummaryWords(view))} <a href="${taskHref(taskId)}#agents">Review agent setup</a></p>`;
11649
11744
  return `<details class="task-chat-agents"><summary>Agent setup</summary><p>${escape(agentsSummaryWords(view))}</p><a href="${taskHref(taskId)}#agents">View agents</a></details>`;
11650
11745
  }
11746
+ /** The demo's one-line promise, on every page. */
11747
+ const DEMO_BANNER = "Demo: a scripted lead and sample projects. Nothing calls a model, reaches outside or spends.";
11651
11748
  /** Every character that could open a tag or an attribute, dead at the sink. */
11652
11749
  function escape(text) {
11653
11750
  return text
@@ -12599,6 +12696,66 @@ ${THEME_DARK}
12599
12696
  padding: .375rem .9rem; font-size: .8125rem; color: var(--muted-foreground);
12600
12697
  }
12601
12698
  .banner .badge { margin-right: .5rem; }
12699
+ /* Chat in \`toolroll demo\`: the scripted lead's conversation. */
12700
+ .demo-chat { display: flex; flex-direction: column; gap: 1rem; max-width: 46rem; margin: 0 auto; padding-bottom: 1rem; }
12701
+ .demo-thread { display: flex; flex-direction: column; gap: 1.25rem; }
12702
+ .demo-turn { display: flex; flex-direction: column; gap: .75rem; scroll-margin-top: 1rem; }
12703
+ .demo-said p { margin: 0; }
12704
+ .demo-you { align-self: flex-end; max-width: min(34rem, 85%); background: var(--so-ink); color: var(--so-paper); border-radius: .875rem .875rem .25rem .875rem; padding: .55rem .85rem; overflow-wrap: anywhere; }
12705
+ .demo-lead { max-width: 40rem; }
12706
+ .demo-who { font-size: .75rem; font-weight: 600; color: var(--so-muted); margin-bottom: .15rem !important; }
12707
+ .demo-chat .card { margin: 0; padding: 1rem 1.125rem; }
12708
+ .demo-chat .card h2 { font-size: .9375rem; margin: .1rem 0 .4rem; }
12709
+ .demo-chat .card h3 { font-size: .75rem; font-weight: 600; color: var(--so-muted); margin: .75rem 0 .25rem; }
12710
+ .demo-chat .card ul { margin: 0; padding-left: 1.1rem; }
12711
+ .demo-chat .card li { margin: .15rem 0; }
12712
+ .demo-chat .card > p { margin: .25rem 0; }
12713
+ .demo-kicker { font-size: .75rem; color: var(--so-muted); margin: 0 !important; }
12714
+ .demo-actions { display: flex; flex-wrap: wrap; align-items: flex-start; gap: .5rem; margin-top: .9rem; }
12715
+ .demo-actions form { margin: 0; }
12716
+ .demo-actions button, .demo-more > summary { min-height: 2.5rem; }
12717
+ .demo-chat button.demo-primary { background: var(--primary); color: var(--primary-foreground); border-color: var(--primary); font-weight: 600; }
12718
+ .demo-chat details.demo-more { flex: 1 1 12rem; margin: 0; padding: 0; border: 0; border-radius: 0; background: none; }
12719
+ .demo-chat details.demo-more[open] { padding-bottom: 0; }
12720
+ .demo-chat .demo-more > summary { display: inline-flex; align-items: center; color: var(--so-ink); padding: .35rem .85rem; border: 1px solid var(--so-input-line); border-radius: calc(var(--radius) - 3px); background: var(--so-paper); font-weight: 500; font-size: .8125rem; cursor: pointer; list-style: none; }
12721
+ .demo-more > summary::-webkit-details-marker { display: none; }
12722
+ .demo-more[open] > summary { margin-bottom: .5rem; }
12723
+ .demo-more form, .demo-composer { display: flex; flex-direction: column; gap: .4rem; }
12724
+ .demo-more label { font-size: .8125rem; font-weight: 500; }
12725
+ .demo-more textarea, .demo-composer textarea { width: 100%; font: inherit; font-size: 1rem; resize: vertical; }
12726
+ .demo-more form button { align-self: flex-start; }
12727
+ .demo-steps { display: flex; flex-wrap: wrap; gap: .4rem 1rem; list-style: none; padding: 0 !important; margin: .4rem 0 !important; }
12728
+ .demo-steps li { display: inline-flex; align-items: center; gap: .4rem; color: var(--so-muted); font-size: .8125rem; margin: 0 !important; }
12729
+ .demo-steps li::before { content: ""; width: .55rem; height: .55rem; border-radius: 50%; border: 1.5px solid currentColor; }
12730
+ .demo-steps li.done { color: var(--so-ink); }
12731
+ .demo-steps li.done::before { background: var(--so-success); border-color: var(--so-success); }
12732
+ .demo-steps li.now { color: var(--so-ink); font-weight: 600; }
12733
+ .demo-steps li.now::before { border-color: var(--so-info); background: var(--so-info); animation: demo-pulse 1.2s ease-in-out infinite; }
12734
+ @keyframes demo-pulse { 50% { opacity: .35; } }
12735
+ @media (prefers-reduced-motion: reduce) { .demo-steps li.now::before { animation: none; } }
12736
+ .demo-state { display: flex; align-items: center; gap: .5rem; margin: 0 !important; }
12737
+ .demo-chat .badge.demo-ready { background: var(--so-info-soft); color: var(--so-info); }
12738
+ .demo-chat .badge.demo-complete { background: var(--so-success-soft); color: var(--so-success); }
12739
+ .demo-pass { color: var(--so-success); font-weight: 500; }
12740
+ .demo-fail { color: var(--so-danger); font-weight: 500; }
12741
+ .demo-chat details.demo-evidence { margin: .5rem 0 0; padding: 0; border: 0; border-top: 1px solid var(--so-line); border-radius: 0; background: none; box-shadow: none; }
12742
+ .demo-evidence > summary { min-height: 2.5rem; display: list-item; padding-block: .55rem; cursor: pointer; font-weight: 500; font-size: .8125rem; }
12743
+ .demo-evidence pre { margin: 0 0 .5rem; max-height: 22rem; overflow: auto; padding: .6rem .75rem; border-radius: .5rem; background: var(--so-raised); font: 400 .75rem/1.55 var(--font-mono); white-space: pre; overflow-wrap: normal; }
12744
+ .demo-diff span { display: block; min-width: max-content; }
12745
+ .demo-diff-add { background: var(--so-success-soft); color: var(--so-success); }
12746
+ .demo-diff-del { background: var(--so-danger-soft); color: var(--so-danger); }
12747
+ .demo-diff-hunk, .demo-diff-meta { color: var(--so-muted); }
12748
+ .demo-diff-file { font-weight: 600; }
12749
+ .demo-evidence figure { margin: 0 0 .5rem; }
12750
+ .demo-evidence img { display: block; width: 100%; height: auto; border: 1px solid var(--so-line); border-radius: .5rem; }
12751
+ .demo-evidence figcaption { margin-top: .35rem; }
12752
+ .demo-hint { padding: 1.25rem 0 .25rem; }
12753
+ .demo-hint-title { font-size: .9375rem; font-weight: 600; margin: 0 0 .75rem; }
12754
+ .demo-suggestions { display: flex; flex-wrap: wrap; gap: .5rem; margin: 0; }
12755
+ .demo-suggestions button { min-height: 2.5rem; border-radius: 999px; padding: .35rem .95rem; }
12756
+ .demo-composer { position: sticky; bottom: 0; padding: .75rem 0 .25rem; background: var(--so-paper); flex-direction: row; align-items: flex-end; }
12757
+ .demo-chat .demo-composer textarea { flex: 1 1 auto; width: auto; min-width: 0; min-height: 2.75rem; }
12758
+ .demo-chat .demo-composer button { flex: none; width: auto; min-height: 2.75rem; padding-inline: 1.1rem; }
12602
12759
  .banner a { color: var(--muted-foreground); }
12603
12760
  .sign-in-banner form { display: inline; margin: 0 0 0 .5rem; padding: 0; border: 0; background: transparent; }
12604
12761
 
@@ -13024,6 +13181,9 @@ ${THEME_DARK}
13024
13181
  .result-notes .recap { margin: .25rem 0 .4rem; font-size: .875rem; }
13025
13182
  .result-attention { margin: .75rem 0 .25rem; padding: .7rem .85rem; border-left: 1px solid var(--warning); border-radius: 0 calc(var(--radius) - 3px) calc(var(--radius) - 3px) 0; background: var(--warning-soft); font-size: .8125rem; }
13026
13183
  .result-attention strong { display: block; font-size: .78rem; }
13184
+ .result-you-check { display: flex; flex-wrap: wrap; align-items: center; gap: .6rem .75rem; margin: .75rem 0; padding: .65rem .85rem; border-radius: calc(var(--radius) - 3px); background: var(--muted); font-size: .8125rem; }
13185
+ .result-you-check ul { flex: 1 1 14rem; min-width: 0; margin: 0; padding: 0; list-style: none; }
13186
+ .result-you-check form { margin: 0; }
13027
13187
  .result-attention ul { margin: .3rem 0 0; padding-left: 1.15rem; }
13028
13188
  .result-attention li { margin: .15rem 0; overflow-wrap: anywhere; }
13029
13189
  .result-tabs { display: flex; gap: .25rem; margin: .9rem 0 .75rem; border-bottom: 1px solid var(--border); overflow-x: auto; scrollbar-width: none; }
@@ -14754,7 +14914,7 @@ function shell(title, body, options = {}) {
14754
14914
  // must not pass as production (adoption review, finding 8; the FENCE is
14755
14915
  // the refuseDemo gate in operate.ts, this is the honest label).
14756
14916
  const demoBanner = (chrome.demo === true
14757
- ? `<div class="banner"><span class="badge">sandbox</span>demo data \u2014 nothing here spends money or reaches a remote</div>`
14917
+ ? `<div class="banner"><span class="badge">demo</span>${escape(DEMO_BANNER.replace(/^Demo: /, ""))}</div>`
14758
14918
  : "") +
14759
14919
  (chrome.modeBanner === undefined
14760
14920
  ? ""
@@ -18449,7 +18609,7 @@ function milestoneProgressHtml(milestones) {
18449
18609
  .map(one => `<li class="milestone milestone-${one.state}"><span class="milestone-badge">${milestoneStateWord(one.state)}</span> ` +
18450
18610
  `${escape(one.description)}${one.note === null ? "" : ` <span class="meta">— ${escape(one.note)}</span>`}</li>`)
18451
18611
  .join("\n") +
18452
- `</ul><p class="meta milestone-progress-note">Live checkpoints from the agent. The task is only done after its proof passes.</p></section>`);
18612
+ `</ul><p class="meta milestone-progress-note">Live checkpoints from the agent.</p></section>`);
18453
18613
  }
18454
18614
  /** Adaptive execution plans (v44): the current plan revision's own reason
18455
18615
  * and evidence, any revision still awaiting an operator's accept/reject,
@@ -18726,7 +18886,7 @@ function taskBodyParts(data) {
18726
18886
  return `<details class="dispatch-proof-details"><summary>Previous assessment</summary><div class="dispatch-proof-body">` +
18727
18887
  `<p class="meta">Saved assessment history. Current task status and checks are shown above.</p>` +
18728
18888
  (accepted === null ? "" : `<p class="meta">Accepted with an exception by ${escape(accepted.approver)}. Check results are unchanged.${accepted.note === null ? "" : ` ${escape(accepted.note)}`}</p>`) +
18729
- (proof.proofReasons.length === 0 ? "" : `<ul>${proof.proofReasons.map(reason => `<li>${escape(reason)}</li>`).join("")}</ul>`) +
18889
+ (proof.proofReasons.length === 0 ? "" : `<ul>${[...new Set(proof.proofReasons.map(plainReasonWords))].map(reason => `<li>${escape(reason)}</li>`).join("")}</ul>`) +
18730
18890
  criterionMatrixHtml(proof.proofMatrix, { compact: true, runId: proof.runId, links: proof.proofMatrixLinks }) +
18731
18891
  semanticCoverageHtml(proof.proofMatrix, proof.qualityMode ?? "default") + `</div></details>`;
18732
18892
  }
@@ -19981,11 +20141,14 @@ function verificationExplanation(verdict, reasons) {
19981
20141
  if (reason === "the sealed diff is unavailable or truncated; the claimed changed paths cannot be verified against it") {
19982
20142
  return "The recorded changes were incomplete, so they could not be verified.";
19983
20143
  }
20144
+ const plainWords = plainReasonWords(reason);
20145
+ if (plainWords !== reason)
20146
+ return plainWords;
19984
20147
  const sentence = reason.trim();
19985
20148
  return sentence === "" ? "" : `${sentence[0]?.toUpperCase() ?? ""}${sentence.slice(1)}${/[.!?]$/.test(sentence) ? "" : "."}`;
19986
20149
  }).filter(Boolean);
19987
20150
  if (plain.length > 0)
19988
- return plain.join(" ");
20151
+ return [...new Set(plain)].join(" ");
19989
20152
  if (verdict === "verified")
19990
20153
  return "Toolroll independently verified this result.";
19991
20154
  if (verdict === "attested")
@@ -20940,8 +21103,8 @@ function completionReceiptCard(view, taskId, place, standing = receiptStatusOf(v
20940
21103
  (view.diff.binaryCount > 0 ? ` · ${view.diff.binaryCount} binary` : "") +
20941
21104
  (view.diff.filesTruncated ? " · list shortened" : "");
20942
21105
  const criteria = facts.checks === null
20943
- ? "No signed rubric"
20944
- : `${facts.checks.passed}/${facts.checks.total} acceptance criteria passed`;
21106
+ ? "No requirements set"
21107
+ : `${facts.checks.passed}/${facts.checks.total} requirements met`;
20945
21108
  // The deliverable leads (package 3): a scout's report by its title, UI
20946
21109
  // work by its validated screenshots. Unverifiable shots are counted as
20947
21110
  // unavailable, never shown, never called validated.
@@ -20962,7 +21125,7 @@ function completionReceiptCard(view, taskId, place, standing = receiptStatusOf(v
20962
21125
  const attention = current === null ? [...facts.evidenceProblems, ...view.caveats] : [];
20963
21126
  const caveats = attention.length === 0
20964
21127
  ? ""
20965
- : `<div class="receipt-caveats" data-result-attention="${attention.length}"><strong>Before you move on</strong><ul>${attention.map(one => `<li>${escape(one)}</li>`).join("")}</ul></div>`;
21128
+ : `<div class="receipt-caveats" data-result-attention="${attention.length}"><strong>Before you move on</strong><ul>${attention.map(one => `<li>${escape(plainReasonWords(one))}</li>`).join("")}</ul></div>`;
20966
21129
  // v51: semantic coverage, distinct from the machine proof word above —
20967
21130
  // the same lines the CLI prints, so chat and terminal cannot disagree.
20968
21131
  // Nothing owed (optional and unsettled, or satisfied) folds behind a
@@ -20994,7 +21157,7 @@ function completionReceiptCard(view, taskId, place, standing = receiptStatusOf(v
20994
21157
  (inReview ? `<details class="receipt-history"><summary>Review history</summary><p class="receipt-review meta" data-receipt-review="${escape(status.token)}">${escape(status.detail)}</p></details>` : "") +
20995
21158
  // The matrix count is the proof's own citation; it reads as verified
20996
21159
  // only when the machine verified the result (workspace package 1).
20997
- `<div class="receipt-facts"><span><strong>${escape(criteria)}</strong><small>${stored.tone === "problem" ? "cited by the agent — not verified" : stored.token === "agent-attested" ? "cited by the agent, no independent check" : "against the approved scope"}</small></span>` +
21160
+ `<div class="receipt-facts"><span><strong>${escape(criteria)}</strong><small>${stored.tone === "problem" ? "the agent's own claim — not checked" : stored.token === "agent-attested" ? "the agent's own claim, no project check" : "against the approved scope"}</small></span>` +
20998
21161
  `<span><strong>${escape(diff)}</strong><small>${facts.head === null ? "no commit recorded" : `head ${escape(facts.head.slice(0, 12))} · ${escape(facts.headSource ?? "")}`}</small></span>` +
20999
21162
  `<span><strong>${view.screenshots.length} screenshot${view.screenshots.length === 1 ? "" : "s"}</strong><small>${view.screenshots.length === 0 ? "none required or captured" : unavailable === 0 ? "validated visual proof" : `${unavailable} unavailable — not validated`}</small></span></div>` +
21000
21163
  `<div class="receipt-actions">` +
@@ -21080,9 +21243,25 @@ function resultPanelParts(detail, o) {
21080
21243
  }
21081
21244
  if (detail.ciFailing && publication !== null)
21082
21245
  attention.push(`CI is failing on PR #${publication.prNumber} at the last check.`);
21246
+ // Requirements only a person can confirm: plain words and one Accept,
21247
+ // in neutral ink — nothing failed. Accepting records the person's
21248
+ // decision and leaves the recorded checks as they are.
21249
+ const personChecks = proof === null || proof.accepted !== null ? [] : [
21250
+ ...proof.matrix.filter(row => row.state === "manual-review").map(row => personCheckWords(row.statement)),
21251
+ ...(proof.matrix.length > 0 ? [] : proof.reasons.filter(reason => manualReviewCriterionOf(reason) !== null).map(() => personCheckWords(null))),
21252
+ ];
21253
+ const acceptable = personChecks.length > 0 && humanReview && o.csrf !== "" && (current != null || detail.assignment == null);
21254
+ const youCheck = personChecks.length === 0 ? null : {
21255
+ lines: [...new Set(personChecks)],
21256
+ accept: acceptable ? { action: `${taskHref(detail.taskId)}/accept-proof`, run: run.id, returnTo: o.returnTo } : null,
21257
+ };
21258
+ const youCheckHtml = youCheck === null ? "" :
21259
+ `<div class="result-you-check" data-result-you-check="${youCheck.lines.length}"><ul>${youCheck.lines.map(one => `<li>${escape(one)}</li>`).join("")}</ul>` +
21260
+ (youCheck.accept === null ? "" : `<form method="post" action="${escape(youCheck.accept.action)}"><input type="hidden" name="csrf" value="${escape(o.csrf)}"><input type="hidden" name="run" value="${youCheck.accept.run}"><input type="hidden" name="return" value="${escape(youCheck.accept.returnTo)}"><button type="submit" data-accept-result>Accept</button></form>`) +
21261
+ `</div>`;
21083
21262
  const attentionHtml = attention.length === 0
21084
21263
  ? ""
21085
- : `<div class="result-attention" data-result-attention="${attention.length}"><ul>${attention.map(one => `<li>${escape(one)}</li>`).join("")}</ul></div>`;
21264
+ : `<div class="result-attention" data-result-attention="${attention.length}"><ul>${attention.map(one => `<li>${escape(plainReasonWords(one))}</li>`).join("")}</ul></div>`;
21086
21265
  // ---- one outcome, one action (repair 2026-09-14) ------------------------
21087
21266
  // The outcome is the handoff's first sentence, bounded; the agent's full
21088
21267
  // account waits behind a disclosure in Summary so nothing is said twice.
@@ -21343,9 +21522,10 @@ function resultPanelParts(detail, o) {
21343
21522
  (current == null || o.place === "review" && (current.state === "ready-to-check" || current.state === "complete") ? "" :
21344
21523
  `<div class="verdict" data-current-outcome>${verdictChipsHtml(current, facts)}` +
21345
21524
  (current.receipt?.checks.status === "failed" || current.receipt?.checks.status === "unavailable" ? `<p class="problem">${escape(current.detail)}</p>` : "") + `</div>`) +
21525
+ attentionHtml +
21526
+ youCheckHtml +
21346
21527
  action +
21347
21528
  (REVIEW_TOKENS.has(status.token) ? `<details class="receipt-history"><summary>Review history</summary><p class="receipt-review meta" data-receipt-review="${escape(status.token)}">${escape(status.detail)}</p></details>` : "") +
21348
- attentionHtml +
21349
21529
  tabs +
21350
21530
  view("summary", summaryParts) +
21351
21531
  view("changes", changeParts) +
@@ -21358,7 +21538,8 @@ function resultPanelParts(detail, o) {
21358
21538
  heading, outcome,
21359
21539
  verdict: current == null ? null : verdictChipsOf(current, facts),
21360
21540
  reviewHistory: REVIEW_TOKENS.has(status.token) ? status.detail : null,
21361
- attention: attention.filter(one => !shortenedMaterialReason(one)),
21541
+ attention: attention.filter(one => !shortenedMaterialReason(one)).map(plainReasonWords),
21542
+ youCheck,
21362
21543
  limits: attention.filter(one => shortenedMaterialReason(one)),
21363
21544
  tabs: RESULT_TABS.map(tab => ({ key: tab.key, label: tab.key === "checks" && proof?.matrix.some(row => row.assessment !== undefined) ? "Requirements" : tab.label, count: tabCounts[tab.key], href: o.hrefFor(tab.key), active: tab.key === o.tab })),
21364
21545
  views: [{ key: "summary", html: summaryParts.join("\n") }, { key: "changes", html: changeParts.join("\n") }, { key: "checks", html: checkParts.join("\n") }],
package/dist/store.d.ts CHANGED
@@ -6063,6 +6063,7 @@ export declare class Store {
6063
6063
  pendingPublications(): Publication[];
6064
6064
  publicationForRun(run: number): Publication | null;
6065
6065
  markPublicationPushed(id: number, now: Date): void;
6066
+ /** A PR that exists is recorded as opened, even if a concurrent pass gave up on the row meanwhile: it truly opened. */
6066
6067
  markPublicationOpened(id: number, prNumber: number, prUrl: string, now: Date): void;
6067
6068
  /** One more failed attempt, error kept. Returns the new count; the caller decides when enough is enough. */
6068
6069
  recordPublicationError(id: number, error: string, now: Date): number;
package/dist/store.js CHANGED
@@ -18509,9 +18509,10 @@ export class Store {
18509
18509
  .prepare("UPDATE publication SET state = 'pushed', updated_at = ? WHERE id = ? AND state = 'intended'")
18510
18510
  .run(now.toISOString(), id);
18511
18511
  }
18512
+ /** A PR that exists is recorded as opened, even if a concurrent pass gave up on the row meanwhile: it truly opened. */
18512
18513
  markPublicationOpened(id, prNumber, prUrl, now) {
18513
18514
  this.db
18514
- .prepare("UPDATE publication SET state = 'opened', pr_number = ?, pr_url = ?, updated_at = ? WHERE id = ? AND state = 'pushed'")
18515
+ .prepare("UPDATE publication SET state = 'opened', pr_number = ?, pr_url = ?, updated_at = ? WHERE id = ? AND state IN ('pushed','failed')")
18515
18516
  .run(prNumber, prUrl, now.toISOString(), id);
18516
18517
  }
18517
18518
  /** One more failed attempt, error kept. Returns the new count; the caller decides when enough is enough. */
@@ -187,7 +187,7 @@ function storedResultStatusOf(result, publication) {
187
187
  label: "Result saved, but its record does not match",
188
188
  // A structural refutation is settled before the approved check is
189
189
  // weighed, so it says nothing about whether that check passed.
190
- detail: withPublication("The proof's claims disagree with the sealed record. Whether the approved check passed is not settled by this verdict."),
190
+ detail: withPublication("What the agent reported doesn't match the changes it saved. This doesn't say whether the project check passed."),
191
191
  tone: "problem",
192
192
  action: { label: "Open the record", kind: "open-review" },
193
193
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolroll",
3
- "version": "0.9.4",
3
+ "version": "0.9.5",
4
4
  "description": "A control plane for unattended coding agents — queue tasks, walk away, come back to pull requests. Agents build in leased worktrees, never touch your default branch, and interrupt you only for decisions that need a human.",
5
5
  "directories": {
6
6
  "doc": "docs"