toolroll 0.9.3 → 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,8 +58,9 @@ 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
- import { checkPublishing, completeAndOpenPullRequest, mergeAsPerson, pullRequestBlocker, pullRequestViewOf, publishingOf, saveMergeSettings, savePublishing, MERGE_METHODS } from './pull-request-flow.js';
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';
64
65
  import { assignmentCatchUp } from './assignment-brief.js';
65
66
  import { assignmentCardOf, assignmentStatusOf, assignmentSummaryHtml, assignmentWithEvidence, ASSIGNMENT_CSS } from './assignment-ui.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 };
@@ -5433,20 +5491,12 @@ export function createDecisionServer(options) {
5433
5491
  function taskPullRequestOf(taskId, who) {
5434
5492
  const family = familyOf(taskId);
5435
5493
  const rootId = family?.root.id ?? taskId;
5436
- const ids = family === null ? [taskId] : [...family.versions.map(one => one.id)].reverse();
5437
5494
  const repo = store.lookupRef(taskId)?.repo ?? null;
5438
5495
  const publishing = publishingOf(store, repo);
5439
5496
  const target = publishing.on ? publishing.base : null;
5440
- for (const id of ids) {
5441
- const ref = store.lookupRef(id);
5442
- if (ref === null)
5443
- continue;
5444
- for (const run of store.runsFor(ref.id)) {
5445
- const view = pullRequestViewOf(store, run.id);
5446
- if (view !== null)
5447
- return { taskId: rootId, view: { ...view, canMerge: view.canMerge && who.role === "approver" && who.via === "cookie" }, offer: null, target };
5448
- }
5449
- }
5497
+ const view = newestPullRequestOf(store, family === null ? [taskId] : family.versions.map(one => one.id));
5498
+ if (view !== null)
5499
+ return { taskId: rootId, view: { ...view, canMerge: view.canMerge && who.role === "approver" && who.via === "cookie" }, offer: null, target };
5450
5500
  if (!publishing.on || who.via !== "cookie" || who.role !== "approver")
5451
5501
  return null;
5452
5502
  const assignment = assignmentOf(store, taskId, clock(), { principal: "operator", repos: admissionList(), includeUnplaced: visible(null) }, evidenceRoot);
@@ -5930,6 +5980,43 @@ export function createDecisionServer(options) {
5930
5980
  const denied = authorizeMutation(request, who, body);
5931
5981
  if (denied !== null)
5932
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
+ }
5933
6020
  // A person says a provider's sign-in works again: its sign-in pause
5934
6021
  // lifts, its tasks may start, and one short message says so.
5935
6022
  const resumeProvider = /^\/providers\/([a-z]+)\/resume$/.exec(url.pathname);
@@ -10228,7 +10315,7 @@ export function createDecisionServer(options) {
10228
10315
  note = validated.note;
10229
10316
  }
10230
10317
  store.acceptProof(latest.id, verifiedAuthor(who.name), note, now);
10231
- return redirect(response, taskHref(taskId));
10318
+ return redirect(response, body.get("return") === null ? taskHref(taskId) : safeReturn(body.get("return")));
10232
10319
  }
10233
10320
  case "complete": {
10234
10321
  if (who.via !== "cookie" || who.role !== "approver")
@@ -11257,7 +11344,7 @@ function criterionMatrixHtml(matrix, options = {}) {
11257
11344
  const state = row.state;
11258
11345
  const awaitingAssessment = row.assessment?.evidenceState === "pass" && row.review === null;
11259
11346
  const confirmed = row.assessment !== undefined && state === "pass";
11260
- 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";
11261
11348
  const cls = awaitingAssessment ? "" : state === "pass" ? "badge-done" : state === "manual-review" ? "badge-manual-review" : "badge-failed";
11262
11349
  const warnings = [];
11263
11350
  // Only replace the known boilerplate. Other recorded failure details
@@ -11656,6 +11743,8 @@ function agentsStripHtml(view, taskId) {
11656
11743
  return `<p class="task-chat-agents">${escape(agentsSummaryWords(view))} <a href="${taskHref(taskId)}#agents">Review agent setup</a></p>`;
11657
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>`;
11658
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.";
11659
11748
  /** Every character that could open a tag or an attribute, dead at the sink. */
11660
11749
  function escape(text) {
11661
11750
  return text
@@ -12607,6 +12696,66 @@ ${THEME_DARK}
12607
12696
  padding: .375rem .9rem; font-size: .8125rem; color: var(--muted-foreground);
12608
12697
  }
12609
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; }
12610
12759
  .banner a { color: var(--muted-foreground); }
12611
12760
  .sign-in-banner form { display: inline; margin: 0 0 0 .5rem; padding: 0; border: 0; background: transparent; }
12612
12761
 
@@ -13032,6 +13181,9 @@ ${THEME_DARK}
13032
13181
  .result-notes .recap { margin: .25rem 0 .4rem; font-size: .875rem; }
13033
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; }
13034
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; }
13035
13187
  .result-attention ul { margin: .3rem 0 0; padding-left: 1.15rem; }
13036
13188
  .result-attention li { margin: .15rem 0; overflow-wrap: anywhere; }
13037
13189
  .result-tabs { display: flex; gap: .25rem; margin: .9rem 0 .75rem; border-bottom: 1px solid var(--border); overflow-x: auto; scrollbar-width: none; }
@@ -14762,7 +14914,7 @@ function shell(title, body, options = {}) {
14762
14914
  // must not pass as production (adoption review, finding 8; the FENCE is
14763
14915
  // the refuseDemo gate in operate.ts, this is the honest label).
14764
14916
  const demoBanner = (chrome.demo === true
14765
- ? `<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>`
14766
14918
  : "") +
14767
14919
  (chrome.modeBanner === undefined
14768
14920
  ? ""
@@ -18457,7 +18609,7 @@ function milestoneProgressHtml(milestones) {
18457
18609
  .map(one => `<li class="milestone milestone-${one.state}"><span class="milestone-badge">${milestoneStateWord(one.state)}</span> ` +
18458
18610
  `${escape(one.description)}${one.note === null ? "" : ` <span class="meta">— ${escape(one.note)}</span>`}</li>`)
18459
18611
  .join("\n") +
18460
- `</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>`);
18461
18613
  }
18462
18614
  /** Adaptive execution plans (v44): the current plan revision's own reason
18463
18615
  * and evidence, any revision still awaiting an operator's accept/reject,
@@ -18734,7 +18886,7 @@ function taskBodyParts(data) {
18734
18886
  return `<details class="dispatch-proof-details"><summary>Previous assessment</summary><div class="dispatch-proof-body">` +
18735
18887
  `<p class="meta">Saved assessment history. Current task status and checks are shown above.</p>` +
18736
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>`) +
18737
- (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>`) +
18738
18890
  criterionMatrixHtml(proof.proofMatrix, { compact: true, runId: proof.runId, links: proof.proofMatrixLinks }) +
18739
18891
  semanticCoverageHtml(proof.proofMatrix, proof.qualityMode ?? "default") + `</div></details>`;
18740
18892
  }
@@ -19989,11 +20141,14 @@ function verificationExplanation(verdict, reasons) {
19989
20141
  if (reason === "the sealed diff is unavailable or truncated; the claimed changed paths cannot be verified against it") {
19990
20142
  return "The recorded changes were incomplete, so they could not be verified.";
19991
20143
  }
20144
+ const plainWords = plainReasonWords(reason);
20145
+ if (plainWords !== reason)
20146
+ return plainWords;
19992
20147
  const sentence = reason.trim();
19993
20148
  return sentence === "" ? "" : `${sentence[0]?.toUpperCase() ?? ""}${sentence.slice(1)}${/[.!?]$/.test(sentence) ? "" : "."}`;
19994
20149
  }).filter(Boolean);
19995
20150
  if (plain.length > 0)
19996
- return plain.join(" ");
20151
+ return [...new Set(plain)].join(" ");
19997
20152
  if (verdict === "verified")
19998
20153
  return "Toolroll independently verified this result.";
19999
20154
  if (verdict === "attested")
@@ -20948,8 +21103,8 @@ function completionReceiptCard(view, taskId, place, standing = receiptStatusOf(v
20948
21103
  (view.diff.binaryCount > 0 ? ` · ${view.diff.binaryCount} binary` : "") +
20949
21104
  (view.diff.filesTruncated ? " · list shortened" : "");
20950
21105
  const criteria = facts.checks === null
20951
- ? "No signed rubric"
20952
- : `${facts.checks.passed}/${facts.checks.total} acceptance criteria passed`;
21106
+ ? "No requirements set"
21107
+ : `${facts.checks.passed}/${facts.checks.total} requirements met`;
20953
21108
  // The deliverable leads (package 3): a scout's report by its title, UI
20954
21109
  // work by its validated screenshots. Unverifiable shots are counted as
20955
21110
  // unavailable, never shown, never called validated.
@@ -20970,7 +21125,7 @@ function completionReceiptCard(view, taskId, place, standing = receiptStatusOf(v
20970
21125
  const attention = current === null ? [...facts.evidenceProblems, ...view.caveats] : [];
20971
21126
  const caveats = attention.length === 0
20972
21127
  ? ""
20973
- : `<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>`;
20974
21129
  // v51: semantic coverage, distinct from the machine proof word above —
20975
21130
  // the same lines the CLI prints, so chat and terminal cannot disagree.
20976
21131
  // Nothing owed (optional and unsettled, or satisfied) folds behind a
@@ -21002,7 +21157,7 @@ function completionReceiptCard(view, taskId, place, standing = receiptStatusOf(v
21002
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>` : "") +
21003
21158
  // The matrix count is the proof's own citation; it reads as verified
21004
21159
  // only when the machine verified the result (workspace package 1).
21005
- `<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>` +
21006
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>` +
21007
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>` +
21008
21163
  `<div class="receipt-actions">` +
@@ -21088,9 +21243,25 @@ function resultPanelParts(detail, o) {
21088
21243
  }
21089
21244
  if (detail.ciFailing && publication !== null)
21090
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>`;
21091
21262
  const attentionHtml = attention.length === 0
21092
21263
  ? ""
21093
- : `<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>`;
21094
21265
  // ---- one outcome, one action (repair 2026-09-14) ------------------------
21095
21266
  // The outcome is the handoff's first sentence, bounded; the agent's full
21096
21267
  // account waits behind a disclosure in Summary so nothing is said twice.
@@ -21351,9 +21522,10 @@ function resultPanelParts(detail, o) {
21351
21522
  (current == null || o.place === "review" && (current.state === "ready-to-check" || current.state === "complete") ? "" :
21352
21523
  `<div class="verdict" data-current-outcome>${verdictChipsHtml(current, facts)}` +
21353
21524
  (current.receipt?.checks.status === "failed" || current.receipt?.checks.status === "unavailable" ? `<p class="problem">${escape(current.detail)}</p>` : "") + `</div>`) +
21525
+ attentionHtml +
21526
+ youCheckHtml +
21354
21527
  action +
21355
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>` : "") +
21356
- attentionHtml +
21357
21529
  tabs +
21358
21530
  view("summary", summaryParts) +
21359
21531
  view("changes", changeParts) +
@@ -21366,7 +21538,8 @@ function resultPanelParts(detail, o) {
21366
21538
  heading, outcome,
21367
21539
  verdict: current == null ? null : verdictChipsOf(current, facts),
21368
21540
  reviewHistory: REVIEW_TOKENS.has(status.token) ? status.detail : null,
21369
- attention: attention.filter(one => !shortenedMaterialReason(one)),
21541
+ attention: attention.filter(one => !shortenedMaterialReason(one)).map(plainReasonWords),
21542
+ youCheck,
21370
21543
  limits: attention.filter(one => shortenedMaterialReason(one)),
21371
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 })),
21372
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;
@@ -6188,6 +6189,7 @@ export declare class Store {
6188
6189
  historyNote?: string;
6189
6190
  }, now: Date): boolean;
