toolroll 0.9.55 → 0.9.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +24 -8
- package/dist/action-ledger.d.ts +2 -4
- package/dist/action-ledger.js +2 -10
- package/dist/approval-policy.d.ts +5 -5
- package/dist/approval-policy.js +4 -4
- package/dist/approval-rules-ui.js +2 -2
- package/dist/assignment-adapters.d.ts +5 -0
- package/dist/assignment.d.ts +24 -3
- package/dist/assignment.js +36 -10
- package/dist/browser/workspace.js +5 -5
- package/dist/browser-workspace.d.ts +12 -12
- package/dist/browser-workspace.js +1 -1
- package/dist/budget-alerts.js +3 -3
- package/dist/chat-actions.d.ts +23 -18
- package/dist/chat-actions.js +110 -88
- package/dist/chat-ask.d.ts +2 -2
- package/dist/chat-ask.js +5 -5
- package/dist/chat-channel.d.ts +15 -15
- package/dist/chat-channel.js +20 -20
- package/dist/chat-controls.d.ts +0 -4
- package/dist/chat-controls.js +0 -1
- package/dist/chat-core.js +1 -1
- package/dist/chat-delivery-state.js +2 -2
- package/dist/chat-delivery.d.ts +2 -2
- package/dist/chat-delivery.js +30 -37
- package/dist/chat-flow.js +3 -3
- package/dist/chat-messages.d.ts +1 -1
- package/dist/chat-messages.js +4 -4
- package/dist/chat-migration.d.ts +1 -1
- package/dist/chat-migration.js +11 -11
- package/dist/chat-rooms.js +3 -3
- package/dist/chat-voice.js +1 -1
- package/dist/chat-warmth.d.ts +2 -2
- package/dist/check-levels.d.ts +3 -0
- package/dist/check-levels.js +5 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +6 -7
- package/dist/contracts/chat-actions.d.ts +32 -27
- package/dist/contracts/chat-actions.js +29 -11
- package/dist/contracts/chat-content.d.ts +1 -1
- package/dist/contracts/chat-content.js +1 -1
- package/dist/contracts/chat-tables.js +4 -4
- package/dist/contracts/console-api.d.ts +6 -6
- package/dist/contracts/console-api.js +7 -7
- package/dist/contracts/flow-propose.d.ts +3 -3
- package/dist/contracts/flow.d.ts +132 -125
- package/dist/contracts/flow.js +27 -12
- package/dist/contracts/lead-context.d.ts +1 -1
- package/dist/contracts/lead-context.js +2 -2
- package/dist/contracts/lead-tools.d.ts +20 -18
- package/dist/contracts/lead-tools.js +15 -15
- package/dist/contracts/stage-output.d.ts +1 -1
- package/dist/contracts/stage-output.js +1 -1
- package/dist/contracts/store-json.d.ts +23 -22
- package/dist/contracts/store-json.js +17 -17
- package/dist/contracts/{teammate-turn.d.ts → subagent-turn.d.ts} +5 -5
- package/dist/contracts/{teammate-turn.js → subagent-turn.js} +7 -7
- package/dist/converse.d.ts +23 -23
- package/dist/converse.js +28 -28
- package/dist/coordinator-proposals.d.ts +2 -2
- package/dist/coordinator-proposals.js +1 -1
- package/dist/coordinator.js +1 -1
- package/dist/deprecations.d.ts +12 -0
- package/dist/deprecations.js +20 -0
- package/dist/desktop-update-gate.js +8 -4
- package/dist/desktop-update.js +6 -1
- package/dist/discord-chat.js +6 -6
- package/dist/dispatch.d.ts +1 -1
- package/dist/dispatch.js +1 -1
- package/dist/dispose.js +0 -5
- package/dist/envelope.js +0 -1
- package/dist/export-ui.js +1 -1
- package/dist/export.js +7 -7
- package/dist/flow-engine.d.ts +2 -2
- package/dist/flow-engine.js +21 -21
- package/dist/flow-insights.d.ts +1 -1
- package/dist/flow-share.js +13 -8
- package/dist/flow-steps.d.ts +5 -5
- package/dist/flow-steps.js +19 -19
- package/dist/flow-triggers.d.ts +1 -1
- package/dist/flow-triggers.js +1 -1
- package/dist/flows-cli.js +1 -1
- package/dist/flows-ui.js +20 -20
- package/dist/flows.js +19 -19
- package/dist/guides.js +4 -3
- package/dist/invoke.js +1 -1
- package/dist/kits-ui.js +5 -5
- package/dist/kits.d.ts +12 -12
- package/dist/kits.js +31 -31
- package/dist/{mate-cli.d.ts → lead-cli.d.ts} +11 -11
- package/dist/{mate-cli.js → lead-cli.js} +38 -38
- package/dist/lead-commitments.js +4 -4
- package/dist/lead-context.d.ts +2 -2
- package/dist/lead-context.js +9 -9
- package/dist/{mate-contract.d.ts → lead-contract.d.ts} +3 -3
- package/dist/{mate-contract.js → lead-contract.js} +7 -6
- package/dist/{mate-doors.d.ts → lead-doors.d.ts} +6 -6
- package/dist/{mate-doors.js → lead-doors.js} +24 -24
- package/dist/lead-follow.d.ts +5 -5
- package/dist/lead-follow.js +9 -9
- package/dist/lead-people.d.ts +5 -5
- package/dist/lead-people.js +14 -14
- package/dist/{mate-progress.d.ts → lead-progress.d.ts} +9 -9
- package/dist/{mate-progress.js → lead-progress.js} +3 -3
- package/dist/lead-status.d.ts +1 -1
- package/dist/lead-status.js +2 -2
- package/dist/{mate-tools.d.ts → lead-tools.d.ts} +19 -19
- package/dist/{mate-tools.js → lead-tools.js} +109 -101
- package/dist/{mate.d.ts → lead.d.ts} +27 -27
- package/dist/{mate.js → lead.js} +102 -102
- package/dist/limits-ui.d.ts +1 -1
- package/dist/limits-ui.js +1 -1
- package/dist/mcp-connect.d.ts +1 -1
- package/dist/mcp-connect.js +3 -3
- package/dist/mcp-core.js +1 -1
- package/dist/memory-pass.d.ts +2 -2
- package/dist/memory-pass.js +7 -7
- package/dist/notification-rows.d.ts +2 -2
- package/dist/operate.d.ts +3 -3
- package/dist/operate.js +52 -42
- package/dist/policy.d.ts +1 -1
- package/dist/principal.d.ts +1 -1
- package/dist/principal.js +2 -2
- package/dist/project-delete-ui.js +1 -1
- package/dist/project-delete.d.ts +1 -1
- package/dist/project-delete.js +26 -26
- package/dist/project-memory.js +1 -1
- package/dist/project-tools.d.ts +1 -1
- package/dist/project-tools.js +1 -1
- package/dist/proof.d.ts +9 -0
- package/dist/proof.js +12 -0
- package/dist/recipe-ui.js +1 -1
- package/dist/release-coverage.d.ts +16 -0
- package/dist/release-coverage.js +92 -0
- package/dist/retention.js +4 -4
- package/dist/scope.d.ts +2 -2
- package/dist/scout-report.d.ts +1 -1
- package/dist/scout-report.js +1 -1
- package/dist/serve.js +40 -45
- package/dist/server/chat.d.ts +4 -4
- package/dist/server/chat.js +105 -103
- package/dist/server/chrome.js +3 -3
- package/dist/server/flows.js +69 -62
- package/dist/server/guards.js +3 -3
- package/dist/server/http.d.ts +3 -3
- package/dist/server/http.js +1 -1
- package/dist/server/live.js +6 -6
- package/dist/server/people-tokens.js +1 -1
- package/dist/server/remote.js +12 -10
- package/dist/server/render-chat.d.ts +30 -30
- package/dist/server/render-chat.js +33 -33
- package/dist/server/render-pages.js +2 -2
- package/dist/server/render-results.d.ts +4 -1
- package/dist/server/render-results.js +23 -9
- package/dist/server/render-settings.d.ts +7 -0
- package/dist/server/render-settings.js +3 -0
- package/dist/server/route-table.js +9 -7
- package/dist/server/runtime.d.ts +13 -13
- package/dist/server/settings.js +14 -13
- package/dist/server/tasks.js +34 -30
- package/dist/setup-guide.d.ts +2 -2
- package/dist/slack-chat.d.ts +2 -2
- package/dist/slack-chat.js +6 -6
- package/dist/spend-ui.d.ts +3 -3
- package/dist/spend-ui.js +9 -9
- package/dist/spend.d.ts +14 -14
- package/dist/spend.js +31 -31
- package/dist/store-baseline.d.ts +19 -0
- package/dist/store-baseline.js +2873 -0
- package/dist/store.d.ts +200 -254
- package/dist/store.js +830 -3152
- package/dist/subagent-admin.d.ts +53 -0
- package/dist/{teammate-admin.js → subagent-admin.js} +37 -37
- package/dist/{teammate-desk.d.ts → subagent-desk.d.ts} +22 -23
- package/dist/{teammate-desk.js → subagent-desk.js} +26 -35
- package/dist/{teammate-memory.d.ts → subagent-memory.d.ts} +17 -17
- package/dist/{teammate-memory.js → subagent-memory.js} +41 -41
- package/dist/{teammate-question.d.ts → subagent-question.d.ts} +6 -6
- package/dist/{teammate-question.js → subagent-question.js} +17 -17
- package/dist/{teammate-tools.d.ts → subagent-tools.d.ts} +18 -18
- package/dist/{teammate-tools.js → subagent-tools.js} +36 -36
- package/dist/{teammate-week.d.ts → subagent-week.d.ts} +10 -10
- package/dist/{teammate-week.js → subagent-week.js} +23 -23
- package/dist/{teammate-work.d.ts → subagent-work.d.ts} +21 -21
- package/dist/{teammate-work.js → subagent-work.js} +50 -50
- package/dist/subagents-ui.d.ts +22 -0
- package/dist/{teammates-ui.js → subagents-ui.js} +45 -45
- package/dist/{teammates.d.ts → subagents.d.ts} +11 -11
- package/dist/{teammates.js → subagents.js} +21 -21
- package/dist/subscription-chat.d.ts +13 -13
- package/dist/subscription-chat.js +19 -19
- package/dist/surface.d.ts +19 -2
- package/dist/surface.js +7 -31
- package/dist/task-outcome-cli.js +3 -1
- package/dist/task-status.d.ts +59 -7
- package/dist/task-status.js +132 -16
- package/dist/team-cli.d.ts +1 -0
- package/dist/team-cli.js +14 -2
- package/dist/team-leads.js +13 -13
- package/dist/team-runtime.d.ts +2 -2
- package/dist/team-runtime.js +9 -9
- package/dist/teams-chat.js +6 -6
- package/dist/telegram-flow.d.ts +3 -3
- package/dist/telegram-flow.js +3 -3
- package/dist/{telegram-mate.d.ts → telegram-lead.d.ts} +5 -5
- package/dist/{telegram-mate.js → telegram-lead.js} +27 -27
- package/dist/telegram-status.d.ts +11 -4
- package/dist/telegram-status.js +18 -11
- package/dist/telegram-team.d.ts +1 -1
- package/dist/telegram-team.js +5 -5
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +13 -34
- package/dist/text-limits.d.ts +18 -18
- package/dist/text-limits.js +18 -18
- package/dist/toolroll-update.d.ts +8 -0
- package/dist/toolroll-update.js +45 -8
- package/dist/work-index.d.ts +6 -1
- package/dist/work-index.js +50 -8
- package/dist/workspace-ui.js +2 -1
- package/package.json +1 -2
- package/dist/teammate-admin.d.ts +0 -53
- package/dist/teammates-ui.d.ts +0 -21
|
@@ -12,15 +12,15 @@ import { isSubscriptionChatProvider } from "../converse.js";
|
|
|
12
12
|
import {} from "../dispatch.js";
|
|
13
13
|
import { firstTaskJourney, START_COMMAND } from "../first-run.js";
|
|
14
14
|
import { FLOW_HREF } from "../flow-engine.js";
|
|
15
|
-
import { PROPOSAL_WAIT_REASON, proposalActGate } from "../
|
|
16
|
-
import {} from "../
|
|
17
|
-
import {
|
|
15
|
+
import { PROPOSAL_WAIT_REASON, proposalActGate } from "../lead-doors.js";
|
|
16
|
+
import {} from "../lead-progress.js";
|
|
17
|
+
import { LEAD_MESSAGE_MAX_CHARS } from "../lead.js";
|
|
18
18
|
import {} from "../needs-you.js";
|
|
19
19
|
import { projectName } from "../project.js";
|
|
20
20
|
import { replyHtmlInline, shapeReply } from "../reply-shape.js";
|
|
21
21
|
import { RESULT_REVIEW_SCRIPT } from "../result-review.js";
|
|
22
22
|
import {} from "../scope.js";
|
|
23
|
-
import {
|
|
23
|
+
import { LEAD_ASK_OTHER } from "../store.js";
|
|
24
24
|
import {} from "../task-control.js";
|
|
25
25
|
import { assignmentStageOf, stageOfCode } from "../task-status.js";
|
|
26
26
|
import {} from "../team-chat-authorization.js";
|
|
@@ -207,7 +207,7 @@ export function chatFleetOverview(snapshot, projects, interactive) {
|
|
|
207
207
|
: html `<a href="/board?scope=all" class="chat-overview-link">open board</a>`}</div><div class="chat-overview-stats"><a href="/work?view=needs-you" class="chat-overview-stat attention"><b>${needsYou}${snapshot.attentionCount?.saturated ? "+" : ""}</b><span>need you</span></a><a href="/board?scope=all" class="chat-overview-stat live"><b>${running}</b><span>building</span></a><a href="/board?scope=all&view=order" class="chat-overview-stat"><b>${queued}</b><span>queued</span></a><a href="/work" class="chat-overview-stat"><b>${done}</b><span>finished today</span></a></div>${rows.length === 0 ? html `<p class="chat-overview-clear">${needsYou === 0 && running === 0 ? html `<span class="dot dot-ok"></span>No tasks are waiting and no builds are running.` : html `<a href="/work">Open Tasks to inspect current work.</a>`}</p>` : html `<div class="chat-overview-items">${rows}</div>`}${completedWorkHtml(snapshot, projects.map(project => project.label), snapshot.assignmentStates)}${saturated ? html `<p class="meta chat-overview-note">Some items aren’t shown. Open Work to see more.</p>` : ""}</section>`;
|
|
208
208
|
}
|
|
209
209
|
/**
|
|
210
|
-
* The
|
|
210
|
+
* The lead's project rail: one bounded pulse per admitted project, plus
|
|
211
211
|
* two roads that preserve the plane's contracts. "ask" sends the stable
|
|
212
212
|
* rN alias the model already sees; "board" uses the existing POST switch
|
|
213
213
|
* instead of smuggling a project change through a GET parameter.
|
|
@@ -234,7 +234,7 @@ export function chatProjectRail(projects, inert) {
|
|
|
234
234
|
return html `<aside class="chat-projects" id="chat-project-panel" aria-label="projects in this conversation"><div class="chat-projects-head"><h2>Projects</h2><span class="badge">${projects.length}</span><button type="button" class="chat-project-close quiet" aria-label="close projects">×</button></div><div class="chat-project-list">${rows}</div></aside>`;
|
|
235
235
|
}
|
|
236
236
|
/** Spend-authorized one-click questions: ordinary /chat posts, not a new door. */
|
|
237
|
-
export function
|
|
237
|
+
export function leadPromptStarters(focus = null) {
|
|
238
238
|
const prompts = focus === null
|
|
239
239
|
? [
|
|
240
240
|
["brief me", "Brief me on what needs my attention, what is building, and the highest-leverage next action across every project."],
|
|
@@ -380,8 +380,8 @@ export function chatPage(chrome, data) {
|
|
|
380
380
|
if (!data.canManage) {
|
|
381
381
|
parts.push(html `<div class="card chat-readonly"><strong>Read-only view</strong><p class="meta">An approver can start the unified conversation and confirm its proposed actions. You can still open every live card and project board here.</p></div>`);
|
|
382
382
|
}
|
|
383
|
-
if (data.canManage && data.
|
|
384
|
-
parts.push(data.
|
|
383
|
+
if (data.canManage && data.leadMint !== undefined)
|
|
384
|
+
parts.push(data.leadMint);
|
|
385
385
|
if (data.canManage && data.coordinatorProposals !== undefined)
|
|
386
386
|
parts.push(data.coordinatorProposals);
|
|
387
387
|
parts.push(data.focusTask === null && !data.catchUp ? html `<details class="chat-fleet-context">${chatOverviewSummaryHtml(data.projects, data.fleetSnapshot?.attentionCount)}${chatFleetOverview(data.fleetSnapshot, data.projects, false)}</details>` : "", chatLimitsHtml({ provider: config.provider, model: config.model, turnsToday: data.turnsToday, dailyTurns: config.dailyTurns, subscription, weekly: subscription ? null : { spent: data.weeklySpent, ceiling: config.weeklyCeilingMicrousd } }));
|
|
@@ -437,7 +437,7 @@ export function chatAckPage(chrome, turn, nonce) {
|
|
|
437
437
|
], "\n"), { chrome });
|
|
438
438
|
}
|
|
439
439
|
/** The card that starts a conversation: the one password ceremony (mate arc §1). */
|
|
440
|
-
export function
|
|
440
|
+
export function leadMintCard(enabled, returnTo = "/chat") {
|
|
441
441
|
const subscription = enabled.billing === "subscription";
|
|
442
442
|
return html `<div class="card mate-mint" id="latest">\n<p><strong>Start a conversation</strong></p>\n${postForm("/chat/mate/mint", joinHtml([
|
|
443
443
|
html `<div class="mate-terms">`,
|
|
@@ -617,7 +617,7 @@ export function proposalCardParts(view, inert, decision, returnTo = null) {
|
|
|
617
617
|
const options = decision === null
|
|
618
618
|
? html `<p class="meta">The decision is gone</p>`
|
|
619
619
|
: html `<ul class="answer-options">${decision.options.map(one => html `<li${one.id === pick ? html ` class="picked"` : ""}><strong>${one.label}</strong>${one.reversible ? "" : html ` <span class="badge">Irreversible</span>`}${one.id === decision.recommendation ? html ` <span class="meta">— the builder recommends this</span>` : ""}${one.id === pick ? html ` <span class="meta">— proposed</span>` : ""}<p class="meta">${one.consequence}</p></li>`)}</ul>`;
|
|
620
|
-
what = html `<h3>Answer <a href="/d/${decisionId}">decision #${decisionId}</a> on <a href="${taskHref(task)}">${task}</a></h3>${decision === null ? "" : html `<p class="proposal-summary">${decision.question}</p>`}${options}<div class="proposal-rationale"><span class="eyebrow">proposed answer</span><strong>${text("optionLabel")}</strong><p>${text("rationale")}</p></div><p class="meta proposal-disclosure">${payload["readConsequences"] === true ? `${view.by.mate ? "The
|
|
620
|
+
what = html `<h3>Answer <a href="/d/${decisionId}">decision #${decisionId}</a> on <a href="${taskHref(task)}">${task}</a></h3>${decision === null ? "" : html `<p class="proposal-summary">${decision.question}</p>`}${options}<div class="proposal-rationale"><span class="eyebrow">proposed answer</span><strong>${text("optionLabel")}</strong><p>${text("rationale")}</p></div><p class="meta proposal-disclosure">${payload["readConsequences"] === true ? `${view.by.mate ? "The lead" : "The coordinator"} read every consequence but not the builder's recommendation` : `${view.by.mate ? "The lead" : "The coordinator"} did not read the consequences`}. You see both here${decision !== null && decision.state !== "open" ? " · this decision is no longer open" : ""}.</p>`;
|
|
621
621
|
}
|
|
622
622
|
else {
|
|
623
623
|
what = html `<h3>Cancel <a href="${taskHref(task)}">${task}</a></h3><p class="proposal-summary">${text("reason")}</p>${facts(["project", mono(repoId)])}`;
|
|
@@ -686,10 +686,10 @@ export function proposalCardParts(view, inert, decision, returnTo = null) {
|
|
|
686
686
|
};
|
|
687
687
|
return { html: cardHtml, card };
|
|
688
688
|
}
|
|
689
|
-
export function
|
|
690
|
-
return
|
|
689
|
+
export function leadProposalCard(proposal, inert, decision, returnTo = null) {
|
|
690
|
+
return leadProposalCardParts(proposal, inert, decision, returnTo).html;
|
|
691
691
|
}
|
|
692
|
-
export function
|
|
692
|
+
export function leadProposalCardParts(proposal, inert, decision, returnTo = null) {
|
|
693
693
|
return proposalCardParts({ id: proposal.id, kind: proposal.kind, payload: proposal.payload, state: proposal.state, outcome: proposal.outcome, by: { mate: true }, actionBase: "/chat/proposal" }, inert, decision, returnTo);
|
|
694
694
|
}
|
|
695
695
|
/** Review and inline team cards share the same server-decided controls and reasons. The team API renders these
|
|
@@ -698,7 +698,7 @@ export function teamProposalCardParts(store, actor, proposal, snapshot, csrf, de
|
|
|
698
698
|
const gate = proposalActGate(store, actor, proposal.thread, now, provider);
|
|
699
699
|
const reason = !gate.ok ? gate.said : null;
|
|
700
700
|
const back = '/chat?conversation=' + encodeURIComponent(snapshot.selected.id);
|
|
701
|
-
const parts = withFormToken(csrf, () =>
|
|
701
|
+
const parts = withFormToken(csrf, () => leadProposalCardParts(proposal, reason !== null, decision, back));
|
|
702
702
|
if (reason !== null && reason !== PROPOSAL_WAIT_REASON && proposal.state === 'pending') {
|
|
703
703
|
parts.html = replaceMarkup(parts.html, new RegExp(htmlString(html `${PROPOSAL_WAIT_REASON}`).replace(/[.*+?^${}()|[\]\\]/g, "\\$&")), () => html `${reason}`);
|
|
704
704
|
parts.card = { ...parts.card, note: reason };
|
|
@@ -717,7 +717,7 @@ export function coordinatorProposalsSection(proposals, decisions, now, heading =
|
|
|
717
717
|
export const REPLACED_THREAD_DIVIDER = "New conversation — the projects I can reach changed";
|
|
718
718
|
/** The replaced thread as the React conversation renders it: words and card outcomes, no buttons. */
|
|
719
719
|
export function replacedBrowserMessages(previous) {
|
|
720
|
-
return
|
|
720
|
+
return leadBrowserMessages({ ...previous, pending: null, ask: null, asks: new Map() }, null).map(message => ({
|
|
721
721
|
...message, cardsHtml: "",
|
|
722
722
|
cards: (message.cards ?? []).map(card => ({ ...card, primary: null, dismissable: false })),
|
|
723
723
|
}));
|
|
@@ -732,20 +732,20 @@ export function replacedThreadHtml(previous) {
|
|
|
732
732
|
}
|
|
733
733
|
/** The lead's question as buttons: each option sends itself as the next message (the same POST as a typed one);
|
|
734
734
|
* "Something else" moves to the composer. Nothing shows while a reply is running. */
|
|
735
|
-
export function
|
|
735
|
+
export function leadAskHtml(ask, target, composer, open = true) {
|
|
736
736
|
if (!open)
|
|
737
737
|
return html `<div class="so-owner-ask" data-ask="${ask.turn}"><p class="so-owner-ask-question"><strong>${ask.question}</strong></p></div>`;
|
|
738
738
|
const hidden = target.task !== null ? { task: target.task } : target.project !== null ? { project: target.project } : {};
|
|
739
|
-
return html `<div class="so-owner-ask" data-ask="${ask.turn}"><p class="so-owner-ask-question"><strong>${ask.question}</strong></p><div class="so-owner-ask-options" role="group" aria-label="Answer options">${ask.options.map(option => postForm("/chat", html `<button type="submit" name="message" value="${option}" class="so-suggestion quiet">${option}</button>`, { attrs: { class: "inline" }, hidden }))}<label for="${composer}" class="so-suggestion so-owner-ask-other">${
|
|
739
|
+
return html `<div class="so-owner-ask" data-ask="${ask.turn}"><p class="so-owner-ask-question"><strong>${ask.question}</strong></p><div class="so-owner-ask-options" role="group" aria-label="Answer options">${ask.options.map(option => postForm("/chat", html `<button type="submit" name="message" value="${option}" class="so-suggestion quiet">${option}</button>`, { attrs: { class: "inline" }, hidden }))}<label for="${composer}" class="so-suggestion so-owner-ask-other">${LEAD_ASK_OTHER}</label></div></div>`;
|
|
740
740
|
}
|
|
741
741
|
/** The thread's messages as the React conversation renders them, each
|
|
742
742
|
* with the proposal cards its turn produced; `back` is where a card's
|
|
743
743
|
* confirm or dismiss returns. */
|
|
744
|
-
export function
|
|
744
|
+
export function leadBrowserMessages(rows, back, target = { task: null, project: null }) {
|
|
745
745
|
const open = rows.pending === null ? rows.ask?.turn ?? null : null;
|
|
746
746
|
const asked = (turn) => {
|
|
747
747
|
const ask = turn === null ? undefined : rows.asks?.get(turn);
|
|
748
|
-
return ask === undefined ? "" :
|
|
748
|
+
return ask === undefined ? "" : leadAskHtml(ask, target, target.task === null ? "lead-message" : "task-message", ask.turn === open);
|
|
749
749
|
};
|
|
750
750
|
// The owner's message each reply answers: when it asked for ids, the reply keeps them.
|
|
751
751
|
const askedBy = new Map();
|
|
@@ -759,7 +759,7 @@ export function mateBrowserMessages(rows, back, target = { task: null, project:
|
|
|
759
759
|
...(() => {
|
|
760
760
|
// Under the lead's message only, as the server-rendered thread does: the person's message shares the turn.
|
|
761
761
|
const parts = message.turn === null || message.role === 'operator' ? [] : rows.proposals.filter(one => one.turn === message.turn)
|
|
762
|
-
.map(one =>
|
|
762
|
+
.map(one => leadProposalCardParts(one, rows.pending !== null, rows.decisions.get(typeof one.payload['decision'] === 'number' ? one.payload['decision'] : -1) ?? null, back));
|
|
763
763
|
return { cardsHtml: htmlString(joinHtml(parts.map(one => one.html))), cards: parts.map(one => one.card) };
|
|
764
764
|
})(),
|
|
765
765
|
}));
|
|
@@ -770,7 +770,7 @@ export function mateBrowserMessages(rows, back, target = { task: null, project:
|
|
|
770
770
|
* decisions the cards name, and the task lens's own facts — and nothing
|
|
771
771
|
* that merely ticks: no relative age, no freshly minted nonce, no csrf.
|
|
772
772
|
* Equal versions mean equal fragments; the poll fetches nothing else. */
|
|
773
|
-
export function
|
|
773
|
+
export function leadChatVersion(rows) {
|
|
774
774
|
const focus = rows.focusTask;
|
|
775
775
|
const facts = [
|
|
776
776
|
rows.messages.map(one => [one.id, one.role, one.turn]),
|
|
@@ -805,7 +805,7 @@ export function mateChatVersion(rows) {
|
|
|
805
805
|
* populated thread. Every child carries a stable `data-key` so the page
|
|
806
806
|
* keeps an unchanged node (its open disclosures, its focus) and replaces
|
|
807
807
|
* only what the server rendered differently. The composer is never here. */
|
|
808
|
-
export function
|
|
808
|
+
export function leadThreadHtml(data) {
|
|
809
809
|
const returnTo = data.focusTask === null ? "/chat" : taskChatHref(data.focusTask.id);
|
|
810
810
|
const parts = [];
|
|
811
811
|
if (data.problem !== null)
|
|
@@ -834,7 +834,7 @@ export function mateThreadHtml(data) {
|
|
|
834
834
|
}
|
|
835
835
|
const cards = message.turn === null ? [] : (byTurn.get(message.turn) ?? []);
|
|
836
836
|
const ask = message.turn === null ? undefined : data.asks?.get(message.turn);
|
|
837
|
-
thread.push(html `<div class="msg mate" data-message-role="assistant" data-key="m${message.id}"${message.id === latestReply ? html ` id="latest"` : ""}>${renderChatText(message.text, asked)}${ask === undefined ? "" :
|
|
837
|
+
thread.push(html `<div class="msg mate" data-message-role="assistant" data-key="m${message.id}"${message.id === latestReply ? html ` id="latest"` : ""}>${renderChatText(message.text, asked)}${ask === undefined ? "" : leadAskHtml(ask, { task: data.focusTask?.id ?? null, project: data.chatProject ?? null }, "chat-message", data.pending === null && data.ask?.turn === ask.turn)}${cards.map(one => leadProposalCard(one, inert, data.decisions.get(typeof one.payload["decision"] === "number" ? one.payload["decision"] : -1) ?? null, data.focusTask === null ? null : returnTo))}<div class="chat-message-foot">${chatActivity(message.activity)}<time datetime="${message.createdAt}">${relativeAge(message.createdAt, data.now)}</time></div></div>`);
|
|
838
838
|
}
|
|
839
839
|
parts.push(html `<div class="thread" data-key="thread" data-chat-list>\n${joinHtml(thread, "\n")}\n</div>`);
|
|
840
840
|
if (data.pending !== null) {
|
|
@@ -850,10 +850,10 @@ export function mateThreadHtml(data) {
|
|
|
850
850
|
}
|
|
851
851
|
/** What follows the composer (package 2): the starters of an EMPTY thread
|
|
852
852
|
* sit under the box; once the first message lands they move above it. */
|
|
853
|
-
export function
|
|
854
|
-
return html `<div id="chat-after-composer" data-chat-region="after">${data.messages.length === 0 && data.pending === null ?
|
|
853
|
+
export function leadAfterComposerHtml(data) {
|
|
854
|
+
return html `<div id="chat-after-composer" data-chat-region="after">${data.messages.length === 0 && data.pending === null ? leadPromptStarters(data.focusTask) : ""}</div>`;
|
|
855
855
|
}
|
|
856
|
-
export function
|
|
856
|
+
export function leadPage(chrome, data) {
|
|
857
857
|
const subscription = isSubscriptionChatProvider(data.config.provider);
|
|
858
858
|
const chatProject = data.focusTask === null ? data.chatProject ?? null : null;
|
|
859
859
|
const returnTo = data.focusTask !== null ? taskChatHref(data.focusTask.id) : chatProject !== null ? projectChatHref(chatProject) : "/chat";
|
|
@@ -888,16 +888,16 @@ export function matePage(chrome, data) {
|
|
|
888
888
|
const lastMessage = data.messages.at(-1);
|
|
889
889
|
const latestReply = data.pending === null && lastMessage?.role === "assistant" ? lastMessage.id : null;
|
|
890
890
|
conversation.push(replacedThreadHtml(data.previous));
|
|
891
|
-
conversation.push(
|
|
891
|
+
conversation.push(leadThreadHtml({ ...data, problem: null, chatProject }));
|
|
892
892
|
conversation.push(
|
|
893
893
|
// The New update action (package 2): hidden until a live update lands
|
|
894
894
|
// while the reader is above the latest message; a real button, so the
|
|
895
895
|
// keyboard reaches it. Only this act moves the reader.
|
|
896
896
|
html `<div class="chat-new-update-holder"><button type="button" class="chat-new-update" id="chat-new-update" hidden>New update ↓</button></div>`, postForm("/chat", joinHtml([
|
|
897
|
-
html `<label>Message<textarea id="chat-message" name="message" rows="1" maxlength="${
|
|
897
|
+
html `<label>Message<textarea id="chat-message" name="message" rows="1" maxlength="${LEAD_MESSAGE_MAX_CHARS}" placeholder="${data.focusTask !== null ? "Ask about this task…" : chatProject !== null ? `Ask about ${projectName(chatProject)}…` : "Describe what you want done…"}"></textarea></label>`,
|
|
898
898
|
html `<button type="submit" aria-label="${data.pending === null ? "send message" : "wait for the current reply before sending"}"${data.pending === null ? "" : html ` disabled`}>Send</button>`,
|
|
899
899
|
], "\n"), {
|
|
900
|
-
attrs: { class: "card composer", id: latestReply === null && data.pending === null ? "latest" : "chat-composer", "aria-label": "message the
|
|
900
|
+
attrs: { class: "card composer", id: latestReply === null && data.pending === null ? "latest" : "chat-composer", "aria-label": "message the lead", "data-chat-session": data.session.id, "data-chat-task": data.focusTask?.id ?? "", "data-chat-user": data.session.approver, "data-chat-busy": data.pending === null ? "0" : "1", "data-chat-version": leadChatVersion(data), "data-chat-approval": data.focusTask?.approval?.digest ?? "" },
|
|
901
901
|
hidden: { request: randomBytes(16).toString("hex"), "request-session": data.session.id, task: data.focusTask?.id, project: chatProject, result: data.resultRunId },
|
|
902
902
|
}),
|
|
903
903
|
// Concise pass (2026-09-13): the status line speaks only when there is
|
|
@@ -906,13 +906,13 @@ export function matePage(chrome, data) {
|
|
|
906
906
|
html `<p class="meta composer-hint" id="chat-connection" role="status" aria-live="polite">${data.pending === null ? "" : "Reply in progress. You can draft your next message or come back later."}</p>`,
|
|
907
907
|
// The explicit reconnection (package 2): shown only once the session
|
|
908
908
|
// or sign-in changed under this page; it reloads on the reader's act.
|
|
909
|
-
html `<p class="meta composer-hint" id="chat-reconnect" hidden><button type="button" class="quiet">Reconnect</button></p>`,
|
|
909
|
+
html `<p class="meta composer-hint" id="chat-reconnect" hidden><button type="button" class="quiet">Reconnect</button></p>`, leadAfterComposerHtml(data), controlsHtml);
|
|
910
910
|
return screen("chat", chatWorkspace(joinHtml(conversation, "\n"), data.projects, false, data.focusTask, data.resultPanel ?? null), { chrome, functional: { script: CHAT_CONTINUITY_SCRIPT + CHAT_UI_SCRIPT + (data.focusTask === null ? "" : RESULT_REVIEW_SCRIPT), fetches: true },
|
|
911
911
|
workspace: {
|
|
912
912
|
conversation: {
|
|
913
|
-
sessionId: data.session.id, user: data.session.approver, version:
|
|
914
|
-
messages:
|
|
915
|
-
pendingTurnId: data.pending?.id ?? null, requestId: randomBytes(16).toString('hex'), maxChars:
|
|
913
|
+
sessionId: data.session.id, user: data.session.approver, version: leadChatVersion(data),
|
|
914
|
+
messages: leadBrowserMessages(data, data.focusTask === null && chatProject === null ? null : returnTo, { task: data.focusTask?.id ?? null, project: chatProject }),
|
|
915
|
+
pendingTurnId: data.pending?.id ?? null, requestId: randomBytes(16).toString('hex'), maxChars: LEAD_MESSAGE_MAX_CHARS,
|
|
916
916
|
taskId: data.focusTask?.id ?? null, resultRunId: data.resultRunId ?? null, project: chatProject,
|
|
917
917
|
...(data.previous == null ? {} : { previous: { messages: replacedBrowserMessages(data.previous) } }),
|
|
918
918
|
},
|
|
@@ -551,9 +551,9 @@ export function workPage(chrome, data) {
|
|
|
551
551
|
? html `<div class="work-empty" data-work-empty="${data.view}"><p>${data.previous ? 'There are no more tasks on this page.' : current.empty}</p>${data.view === 'all' && !data.previous ? html `<p><a class="button-link" href="${chrome.chat === true ? '/chat' : '/tasks/new'}">${chrome.chat === true ? 'Start in chat' : 'Add a task'}</a></p>` : html `<a href="${href('all')}">See all tasks →</a>`}</div>`
|
|
552
552
|
: html `<div class="work-list">${data.work.items.map(rowHtml)}</div>`;
|
|
553
553
|
const pages = !data.previous && data.work.nextCursor === null ? null : html `<nav class="row work-pagination" aria-label="Task pages">${data.previous ? html `<a class="button-link" href="${href(data.view)}">First page</a>` : null}${data.work.nextCursor === null ? null : html `<a class="button-link" rel="next" href="${href(data.view, data.work.nextCursor)}">Next page</a>`}</nav>`;
|
|
554
|
-
const tools = html `<details class="work-tools"><summary>Work tools${CHEVRON_ICON}</summary><nav class="work-tools-menu">${[['/inbox', 'Inbox'],
|
|
554
|
+
const tools = html `<details class="work-tools"><summary>Work tools${CHEVRON_ICON}</summary><nav class="work-tools-menu">${[['/inbox', 'Inbox'], ['/board', 'Board'], ['/board?view=order', 'Order'], ['/tasks', 'Task list'], ['/recipes', 'Recipes'], ...(chrome.projectScoped ? [] : [['/workbench', 'Portfolio']]), ['/ledger', 'Action ledger']]
|
|
555
555
|
.map(([path, label]) => html `<a href="${path}">${label}</a>`)}</nav></details>`;
|
|
556
|
-
const toolLinks = [['/inbox', 'Inbox'],
|
|
556
|
+
const toolLinks = [['/inbox', 'Inbox'], ['/board', 'Board'], ['/board?view=order', 'Order'], ['/tasks', 'Task list'], ['/recipes', 'Recipes'], ...(chrome.projectScoped ? [] : [['/workbench', 'Portfolio']])];
|
|
557
557
|
const view = {
|
|
558
558
|
kind: 'tasks',
|
|
559
559
|
tabs: WORK_VIEWS.map(one => ({ label: one.label, href: href(one.key), count: data.work.totals[one.key], active: one.key === data.view })),
|
|
@@ -12,7 +12,7 @@ import { type BuildReviewView } from "../review-switch.js";
|
|
|
12
12
|
import { type AcceptanceCriterion, type approvalOf } from "../scope.js";
|
|
13
13
|
import { type ReportItem } from "../scout-report.js";
|
|
14
14
|
import { type Artifact, type DiffComment, type Publication, type RepairChainRow, type ReviewRetryState, type Run, type Store, type TaskState } from "../store.js";
|
|
15
|
-
import { type PullRequestFact, type TaskStatus } from "../task-status.js";
|
|
15
|
+
import { type ReleaseState, type PullRequestFact, type TaskStatus } from "../task-status.js";
|
|
16
16
|
import { type DisplayStatus, type PublicationFacts, type ReviewFacts } from "../workspace-ui.js";
|
|
17
17
|
import { type Chrome, type Screen } from "./chrome.js";
|
|
18
18
|
import { type Html } from "../html.js";
|
|
@@ -78,6 +78,7 @@ export declare function criterionMatrixHtml(matrix: readonly CriterionMatrixRow[
|
|
|
78
78
|
links?: EvidenceLinkMap;
|
|
79
79
|
fileAnchors?: ReadonlyMap<string, string>;
|
|
80
80
|
verdict?: string | null;
|
|
81
|
+
release?: ReleaseState | undefined;
|
|
81
82
|
}): Html;
|
|
82
83
|
/** One policy summary beneath the requirements. Per-requirement concerns
|
|
83
84
|
* are already visible in the matrix; do not repeat their full text here. */
|
|
@@ -674,6 +675,8 @@ export declare const CHECK_EXCERPT_LINES = 12;
|
|
|
674
675
|
* change's first file, labelled as not cited), the screenshots it cites
|
|
675
676
|
* (none cited: every validated one), and the agent's own note. */
|
|
676
677
|
export declare function personCheckItems(proof: ProofBundleView, patchText: string | null, shots: readonly ResultScreenshot[], runId: number): BrowserCheckItem[];
|
|
678
|
+
/** The completion blocker no acceptance resolves (a failed check, an unresolved HIGH finding), in words, or null. */
|
|
679
|
+
export declare function hardBlockerOf(assignment: AssignmentSnapshot | null | undefined): string | null;
|
|
677
680
|
/** A Needs you result's one action (needs-you.ts): a link to the act that resolves it, or Confirm it
|
|
678
681
|
* stopped behind the password. Null under every other headline. */
|
|
679
682
|
export declare function needActionOf(assignment: AssignmentSnapshot | null, status: TaskStatus | null, csrf: string, returnTo: string, result?: {
|
|
@@ -21,7 +21,7 @@ import {} from "../scope.js";
|
|
|
21
21
|
import { parseReport } from "../scout-report.js";
|
|
22
22
|
import {} from "../store.js";
|
|
23
23
|
import { runCostWords } from "../summary.js";
|
|
24
|
-
import { demoChecksOf, pullRequestFactOf, requirementsOf, requirementWordOf, STATUS_MORE, statusDetailsHtml, statusIconSvg, statusWhyHtml } from "../task-status.js";
|
|
24
|
+
import { checkBackingOf, CHECKED_AT_RELEASE, demoChecksOf, pullRequestFactOf, requirementsOf, requirementWordOf, STATUS_MORE, statusDetailsHtml, statusIconSvg, statusWhyHtml } from "../task-status.js";
|
|
25
25
|
import { ACCEPT_NEEDS_REASON, acceptWordsOf, cantAcceptYetOf, evidenceProblemOf, MISMATCH_HEADLINE, receiptHeadingOf, receiptPublicationWords, reportMismatchesOf, RESULT_DECISION_SENTENCE, resultHeadlineOf, resultStatusOf, REVIEW_TOKENS } from "../workspace-ui.js";
|
|
26
26
|
import { createHash, randomBytes } from "node:crypto";
|
|
27
27
|
import { buildsViews, oneLineOf, projectChip, safePrUrl, screen, sentenceCase, strokeIcon, when, whenTime } from "./chrome.js";
|
|
@@ -173,13 +173,14 @@ export function criterionMatrixHtml(matrix, options = {}) {
|
|
|
173
173
|
const confirmed = row.assessment !== undefined && state === "pass";
|
|
174
174
|
// Plain words (task-status brief): the retired assessment step is not a state a person acts on.
|
|
175
175
|
// The card's Requirements row reads the same words from the same source (task-status.ts), so they agree.
|
|
176
|
-
const label = requirementWordOf(row, options.verdict);
|
|
176
|
+
const label = requirementWordOf(row, options.verdict, options.release);
|
|
177
177
|
const cls = label === "Met" ? "badge-done" : label === "You check" ? "badge-manual-review" : "badge-note";
|
|
178
178
|
const warnings = [];
|
|
179
179
|
// Only replace the known boilerplate. Other recorded failure details
|
|
180
180
|
// remain verbatim, so concision cannot hide a different problem.
|
|
181
181
|
const detail = row.detail.filter(line => !(row.assessment && row.review && line === `${row.review.author} ${row.review.judgement === "contradicts" ? "contradicts" : "needs more evidence for"} criterion "${row.id}": ${row.review.note}`) && !(state === "manual-review" && /^criterion "[^"\n]+" requires manual-review evidence — an operator must accept it before this can verify$/.test(line)));
|
|
182
|
-
|
|
182
|
+
// A requirement a release check covers, or that waits only on one, has nothing wrong to say.
|
|
183
|
+
if (!awaitingAssessment && state !== "pass" && label !== "Met" && label !== CHECKED_AT_RELEASE && detail.length > 0)
|
|
183
184
|
warnings.push(html `<ul class="requirement-issues">${detail.map(line => html `<li>${line}</li>`)}</ul>`);
|
|
184
185
|
const review = row.review;
|
|
185
186
|
if (review !== null && review.judgement !== "upholds") {
|
|
@@ -671,13 +672,16 @@ export function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab,
|
|
|
671
672
|
action: false,
|
|
672
673
|
});
|
|
673
674
|
parts.push(html `<div id="verification" data-cockpit-section="result">${panel.html}</div>`);
|
|
674
|
-
|
|
675
|
+
// What no acceptance resolves (a failed check, a HIGH finding): Complete is refused, so no Accept is offered and
|
|
676
|
+
// the one line says the next step.
|
|
677
|
+
const hardStop = hardBlockerOf(assignment);
|
|
678
|
+
const blocked = hardStop ?? cantAcceptYetOf(proof?.verdict ?? null, proof?.reasons ?? [], accepted);
|
|
675
679
|
const youCheck = panel.panel.youCheck;
|
|
676
680
|
// Accept and finish posts the completion; when an acceptance is owed (the person's own checks, or a reason a
|
|
677
681
|
// report that doesn't match its changes asks for), the same request records it first.
|
|
678
682
|
const owed = accepted ? null : youCheck?.accept != null ? { note: null } : panel.panel.need?.accept != null ? { note: panel.panel.need.accept.note }
|
|
679
683
|
: blocked === ACCEPT_NEEDS_REASON ? { note: "Why is this safe to accept?" } : null;
|
|
680
|
-
const complete = assignment?.state === "ready-to-check" && assignment.receipt !== null && canRetryReview && csrf !== ""
|
|
684
|
+
const complete = hardStop === null && assignment?.state === "ready-to-check" && assignment.receipt !== null && canRetryReview && csrf !== ""
|
|
681
685
|
? { action: `${taskHref(view.taskId)}/complete`, receipt: assignment.receipt.digest, run: run.id, accept: owed } : null;
|
|
682
686
|
if (complete !== null)
|
|
683
687
|
parts.push(completionForm(view.taskId, run.id, complete.receipt, view.detail?.pullRequestTo ?? null, owed));
|
|
@@ -686,8 +690,10 @@ export function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab,
|
|
|
686
690
|
const matrix = proof === null || proof.proofProblem !== null ? [] : proof.matrix;
|
|
687
691
|
const unanswered = accepted || youCheck == null ? [] : youCheck.items.length > 0 ? youCheck.items.map(one => one.statement === "" ? one.words : one.statement) : youCheck.lines;
|
|
688
692
|
const base = !acceptsHere ? null : acceptWordsOf({
|
|
689
|
-
|
|
690
|
-
|
|
693
|
+
// Checks Off that a release check already covers read as passed (release-coverage.ts).
|
|
694
|
+
checks: checks === undefined ? null : checks.running != null ? "running" : checks.level === "off" && checks.status !== "passed" ? checks.release != null ? "passed" : "off" : checks.status,
|
|
695
|
+
// Release-aware words (task-status.ts): a requirement waiting only on a release check, or covered by one, isn't unmet.
|
|
696
|
+
unmet: matrix.filter(row => !["Met", "You check", CHECKED_AT_RELEASE].includes(requirementWordOf(row, null, checks === undefined ? undefined : checkBackingOf(checks)))).length,
|
|
691
697
|
action: complete !== null ? "complete" : "accept",
|
|
692
698
|
publishing: view.detail?.publishing ?? "other",
|
|
693
699
|
proof: proof !== null && proof.proof !== null && proof.proofProblem === null,
|
|
@@ -700,7 +706,7 @@ export function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab,
|
|
|
700
706
|
const followUps = view.detail.followUps ?? null;
|
|
701
707
|
const checksRunning = checks?.running != null || (followUps?.checks.some(one => one.state === "waiting" || one.state === "running") ?? false);
|
|
702
708
|
const runChecks = canRetryReview && csrf !== "" && followUps !== null && (followUps.full || followUps.quick) && !checksRunning &&
|
|
703
|
-
(checks === undefined || checks.status === "not-run" || checks.status === "unavailable")
|
|
709
|
+
(checks === undefined || ((checks.status === "not-run" || checks.status === "unavailable") && checks.release == null))
|
|
704
710
|
// Back on this result's Checks tab, where #follow-ups shows the run it started.
|
|
705
711
|
? { action: `/r/${run.id}/checks`, level: followUps.full ? "full" : "quick", returnTo: `${here}&run=${run.id}&tab=checks` } : null;
|
|
706
712
|
const need = panel.panel.need;
|
|
@@ -1774,7 +1780,8 @@ export function resultPanelParts(detail, o) {
|
|
|
1774
1780
|
checkParts.push(detail.signedCriteria > 0 ? html `<p class="meta">The approved scope has ${detail.signedCriteria} requirement${detail.signedCriteria === 1 ? "" : "s"}, but this build has no requirement-by-requirement verification.</p>` : html `<p class="meta">This scope signed no acceptance checks.</p>`);
|
|
1775
1781
|
}
|
|
1776
1782
|
else {
|
|
1777
|
-
checkParts.push(criterionMatrixHtml(proof.matrix, { runId, links: proof.matrixLinks, fileAnchors: detail.fileAnchors, verdict: proof.verdict
|
|
1783
|
+
checkParts.push(criterionMatrixHtml(proof.matrix, { runId, links: proof.matrixLinks, fileAnchors: detail.fileAnchors, verdict: proof.verdict,
|
|
1784
|
+
release: current?.receipt == null ? undefined : checkBackingOf(current.receipt.checks) }));
|
|
1778
1785
|
}
|
|
1779
1786
|
checkParts.push(semanticCoverageHtml(proof.matrix, proof.qualityMode));
|
|
1780
1787
|
if (reviewConflict(proof.matrix, proof.machineVerdict, proof.verdict))
|
|
@@ -1930,11 +1937,18 @@ export function personCheckItems(proof, patchText, shots, runId) {
|
|
|
1930
1937
|
};
|
|
1931
1938
|
});
|
|
1932
1939
|
}
|
|
1940
|
+
/** The completion blocker no acceptance resolves (a failed check, an unresolved HIGH finding), in words, or null. */
|
|
1941
|
+
export function hardBlockerOf(assignment) {
|
|
1942
|
+
return assignment?.readiness?.blockers.find(one => one.key === "check-failed" || one.key === "high")?.message ?? null;
|
|
1943
|
+
}
|
|
1933
1944
|
/** A Needs you result's one action (needs-you.ts): a link to the act that resolves it, or Confirm it
|
|
1934
1945
|
* stopped behind the password. Null under every other headline. */
|
|
1935
1946
|
export function needActionOf(assignment, status, csrf, returnTo, result) {
|
|
1936
1947
|
if (assignment === null || status === null || status.headline !== "Needs you" || status.need == null)
|
|
1937
1948
|
return null;
|
|
1949
|
+
// Accepting can't resolve it: Request changes is the way on, beside the reason.
|
|
1950
|
+
if (result !== undefined && hardBlockerOf(assignment) !== null)
|
|
1951
|
+
return null;
|
|
1938
1952
|
const action = assignment.primaryAction;
|
|
1939
1953
|
const rebuild = { href: null, confirm: null, rebuild: { action: `${taskHref(assignment.rootId)}/requeue` } };
|
|
1940
1954
|
// Built to an earlier plan: Build again, in place (the task page's requeue).
|
|
@@ -44,6 +44,13 @@ export declare function leadSettingsHtml(data: {
|
|
|
44
44
|
when: string;
|
|
45
45
|
until: string;
|
|
46
46
|
}[];
|
|
47
|
+
/** D5: the lead's subagents in the projects this person can use: name and role, project, and whether it's paused. */
|
|
48
|
+
subagents?: {
|
|
49
|
+
id: number;
|
|
50
|
+
label: string;
|
|
51
|
+
project: string;
|
|
52
|
+
paused: boolean;
|
|
53
|
+
}[];
|
|
47
54
|
}): Html;
|
|
48
55
|
/** The lead's full settings (Settings → Lead → Advanced): provider, model, limits and a direct API key. */
|
|
49
56
|
export declare function leadConfigForm(current: import("../store.js").ChatConfig | null, data: LeadFormFacts): Html;
|
|
@@ -53,6 +53,9 @@ export function leadSettingsHtml(data) {
|
|
|
53
53
|
// What the lead promised to follow up on; it reports each once, here in chat, and drops it after 7 days.
|
|
54
54
|
(data.promises ?? []).length === 0 ? "" : html `<section class="card lead-promises" data-lead-promises><h2>Promises</h2><ul class="lead-promise-list">${data.promises.map(one => html `<li data-promise="${one.id}"><p>${one.what}</p><p class="meta">${one.when.charAt(0).toUpperCase() + one.when.slice(1)} · <span class="nowrap">until ${new Date(one.until).toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" })}</span></p>\
|
|
55
55
|
${postForm("/settings/lead/promise/cancel", html `<button type="submit" class="secondary">Cancel</button>`, { attrs: { class: "inline" }, returnTo, hidden: { promise: one.id } })}</li>`)}</ul></section>`,
|
|
56
|
+
// D5: the lead's subagents, one line each; everything else about one is on its own page.
|
|
57
|
+
data.subagents === undefined ? "" : html `<section class="card lead-subagents" id="subagents" data-lead-subagents><h2>Subagents</h2>${data.subagents.length === 0 ? html `<p class="meta">None yet. Your lead can ask one for help once it's here.</p>`
|
|
58
|
+
: html `<ul class="lead-subagent-list">${data.subagents.map(one => html `<li data-subagent="${one.id}"><a href="/settings/lead/subagents/${one.id}">${one.label}</a><span class="meta">${one.project}${one.paused ? " · Paused" : ""}</span></li>`)}</ul>`}<p><a class="button-link" href="/settings/lead/subagents?add=1#add" data-add-subagent>Add subagent</a></p></section>`,
|
|
56
59
|
html `<details class="lead-advanced" data-lead-advanced><summary>Advanced</summary>`,
|
|
57
60
|
leadConfigForm(config, facts),
|
|
58
61
|
config === null ? "" : postForm("/chat/config", html `<input type="password" name="token" placeholder="your password" autocomplete="current-password" aria-label="Your password"><button type="submit" class="secondary">Turn the lead off</button>`, { attrs: { class: "inline" }, returnTo, hidden: { off: "1" } }),
|
|
@@ -148,9 +148,11 @@ get("flows", "flows.gallery", "^\\/flows\\/new\\/[a-z-]{1,40}$", { limited: "res
|
|
|
148
148
|
get("flows", "flows.page", "/flows", { limited: "resource", ...free });
|
|
149
149
|
get("flows", "kits", "/kits");
|
|
150
150
|
get("flows", "kit.page", "^\\/kits\\/[a-z-]{1,40}$", { sample: "/kits/support" });
|
|
151
|
-
get("flows", "
|
|
152
|
-
|
|
153
|
-
get("flows", "
|
|
151
|
+
get("flows", "subagents", "/settings/lead/subagents", free);
|
|
152
|
+
// D5: subagents were teammates; old links to them land on the same pages under Settings → Lead.
|
|
153
|
+
get("flows", "subagents.legacy", "^\\/teammates(\\/[1-9][0-9]{0,9}(\\/soul\\.md)?)?$", { ...free, sample: "/teammates/1" });
|
|
154
|
+
get("flows", "subagent.page", `^\\/settings\\/lead\\/subagents\\/[1-9][0-9]{0,9}$`, { ...free, sample: "/settings/lead/subagents/1" });
|
|
155
|
+
get("flows", "subagent.soul-file", `^\\/settings\\/lead\\/subagents\\/[1-9][0-9]{0,9}\\/soul\\.md$`, { ...free, sample: "/settings/lead/subagents/1/soul.md" });
|
|
154
156
|
get("flows", "flow.insights", `^\\/flows\\/${FLOW}\\/insights$`, { project: "flow", limited: "resource", ...free, sample: "/flows/1/insights" });
|
|
155
157
|
get("flows", "flow.run", `^\\/flows\\/${FLOW}\\/runs\\/${FLOW}\\/${FLOW}$`, { project: "flow", limited: "resource", ...free, sample: "/flows/1/runs/2/3" });
|
|
156
158
|
get("flows", "flow.export", `^\\/flows\\/${FLOW}\\/export$`, { project: "flow", limited: "resource", ...free, sample: "/flows/1/export" });
|
|
@@ -222,11 +224,11 @@ post("settings", "settings.skills-import", "/settings/skills/import", { project:
|
|
|
222
224
|
post("settings", "settings.skills-change", "/settings/skills/change", { project: "form", limited: "unscoped", ...approver("Sign in as an approver to manage skills.", "/settings/skills") });
|
|
223
225
|
for (const one of ["setup", "sample", "github"])
|
|
224
226
|
post("flows", `kit.${one}`, `^\\/kits\\/[a-z-]{1,40}\\/${one}$`, { project: "form", sample: `/kits/support/${one}`, ...approver("Sign in as an approver to set kits up.", "/kits") });
|
|
225
|
-
const
|
|
226
|
-
post("flows", "
|
|
227
|
+
const SUBAGENTS = anyApprover("Sign in as an approver to look after subagents.", "/settings/lead/subagents");
|
|
228
|
+
post("flows", "subagent.new", "/settings/lead/subagents/new", { project: "form", ...SUBAGENTS });
|
|
227
229
|
for (const one of ["soul", "state", "note", "settings", "summary", "tools", "memory", "routines", "week"])
|
|
228
|
-
post("flows", `
|
|
229
|
-
post("flows", "
|
|
230
|
+
post("flows", `subagent.${one}`, `^\\/settings\\/lead\\/subagents\\/[1-9][0-9]{0,9}\\/${one}$`, { sample: `/settings/lead/subagents/1/${one}`, ...SUBAGENTS });
|
|
231
|
+
post("flows", "subagent.answer", "^\\/settings\\/lead\\/subagents\\/questions\\/[1-9][0-9]{0,9}\\/answer$", { sample: "/settings/lead/subagents/questions/1/answer" });
|
|
230
232
|
post("flows", "flows.gallery-create", "^\\/flows\\/new\\/[a-z-]{1,40}$", { project: "form", limited: "resource", sample: "/flows/new/triage" });
|
|
231
233
|
// Flow editing answers JSON: a caller without approver standing hears it in the editor's own format.
|
|
232
234
|
const CREATE_FLOWS = approver("Sign in as an approver to create flows.", "/flows");
|
package/dist/server/runtime.d.ts
CHANGED
|
@@ -24,8 +24,8 @@ import { type ProviderConnection } from "../provider-connection.js";
|
|
|
24
24
|
import { type ProviderId } from "../provider.js";
|
|
25
25
|
import { authenticateApprover as checkApproverPassword, type Scope } from "../scope.js";
|
|
26
26
|
import { SourceBudget } from "../sign-in-guard.js";
|
|
27
|
-
import type { CoordinatorProposal, DirectChatProviderId,
|
|
28
|
-
import { type Decision, type
|
|
27
|
+
import type { CoordinatorProposal, DirectChatProviderId, LeadMessage, LeadProposal, LeadTurn, PlanRevision, TaskFamily, TaskRef } from "../store.js";
|
|
28
|
+
import { type Decision, type LeadAsk, type LeadThreadScope, type Run, type Store, type Task, type TaskState } from "../store.js";
|
|
29
29
|
import type { TeamChatProviderResolver } from '../team-chat-authorization.js';
|
|
30
30
|
import type { TeamResponse } from '../team-contract.js';
|
|
31
31
|
import { createTeamRuntime } from '../team-runtime.js';
|
|
@@ -108,24 +108,24 @@ export interface ServerRuntime {
|
|
|
108
108
|
flowRooms: FlowRooms;
|
|
109
109
|
providerHome: string;
|
|
110
110
|
toolHome: string;
|
|
111
|
-
|
|
111
|
+
leadPrincipal: (who: Who & {
|
|
112
112
|
via: "cookie";
|
|
113
113
|
}) => VerifiedApprover | null;
|
|
114
|
-
chatScopeOf: (focusTask: TaskChatFocus | null, chatProject: string | null) =>
|
|
114
|
+
chatScopeOf: (focusTask: TaskChatFocus | null, chatProject: string | null) => LeadThreadScope;
|
|
115
115
|
liveTurns: Map<number, LiveTurn>;
|
|
116
116
|
/** A lead reply started on a thread: pages with that conversation open follow it (server/live.ts). */
|
|
117
117
|
liveTurnStarted: (thread: number) => void;
|
|
118
|
-
|
|
118
|
+
leadConversationRows: (who: Who & {
|
|
119
119
|
via: "cookie";
|
|
120
120
|
}, principal: VerifiedApprover, focusTask: TaskChatFocus | null, now: Date, chatProject?: string | null) => {
|
|
121
|
-
messages:
|
|
122
|
-
proposals:
|
|
121
|
+
messages: LeadMessage[];
|
|
122
|
+
proposals: LeadProposal[];
|
|
123
123
|
decisions: Map<number, Decision>;
|
|
124
124
|
coordinatorProposals: CoordinatorProposal[];
|
|
125
|
-
pending:
|
|
126
|
-
recent:
|
|
127
|
-
ask:
|
|
128
|
-
asks: Map<number,
|
|
125
|
+
pending: LeadTurn | null;
|
|
126
|
+
recent: LeadTurn[];
|
|
127
|
+
ask: LeadAsk | null;
|
|
128
|
+
asks: Map<number, LeadAsk>;
|
|
129
129
|
previous: ReplacedThread | null;
|
|
130
130
|
};
|
|
131
131
|
demoLeadHere: () => DemoLead | null;
|
|
@@ -137,7 +137,7 @@ export interface ServerRuntime {
|
|
|
137
137
|
teamChatProvider: TeamChatProviderResolver;
|
|
138
138
|
pullRequestTargetOf: (runId: number) => string | null;
|
|
139
139
|
chatEnablement: () => ChatEnablement;
|
|
140
|
-
|
|
140
|
+
startLeadConversation: (who: Who & {
|
|
141
141
|
via: "cookie";
|
|
142
142
|
}, principal: VerifiedApprover, enabled: Extract<ChatEnablement, {
|
|
143
143
|
ok: true;
|
|
@@ -185,7 +185,7 @@ export interface ServerRuntime {
|
|
|
185
185
|
status: number;
|
|
186
186
|
message: string;
|
|
187
187
|
} | null;
|
|
188
|
-
|
|
188
|
+
leadSaid: Map<string, {
|
|
189
189
|
turn: number | null;
|
|
190
190
|
message: string;
|
|
191
191
|
}>;
|
package/dist/server/settings.js
CHANGED
|
@@ -8,8 +8,8 @@ import { skillTestResult } from "../project-skills.js";
|
|
|
8
8
|
import { discoverTools, projectToolsOf } from "../project-tools.js";
|
|
9
9
|
import { publishingOf } from '../pull-request-flow.js';
|
|
10
10
|
import { skillTestFeedbackHtml } from "../skills-ui.js";
|
|
11
|
-
import { nameOf } from "../
|
|
12
|
-
import { grantTool } from "../
|
|
11
|
+
import { labelOf, nameOf } from "../subagent-admin.js";
|
|
12
|
+
import { grantTool } from "../subagent-tools.js";
|
|
13
13
|
import { learningHtml } from "../workspace-ui.js";
|
|
14
14
|
import { adapterPolicy } from "./route-policy.js";
|
|
15
15
|
import { createHash } from "node:crypto";
|
|
@@ -100,7 +100,7 @@ import { skillsHtml, skillsScript } from "../skills-ui.js";
|
|
|
100
100
|
import { checkSlackCredentials, clearSlackCredentials, loadSlackCredentials, saveSlackCredentials, SLACK_MANIFEST, SlackError } from "../slack-api.js";
|
|
101
101
|
import { slackSettingsHtml } from "../slack-settings.js";
|
|
102
102
|
import { spendCsv, spendHtml } from "../spend-ui.js";
|
|
103
|
-
import { budgetStates, monthNamed, spendItems, usd as spendUsd,
|
|
103
|
+
import { budgetStates, monthNamed, spendItems, usd as spendUsd, subagentNames as subagentNamesOf } from "../spend.js";
|
|
104
104
|
import { removeSsoSettings, saveSsoSettings, SSO_CALLBACK, ssoChangeWords } from "../sso-settings.js";
|
|
105
105
|
import { ssoSettingsHtml } from "../sso-ui.js";
|
|
106
106
|
import { lastSweep, saveSweep, storageSweepOff } from "../storage-sweep.js";
|
|
@@ -129,23 +129,23 @@ export function createSettingsHandlers(runtime) {
|
|
|
129
129
|
if (month === null)
|
|
130
130
|
return refuse(response, who, 400, "Choose a month like 2026-09.", "/spend");
|
|
131
131
|
const items = spendItems(store.handle, month.from, month.to);
|
|
132
|
-
const
|
|
133
|
-
const
|
|
132
|
+
const subagents = store.subagents([...new Set([...managedRepos(), ...store.knownRepos()])]);
|
|
133
|
+
const subagentNames = subagentNamesOf(store.handle);
|
|
134
134
|
if (url.searchParams.get("format") === "csv") {
|
|
135
135
|
response.writeHead(200, { "content-type": "text/csv; charset=utf-8", "content-disposition": `attachment; filename="standing-orders-spend-${month.name}.csv"`, "cache-control": "no-store", "x-content-type-options": "nosniff" });
|
|
136
|
-
return void response.end(spendCsv(items,
|
|
136
|
+
return void response.end(spendCsv(items, subagentNames));
|
|
137
137
|
}
|
|
138
138
|
const shift = (by) => { const at = new Date(`${month.name}-01T00:00:00.000Z`); at.setUTCMonth(at.getUTCMonth() + by); return at.toISOString().slice(0, 7); };
|
|
139
139
|
const projects = consoleProjects();
|
|
140
140
|
const people = store.accountFacts().filter(one => one.revokedAt === null).map(one => one.name);
|
|
141
141
|
const view = {
|
|
142
142
|
month: month.name, previous: shift(-1), next: month.name === current.name ? null : shift(1),
|
|
143
|
-
items, budgets: budgetStates(store.budgets(), items),
|
|
143
|
+
items, budgets: budgetStates(store.budgets(), items), subagentNames, csrf: who.via === "cookie" ? who.session.csrf : "",
|
|
144
144
|
targets: [
|
|
145
145
|
{ value: "installation:*", label: "Everything", group: "Everything" },
|
|
146
146
|
...projects.map(repo => ({ value: `project:${repo}`, label: projectName(repo), group: "Projects" })),
|
|
147
147
|
...people.map(name => ({ value: `person:${name}`, label: name, group: "People" })),
|
|
148
|
-
...
|
|
148
|
+
...subagents.map(one => ({ value: `subagent:${one.id}`, label: `${subagentNames.get(one.id) ?? one.handle} (${projectName(one.repo)})`, group: "Subagents" })),
|
|
149
149
|
],
|
|
150
150
|
};
|
|
151
151
|
return sendScreen(response, 200, screen("Spend", spendHtml(view, { said: url.searchParams.get("said"), problem: url.searchParams.get("problem") }), { chrome: chromeFor(null, "spend") }));
|
|
@@ -536,7 +536,8 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
|
|
|
536
536
|
const promises = openCommitments(store, who.name, 50).map(one => ({ id: one.id, what: one.what, when: conditionWords(store, one.condition), until: one.expiresAt }));
|
|
537
537
|
return sendScreen(response, 200, screen("Lead", leadSettingsHtml({ config, facts, words: leadWords(), signedIn, command: agentSignInCommand(),
|
|
538
538
|
said: url.searchParams.get("said"), saved: url.searchParams.get("saved") === "1", identity: leadIdentityOf(store, who.name), promises,
|
|
539
|
-
about: aboutYouOf(store, who.name), aboutSaved: url.searchParams.get("saved") === "about"
|
|
539
|
+
about: aboutYouOf(store, who.name), aboutSaved: url.searchParams.get("saved") === "about",
|
|
540
|
+
subagents: store.subagents(consoleProjects().filter(repo => store.accountCanAccess(who.name, repo))).map(one => ({ id: one.id, label: labelOf(one), project: projectName(one.repo), paused: one.state === "paused" })) }), { chrome: chromeFor(project, "settings") }));
|
|
540
541
|
}
|
|
541
542
|
async function telegramPage(ctx) {
|
|
542
543
|
const { response, now, project } = ctx;
|
|
@@ -754,16 +755,16 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
|
|
|
754
755
|
const back = (key, words) => redirect(response, `/spend?${key}=${encodeURIComponent(words)}`);
|
|
755
756
|
if (!authenticateApprover(who, body.get("password") ?? "").ok)
|
|
756
757
|
return back("problem", "Enter your Toolroll password to change a budget.");
|
|
757
|
-
const target = /^(installation|project|person|
|
|
758
|
+
const target = /^(installation|project|person|subagent):(.+)$/.exec(body.get("target") ?? "");
|
|
758
759
|
if (target === null)
|
|
759
760
|
return back("problem", "Choose what the budget is for.");
|
|
760
761
|
const scope = target[1], key = target[2];
|
|
761
762
|
const known = scope === "installation" ? key === "*"
|
|
762
763
|
: scope === "project" ? consoleProjects().includes(key)
|
|
763
764
|
: scope === "person" ? store.accountFacts().some(one => one.name === key && one.revokedAt === null)
|
|
764
|
-
: store.
|
|
765
|
+
: store.subagents([...new Set([...managedRepos(), ...store.knownRepos()])]).some(one => String(one.id) === key);
|
|
765
766
|
if (!known)
|
|
766
|
-
return back("problem", "That isn't a project, person or
|
|
767
|
+
return back("problem", "That isn't a project, person or subagent here.");
|
|
767
768
|
const label = budgetLabel({ scope, key }).replace(/'s$/, "");
|
|
768
769
|
if (body.get("action") === "remove") {
|
|
769
770
|
const existing = store.budgets().find(one => one.scope === scope && one.key === key);
|
|
@@ -2315,7 +2316,7 @@ ${modeWords(terms)
|
|
|
2315
2316
|
return done(back, "problem", finished.said);
|
|
2316
2317
|
// From a kit: its teammate may use the tool now (reading freely, the rest after a person approves each call).
|
|
2317
2318
|
const set = kit === null || !kit.tools.some(one => one.tool === service.id) ? null : kitInstalled(store, kit, visit.repo);
|
|
2318
|
-
if (set !== null && store.
|
|
2319
|
+
if (set !== null && store.subagentGrant(set.mate.id, service.id) === null) {
|
|
2319
2320
|
const granted = await grantTool(store, set.mate, service.id, visit.by, now, { toolHome });
|
|
2320
2321
|
if (granted.ok)
|
|
2321
2322
|
return done(back, "said", `${service.label} is connected to ${to}, and ${nameOf(set.mate)} can use it: reading freely, the rest after you approve each call.`);
|