6190
6191
  /** What a card's current zone is doing: its task, the card's main work, what it waits on, what zones reported. */
6192
+ /** Read and written in one transaction, so a pass changing one field never writes back another pass's older task or waiting. */
6191
6193
  updateFlowCard(id: number, change: {
6192
6194
  task?: string | null;
6193
6195
  primaryTask?: string;
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. */
@@ -18795,11 +18796,14 @@ export class Store {
18795
18796
  });
18796
18797
  }
18797
18798
  /** What a card's current zone is doing: its task, the card's main work, what it waits on, what zones reported. */
18799
+ /** Read and written in one transaction, so a pass changing one field never writes back another pass's older task or waiting. */
18798
18800
  updateFlowCard(id, change, now) {
18799
- const card = this.getFlowCard(id);
18800
- if (card === null)
18801
- return;
18802
- this.db.prepare("UPDATE flow_card SET task = ?, primary_task = ?, waiting = ?, outputs_json = ?, state = ?, updated_at = ? WHERE id = ?").run(change.task === undefined ? card.task : change.task, change.primaryTask ?? card.primaryTask, change.waiting === undefined ? card.waiting : change.waiting, JSON.stringify(change.outputs ?? card.outputs), change.state ?? card.state, now.toISOString(), id);
18801
+ this.transact(() => {
18802
+ const card = this.getFlowCard(id);
18803
+ if (card === null)
18804
+ return;
18805
+ this.db.prepare("UPDATE flow_card SET task = ?, primary_task = ?, waiting = ?, outputs_json = ?, state = ?, updated_at = ? WHERE id = ?").run(change.task === undefined ? card.task : change.task, change.primaryTask ?? card.primaryTask, change.waiting === undefined ? card.waiting : change.waiting, JSON.stringify(change.outputs ?? card.outputs), change.state ?? card.state, now.toISOString(), id);
18806
+ });
18803
18807
  }
18804
18808
  flowEvents(card) {
18805
18809
  return this.db.prepare("SELECT * FROM flow_event WHERE card = ? ORDER BY id").all(card).map(row => ({
package/dist/surface.js CHANGED
@@ -160,10 +160,16 @@ export const COMMAND_GUIDE = [
160
160
  flags: [jsonFlag, dbFlag,
161
161
  { name: 'token-env', takesValue: true, meaning: 'scoped coordinator credential environment variable' },
162
162
  { name: 'token-file', takesValue: true, meaning: 'scoped coordinator credential file; defaults to selected project reference' },
163
- ...(action === 'complete' ? [{ name: 'digest', takesValue: true, meaning: 'exact receipt from assignment show; required for JSON or scoped agents' }]
163
+ ...(action === 'complete' ? [{ name: 'digest', takesValue: true, meaning: 'exact receipt from assignment show; required for JSON or scoped agents' },
164
+ { name: 'pull-request', takesValue: false, meaning: "also open the result's pull request from that commit, as the console's Complete does; an approver's sign-in (--as/--token), never a scoped credential; refuses with pull-requests-off and the setup command" }]
164
165
  : [{ name: 'feedback', takesValue: true, meaning: 'specific requested change' }, { name: 'run', takesValue: true, meaning: 'exact result run; defaults to current result' }, { name: 'source', takesValue: true, meaning: 'exact scope digest; defaults to current terms' }, { name: 'key', takesValue: true, meaning: '32 hexadecimal characters for exact replay; defaults to a stable feedback identity' }]),
165
166
  ],
166
167
  })),
168
+ { invocation: "task merge", synopsis: "merge the task's pull request once its checks pass — the console's Merge, behind the approver's password; the project's method (squash by default), branch deleted, recorded in the ledger",
169
+ audience: "operator", agentMayInvoke: false, mutation: "identity-idempotent",
170
+ positionals: [{ name: "task", required: true, meaning: "any version of the task" }],
171
+ flags: [jsonFlag, dbFlag, { name: "as", takesValue: true, meaning: "the approver merging" }, { name: "token", takesValue: true, meaning: "their password" }],
172
+ notableReasons: ["usage", "not-an-approver"] },
167
173
  ...["show", "updates", "claim", "check", "brief", "inbox", "ack"].map(action => ({
168
174
  invocation: `assignment ${action}`,
169
175
  synopsis: action === "show" ? "read the root, current work, owner and exact receipt"
@@ -1,4 +1,6 @@
1
1
  import { type VerifiedApprover } from './principal.js';
2
+ import { type PullRequestView } from './pull-request-flow.js';
3
+ import type { PublishExec } from './publish.js';
2
4
  import type { Store } from './store.js';
3
5
  export declare function runTaskOutcomeCommand(action: 'complete' | 'revise', positional: readonly string[], flags: Map<string, string | true>, context: {
4
6
  store: Store;
@@ -8,3 +10,19 @@ export declare function runTaskOutcomeCommand(action: 'complete' | 'revise', pos
8
10
  now: Date;
9
11
  operator: () => Promise<VerifiedApprover | null>;
10
12
  }): Promise<number>;
13
+ /** `task merge <task>`: the console's Merge — the newest pull request any version of the task opened through
14
+ * Complete, merged behind the approver's password once a fresh read shows its checks passing. */
15
+ export declare function runTaskMergeCommand(positional: readonly string[], flags: Map<string, string | true>, context: {
16
+ store: Store;
17
+ json: boolean;
18
+ write: (line: string) => void;
19
+ clock: () => Date;
20
+ exec?: PublishExec;
21
+ demo: boolean;
22
+ credentials: () => Promise<{
23
+ name: string;
24
+ token: string;
25
+ } | null>;
26
+ }): Promise<number>;
27
+ /** The lines `task show` prints for a task's pull request. */
28
+ export declare function pullRequestLines(view: PullRequestView | null): string[];