toolroll 0.9.94 → 0.9.96
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 +17 -1
- package/dist/agent-fence.d.ts +6 -0
- package/dist/agent-fence.js +2 -2
- package/dist/assignment-status.d.ts +1 -1
- package/dist/assignment-ui.js +2 -0
- package/dist/attest.d.ts +3 -0
- package/dist/attest.js +10 -0
- package/dist/browser/terminal.js +1 -1
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +24 -24
- package/dist/browser-crew.js +2 -0
- package/dist/browser-workspace.d.ts +5 -0
- package/dist/build-review.js +2 -0
- package/dist/builder.d.ts +7 -2
- package/dist/builder.js +183 -79
- package/dist/card-catalog.d.ts +209 -0
- package/dist/card-catalog.js +133 -0
- package/dist/card-cli.d.ts +43 -0
- package/dist/card-cli.js +44 -0
- package/dist/card-render.d.ts +43 -0
- package/dist/card-render.js +260 -0
- package/dist/card-sources.d.ts +68 -0
- package/dist/card-sources.js +234 -0
- package/dist/chat-actions.d.ts +95 -8
- package/dist/chat-actions.js +449 -45
- package/dist/chat-approval.d.ts +13 -0
- package/dist/chat-approval.js +39 -9
- package/dist/chat-card-part.d.ts +11 -0
- package/dist/chat-card-part.js +18 -0
- package/dist/chat-card.d.ts +46 -0
- package/dist/chat-card.js +99 -0
- package/dist/chat-channel.d.ts +2 -0
- package/dist/chat-channel.js +26 -14
- package/dist/chat-controls.d.ts +4 -0
- package/dist/chat-controls.js +4 -0
- package/dist/chat-decide.d.ts +3 -14
- package/dist/chat-decide.js +43 -54
- package/dist/chat-delivery.js +13 -4
- package/dist/chat-flow.d.ts +0 -27
- package/dist/chat-flow.js +31 -2
- package/dist/chat-quiet.js +1 -1
- package/dist/chat-task-actions.d.ts +3 -2
- package/dist/chat-task-actions.js +19 -5
- package/dist/chat-terms.d.ts +14 -2
- package/dist/chat-terms.js +35 -0
- package/dist/check-failure.d.ts +2 -0
- package/dist/check-failure.js +34 -2
- package/dist/check-repair.d.ts +47 -0
- package/dist/check-repair.js +240 -0
- package/dist/claim.d.ts +6 -0
- package/dist/claim.js +13 -2
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +2 -0
- package/dist/cli.js +4 -0
- package/dist/coding-provider.d.ts +1 -1
- package/dist/coding-provider.js +9 -5
- package/dist/coding-workspace.d.ts +1 -1
- package/dist/coding-workspace.js +4 -4
- package/dist/contracts/chat-actions.d.ts +83 -0
- package/dist/contracts/chat-actions.js +18 -0
- package/dist/contracts/chat-card.d.ts +91 -0
- package/dist/contracts/chat-card.js +54 -0
- package/dist/contracts/chat-content.d.ts +191 -0
- package/dist/contracts/chat-content.js +4 -0
- package/dist/contracts/console-api.d.ts +2 -0
- package/dist/contracts/console-api.js +2 -0
- package/dist/contracts/flow-propose.d.ts +8 -1
- package/dist/contracts/flow-propose.js +3 -1
- package/dist/contracts/flow-send.d.ts +126 -0
- package/dist/contracts/flow-send.js +3 -0
- package/dist/contracts/lead-tools.d.ts +173 -0
- package/dist/contracts/lead-tools.js +15 -0
- package/dist/contracts/store-json.d.ts +16 -0
- package/dist/discord-chat.js +14 -0
- package/dist/dispatch.d.ts +1 -1
- package/dist/dispatch.js +7 -1
- package/dist/dispose.d.ts +19 -0
- package/dist/dispose.js +47 -7
- package/dist/eval-oracle.js +4 -1
- package/dist/evidence-pair.d.ts +53 -0
- package/dist/evidence-pair.js +128 -0
- package/dist/exhaustion.d.ts +17 -4
- package/dist/exhaustion.js +44 -4
- package/dist/export.js +1 -1
- package/dist/failing-first.d.ts +31 -0
- package/dist/failing-first.js +44 -0
- package/dist/fallback-schema.d.ts +8 -0
- package/dist/fallback-schema.js +40 -0
- package/dist/fallback.d.ts +138 -0
- package/dist/fallback.js +605 -0
- package/dist/findings.d.ts +224 -0
- package/dist/findings.js +449 -0
- package/dist/fleet-schema.d.ts +17 -0
- package/dist/fleet-schema.js +135 -0
- package/dist/flow-browser.js +2 -2
- package/dist/flow-code.d.ts +2 -2
- package/dist/flow-code.js +9 -5
- package/dist/flow-result-card.d.ts +22 -0
- package/dist/flow-result-card.js +55 -0
- package/dist/flow-send.js +12 -9
- package/dist/flow-steps.js +1 -1
- package/dist/flow-triggers.js +1 -1
- package/dist/flow-unattended.d.ts +26 -0
- package/dist/flow-unattended.js +12 -3
- package/dist/flows-ui.js +10 -2
- package/dist/follow-through-brief.d.ts +12 -0
- package/dist/follow-through-brief.js +31 -0
- package/dist/follow-through-cli.d.ts +23 -0
- package/dist/follow-through-cli.js +153 -0
- package/dist/git-incident.d.ts +16 -0
- package/dist/git-incident.js +69 -6
- package/dist/handover-note.d.ts +33 -0
- package/dist/handover-note.js +106 -0
- package/dist/invoke.d.ts +20 -1
- package/dist/invoke.js +108 -44
- package/dist/lead-cli.js +26 -24
- package/dist/lead-contract.js +1 -0
- package/dist/lead-doors.d.ts +4 -0
- package/dist/lead-doors.js +2 -2
- package/dist/lead-progress.js +2 -0
- package/dist/lead-tools.js +111 -32
- package/dist/lead.js +1 -1
- package/dist/local-owner.js +5 -0
- package/dist/modes.d.ts +6 -6
- package/dist/modes.js +3 -1
- package/dist/needs-you.d.ts +5 -2
- package/dist/needs-you.js +14 -2
- package/dist/operate-remote-arguments.js +1 -0
- package/dist/operate-remote.js +1 -1
- package/dist/operate.d.ts +1 -1
- package/dist/operate.js +165 -14
- package/dist/plan-login.d.ts +14 -0
- package/dist/plan-login.js +58 -0
- package/dist/planner.d.ts +1 -1
- package/dist/planner.js +2 -1
- package/dist/project-delete-ui.d.ts +12 -0
- package/dist/project-delete-ui.js +18 -1
- package/dist/project-delete.js +6 -0
- package/dist/project-knowledge.d.ts +6 -6
- package/dist/project-tools.d.ts +4 -4
- package/dist/project-tools.js +23 -7
- package/dist/protected-storage-look.d.ts +1 -0
- package/dist/protected-storage-look.js +13 -0
- package/dist/protected-storage.d.ts +70 -39
- package/dist/protected-storage.js +196 -147
- package/dist/provider-capture.d.ts +34 -0
- package/dist/provider-capture.js +53 -0
- package/dist/release-carry.js +10 -1
- package/dist/release.js +9 -1
- package/dist/scope.d.ts +22 -3
- package/dist/scope.js +71 -20
- package/dist/scout.d.ts +1 -1
- package/dist/scout.js +2 -1
- package/dist/serve.js +2 -0
- package/dist/server/chat.js +2 -2
- package/dist/server/console.css +50 -0
- package/dist/server/render-chat.js +14 -1
- package/dist/server/render-follow-through.d.ts +11 -0
- package/dist/server/render-follow-through.js +68 -0
- package/dist/server/render-results.js +1 -1
- package/dist/server/render-tasks.d.ts +3 -0
- package/dist/server/render-tasks.js +24 -5
- package/dist/server/route-table.js +3 -0
- package/dist/server/settings.js +37 -12
- package/dist/server/task-views.js +5 -0
- package/dist/server/tasks.js +76 -0
- package/dist/settings-groups-ui.d.ts +1 -0
- package/dist/settings-groups-ui.js +3 -1
- package/dist/settings-kit.js +3 -2
- package/dist/settings-proofs.js +3 -1
- package/dist/setup-guide.d.ts +2 -2
- package/dist/slack-chat.js +14 -1
- package/dist/steer-mailbox.d.ts +8 -1
- package/dist/steer-mailbox.js +164 -6
- package/dist/store.d.ts +11 -4
- package/dist/store.js +93 -30
- package/dist/surface.js +24 -4
- package/dist/task-control.d.ts +2 -1
- package/dist/task-status.js +5 -0
- package/dist/teardown-git.d.ts +2 -0
- package/dist/teardown-git.js +75 -19
- package/dist/telegram-lead.js +23 -6
- package/dist/telegram.js +12 -1
- package/dist/toolroll-update.js +2 -0
- package/dist/work-index.js +22 -7
- package/dist/work-summary.d.ts +1 -1
- package/dist/work-summary.js +5 -2
- package/dist/workspace-ui.d.ts +3 -1
- package/dist/workspace-ui.js +10 -1
- package/dist/worktree.js +23 -1
- package/package.json +2 -1
package/dist/scope.d.ts
CHANGED
|
@@ -205,6 +205,7 @@ export declare function canonicalProfileJson(profile: ExecutionProfile): string;
|
|
|
205
205
|
* same 128 bits every other safety digest here uses (finding 21). */
|
|
206
206
|
export declare function profileDigestOf(profile: ExecutionProfile): string;
|
|
207
207
|
export declare const CHAIN_DIGEST_VERSION = 1;
|
|
208
|
+
export declare const LIVE_CHAIN_VERSION = 2;
|
|
208
209
|
/** One entry of an approved fallback chain: the WHOLE execution profile
|
|
209
210
|
* (so a claude->codex switch cannot inherit one provider's repair config)
|
|
210
211
|
* and the auth mode it runs under. Order is authority. */
|
|
@@ -214,15 +215,15 @@ export type ChainEntry = {
|
|
|
214
215
|
};
|
|
215
216
|
/** The stored snapshot bytes for a chain: the version rides IN the
|
|
216
217
|
* snapshot, exactly like a single profile's. */
|
|
217
|
-
export declare function canonicalChainJson(entries: readonly ChainEntry[]): string;
|
|
218
|
+
export declare function canonicalChainJson(entries: readonly ChainEntry[], version?: number): string;
|
|
218
219
|
/** sha256 over a domain-separated canonical chain encoding — a DIFFERENT
|
|
219
220
|
* domain from a single profile ("standing-orders:chain:" vs
|
|
220
221
|
* ":profile:"), so a chain digest can never collide with a profile
|
|
221
222
|
* digest, truncated to the same 128 bits. */
|
|
222
|
-
export declare function chainDigestOf(entries: readonly ChainEntry[]): string;
|
|
223
|
+
export declare function chainDigestOf(entries: readonly ChainEntry[], version?: number): string;
|
|
223
224
|
/** Strict re-hydration of a stored chain — every entry proved through the
|
|
224
225
|
* single-profile rehydrator; anything unexpected is null. */
|
|
225
|
-
export declare function chainFromJson(json: string | null): ChainEntry[] | null;
|
|
226
|
+
export declare function chainFromJson(json: string | null, version?: number): ChainEntry[] | null;
|
|
226
227
|
/**
|
|
227
228
|
* THE STRICT RAW TERMS READ (raw authority repair): the stored columns of
|
|
228
229
|
* a scope or a standing order, proved EXACTLY — a JSON list is a list of
|
|
@@ -499,6 +500,7 @@ export declare function digestOf(scope: Pick<Scope, "goal" | "outOfScope" | "tou
|
|
|
499
500
|
releaseCheck?: ReleaseCheckPurpose | null;
|
|
500
501
|
}, target?: ExecutionProfile | null | {
|
|
501
502
|
chain: readonly ChainEntry[];
|
|
503
|
+
version?: number;
|
|
502
504
|
}, route?: PhaseRoute | null): string;
|
|
503
505
|
export declare function propose(store: Store, input: ScopeInput): Scope;
|
|
504
506
|
export type GuardedProposeResult = {
|
|
@@ -798,6 +800,17 @@ export declare function approve(store: Store, taskId: string, by: string, now: D
|
|
|
798
800
|
fastStart?: {
|
|
799
801
|
rule: string;
|
|
800
802
|
at: string;
|
|
803
|
+
},
|
|
804
|
+
/** A person's own yes in chat (owner's decision, docs/plans/toolroll-cards.md): their tap or signed-in click stands in
|
|
805
|
+
* for a password, so `token` is unused. Every rule below still applies. The caller has proved the person is the one
|
|
806
|
+
* tapping. `shown` is what a card that showed its terms whole showed: those words, and the digest kept when it was
|
|
807
|
+
* made. Null when the card showed less: the seal then opens none of the walls a person's reading opens. */
|
|
808
|
+
chat?: {
|
|
809
|
+
modeDigest: string;
|
|
810
|
+
shown: {
|
|
811
|
+
expect: string;
|
|
812
|
+
terms: string;
|
|
813
|
+
} | null;
|
|
801
814
|
}): ApproveResult;
|
|
802
815
|
/** The ledger act that records how an approval's person was proved (password-now, sso-now, fresh-password, fresh-sso). */
|
|
803
816
|
export declare const APPROVAL_PROOF = "approval proved";
|
|
@@ -840,6 +853,7 @@ export type ScopeAuthority = {
|
|
|
840
853
|
route: PhaseRoute;
|
|
841
854
|
digest: string;
|
|
842
855
|
authMode: AuthMode | null;
|
|
856
|
+
chain?: ChainEntry[];
|
|
843
857
|
} | {
|
|
844
858
|
ok: false;
|
|
845
859
|
reason: "terms" | "unrouted" | "unresolved" | "profile" | "chain" | "route" | "parity" | "digest" | "auth-mode";
|
|
@@ -1036,3 +1050,8 @@ export declare function localApprovalEligible(store: Store, by: string, taskId:
|
|
|
1036
1050
|
reason: LocalApprovalRefusal;
|
|
1037
1051
|
message: string;
|
|
1038
1052
|
};
|
|
1053
|
+
/** A chain that may dispatch: version 2, a base and at least one fallback. An older chain is history, never this. */
|
|
1054
|
+
export declare function liveChainFromJson(json: string | null): ChainEntry[] | null;
|
|
1055
|
+
/** The chain in full, the words a yes agrees to: order, agent, model, how it bills, what it may do, and whether Toolroll
|
|
1056
|
+
* can see it run out. Every approval surface shows these lines; empty for a scope with no chain. */
|
|
1057
|
+
export declare function chainTermsLines(scope: Pick<Scope, "proposedChainJson">): string[];
|
package/dist/scope.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// The rubric's vocabulary loads before anything that reaches the plan contract (see contracts/acceptance-terms.ts).
|
|
2
2
|
export { ACCEPTANCE_LIMITS, EVIDENCE_KINDS } from "./contracts/acceptance-terms.js";
|
|
3
|
+
import { cardShowsScope, cardTermsDigest } from "./chat-terms.js";
|
|
3
4
|
import { mintSecret } from "./secret-mint.js";
|
|
4
5
|
import { passwordlessOn } from "./passwordless.js";
|
|
5
6
|
import { routeChangeProblem } from "./route-change-check.js";
|
|
@@ -154,29 +155,30 @@ export function profileDigestOf(profile) {
|
|
|
154
155
|
.digest("hex")
|
|
155
156
|
.slice(0, 32);
|
|
156
157
|
}
|
|
157
|
-
// ---- fallback chains (
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
//
|
|
158
|
+
// ---- fallback chains (fallback.ts, docs/plans/fallback-chains.md) ----------
|
|
159
|
+
// Version 1 is history (v30-v114): an old approval still decodes and its digest
|
|
160
|
+
// still re-derives, so old tasks verify and render, and it holds no authority.
|
|
161
|
+
// Version 2 is what filing writes now: only it dispatches.
|
|
161
162
|
export const CHAIN_DIGEST_VERSION = 1;
|
|
163
|
+
export const LIVE_CHAIN_VERSION = 2;
|
|
162
164
|
/** The stored snapshot bytes for a chain: the version rides IN the
|
|
163
165
|
* snapshot, exactly like a single profile's. */
|
|
164
|
-
export function canonicalChainJson(entries) {
|
|
165
|
-
return canonicalJson({ digestVersion:
|
|
166
|
+
export function canonicalChainJson(entries, version = CHAIN_DIGEST_VERSION) {
|
|
167
|
+
return canonicalJson({ digestVersion: version, chain: entries });
|
|
166
168
|
}
|
|
167
169
|
/** sha256 over a domain-separated canonical chain encoding — a DIFFERENT
|
|
168
170
|
* domain from a single profile ("standing-orders:chain:" vs
|
|
169
171
|
* ":profile:"), so a chain digest can never collide with a profile
|
|
170
172
|
* digest, truncated to the same 128 bits. */
|
|
171
|
-
export function chainDigestOf(entries) {
|
|
173
|
+
export function chainDigestOf(entries, version = CHAIN_DIGEST_VERSION) {
|
|
172
174
|
return createHash("sha256")
|
|
173
|
-
.update(`standing-orders:chain:v${
|
|
175
|
+
.update(`standing-orders:chain:v${version}:${canonicalChainJson(entries, version)}`, "utf8")
|
|
174
176
|
.digest("hex")
|
|
175
177
|
.slice(0, 32);
|
|
176
178
|
}
|
|
177
179
|
/** Strict re-hydration of a stored chain — every entry proved through the
|
|
178
180
|
* single-profile rehydrator; anything unexpected is null. */
|
|
179
|
-
export function chainFromJson(json) {
|
|
181
|
+
export function chainFromJson(json, version = CHAIN_DIGEST_VERSION) {
|
|
180
182
|
if (json === null)
|
|
181
183
|
return null;
|
|
182
184
|
let parsed;
|
|
@@ -189,7 +191,7 @@ export function chainFromJson(json) {
|
|
|
189
191
|
if (parsed === null || typeof parsed !== "object")
|
|
190
192
|
return null;
|
|
191
193
|
const wrapper = parsed;
|
|
192
|
-
if (wrapper.digestVersion !==
|
|
194
|
+
if (wrapper.digestVersion !== version)
|
|
193
195
|
return null;
|
|
194
196
|
// Exact keys on the wrapper and on every entry (v48 integrity): a
|
|
195
197
|
// snapshot carrying anything this code never writes is not one it wrote.
|
|
@@ -602,7 +604,7 @@ route) {
|
|
|
602
604
|
const innerDigest = target == null
|
|
603
605
|
? null
|
|
604
606
|
: "chain" in target
|
|
605
|
-
? chainDigestOf(target.chain)
|
|
607
|
+
? chainDigestOf(target.chain, target.version)
|
|
606
608
|
: profileDigestOf(target);
|
|
607
609
|
return createHash("sha256")
|
|
608
610
|
.update(JSON.stringify({
|
|
@@ -1074,7 +1076,12 @@ export function fileAndSealUnderMode(store, input) {
|
|
|
1074
1076
|
}
|
|
1075
1077
|
export function approve(store, taskId, by, now, sawDigest, token, mutation = {},
|
|
1076
1078
|
/** A fast start (fast-start.ts): Build is this same yes, with its marker in the proof. Only that road passes it. */
|
|
1077
|
-
fastStart
|
|
1079
|
+
fastStart,
|
|
1080
|
+
/** A person's own yes in chat (owner's decision, docs/plans/toolroll-cards.md): their tap or signed-in click stands in
|
|
1081
|
+
* for a password, so `token` is unused. Every rule below still applies. The caller has proved the person is the one
|
|
1082
|
+
* tapping. `shown` is what a card that showed its terms whole showed: those words, and the digest kept when it was
|
|
1083
|
+
* made. Null when the card showed less: the seal then opens none of the walls a person's reading opens. */
|
|
1084
|
+
chat) {
|
|
1078
1085
|
// Read and write in one transaction, re-reading inside it — the credential
|
|
1079
1086
|
// check included, so a token rotated between authentication and the write
|
|
1080
1087
|
// cannot leave an approval signed by an authority that no longer exists.
|
|
@@ -1082,7 +1089,14 @@ fastStart) {
|
|
|
1082
1089
|
// this approval — which would resurrect the old wording *and* mark it
|
|
1083
1090
|
// agreed, the precise opposite of what the digest is for.
|
|
1084
1091
|
return store.transact(() => {
|
|
1085
|
-
const
|
|
1092
|
+
const repoNow = store.lookupRef(taskId)?.repo ?? null;
|
|
1093
|
+
const account = chat === undefined ? null : store.accountOf(by);
|
|
1094
|
+
// A machine never reaches this road, and the person's role and reach are read here, in the seal's transaction.
|
|
1095
|
+
if (chat !== undefined && machineActing())
|
|
1096
|
+
return { ok: false, reason: "machine-proof" };
|
|
1097
|
+
if (chat !== undefined && (account === null || account.revokedAt !== null || account.role !== "approver" || repoNow === null || !store.accountCanAccess(by, repoNow)))
|
|
1098
|
+
return { ok: false, reason: "not-an-approver" };
|
|
1099
|
+
const authenticated = chat !== undefined ? { ok: true, method: "chat", at: now.getTime() } : authenticateApprover(store, by, token, repoNow);
|
|
1086
1100
|
if (!authenticated.ok)
|
|
1087
1101
|
return authenticated;
|
|
1088
1102
|
// ONLY A PERSON APPROVES (docs/plans/approval-policy.md, phase 1): a lead token or a remote API token proves the
|
|
@@ -1110,6 +1124,10 @@ fastStart) {
|
|
|
1110
1124
|
// the new one in silence — which is the whole failure this guards.
|
|
1111
1125
|
if (sawDigest !== scope.digest)
|
|
1112
1126
|
return { ok: false, reason: "changed" };
|
|
1127
|
+
// What the card showed when it was made is what this yes is to: other words, or words leaving out a widening, aren't.
|
|
1128
|
+
const shown = chat?.shown ?? null;
|
|
1129
|
+
if (shown !== null && (cardTermsDigest(shown.terms) !== shown.expect || !cardShowsScope(scope, shown.terms)))
|
|
1130
|
+
return { ok: false, reason: "changed" };
|
|
1113
1131
|
// The "what changed" line a person read describes this exact route, or this yes isn't to what they read (route-change.ts).
|
|
1114
1132
|
if (routeChangeProblem(store, taskId, scope))
|
|
1115
1133
|
return { ok: false, reason: "changed" };
|
|
@@ -1174,7 +1192,8 @@ fastStart) {
|
|
|
1174
1192
|
// profile — the exact bytes the digest the approver signed was bound
|
|
1175
1193
|
// to. Routing saveScope here would re-run resolution and could sign a
|
|
1176
1194
|
// profile nobody saw.
|
|
1177
|
-
const sealed = store.sealScopeApproval(taskId, by, now, mutation)
|
|
1195
|
+
const sealed = chat === undefined ? store.sealScopeApproval(taskId, by, now, mutation)
|
|
1196
|
+
: store.sealScopeApproval(taskId, by, now, mutation, { kind: "chat", modeDigest: chat.modeDigest, ...(shown === null ? {} : { card: "shown", cardDigest: shown.expect, shownTerms: shown.terms }) }, "person");
|
|
1178
1197
|
const approved = sealed ? store.getScope(taskId) : null;
|
|
1179
1198
|
if (!sealed || approved === null) {
|
|
1180
1199
|
refused.result = { ok: false, reason: sealed ? "no-scope" : "changed" };
|
|
@@ -1294,10 +1313,11 @@ export function scopeAuthorityOf(scope, env = {}) {
|
|
|
1294
1313
|
if (profile === null) {
|
|
1295
1314
|
return { ok: false, reason: "profile", problem: profileJson === null ? "the scope carries no agent profile" : "the scope's agent profile cannot be read exactly (unknown keys, an unsafe number, or a clock no timer can hold)" };
|
|
1296
1315
|
}
|
|
1297
|
-
//
|
|
1298
|
-
//
|
|
1299
|
-
|
|
1300
|
-
|
|
1316
|
+
// Fallback agents: a current chain whose base IS this profile. An older
|
|
1317
|
+
// chain (v30-v114) holds no authority: saving the scope again re-files it.
|
|
1318
|
+
const chain = scope.proposedChainJson == null ? null : liveChainFromJson(scope.proposedChainJson);
|
|
1319
|
+
if (scope.proposedChainJson != null && (chain === null || profileDigestOf(chain[0].profile) !== profileDigestOf(profile))) {
|
|
1320
|
+
return { ok: false, reason: "chain", problem: "this scope named fallback agents from an older version, or they can't be read — save the scope again to choose its agents" };
|
|
1301
1321
|
}
|
|
1302
1322
|
const routeJson = scope.proposedRouteJson ?? null;
|
|
1303
1323
|
const route = routeFromJson(routeJson);
|
|
@@ -1307,7 +1327,7 @@ export function scopeAuthorityOf(scope, env = {}) {
|
|
|
1307
1327
|
const parity = routeParityProblem(route, profile, { riskLevel: scope.riskLevel ?? "routine", qualityMode: scope.qualityMode ?? "default" });
|
|
1308
1328
|
if (parity !== null)
|
|
1309
1329
|
return { ok: false, reason: "parity", problem: parity };
|
|
1310
|
-
const digest = digestOf({ goal: scope.goal, outOfScope: scope.outOfScope, touches: scope.touches, budgetMicrousd: scope.budgetMicrousd, acceptance: scope.acceptance, candidate: scope.candidate ?? null, qualityMode: scope.qualityMode ?? "default", releaseTerms: scope.releaseTerms ?? null, releaseCheck: scope.releaseCheck ?? null }, profile, route);
|
|
1330
|
+
const digest = digestOf({ goal: scope.goal, outOfScope: scope.outOfScope, touches: scope.touches, budgetMicrousd: scope.budgetMicrousd, acceptance: scope.acceptance, candidate: scope.candidate ?? null, qualityMode: scope.qualityMode ?? "default", releaseTerms: scope.releaseTerms ?? null, releaseCheck: scope.releaseCheck ?? null }, chain === null ? profile : { chain, version: LIVE_CHAIN_VERSION }, route);
|
|
1311
1331
|
if (digest !== scope.digest) {
|
|
1312
1332
|
return { ok: false, reason: "digest", problem: "the scope's reference does not re-derive from its own terms, agents, and route — file it again" };
|
|
1313
1333
|
}
|
|
@@ -1323,8 +1343,11 @@ export function scopeAuthorityOf(scope, env = {}) {
|
|
|
1323
1343
|
if (!read.ok)
|
|
1324
1344
|
return { ok: false, reason: "auth-mode", problem: read.problem };
|
|
1325
1345
|
authMode = read.mode;
|
|
1346
|
+
// The base was filed under one credential; a yes on another isn't a yes to these terms.
|
|
1347
|
+
if (chain !== null && chain[0].authMode !== authMode)
|
|
1348
|
+
return { ok: false, reason: "auth-mode", problem: "the agent's sign-in changed since this was filed — save the scope again" };
|
|
1326
1349
|
}
|
|
1327
|
-
return { ok: true, profile, route, digest, authMode };
|
|
1350
|
+
return { ok: true, profile, route, digest, authMode, ...(chain === null ? {} : { chain }) };
|
|
1328
1351
|
}
|
|
1329
1352
|
/** The rubric's approval-card lines (v39): one per criterion, the id in
|
|
1330
1353
|
* front (the exact string a proof must answer by) and its required
|
|
@@ -1363,6 +1386,7 @@ evidence = null) {
|
|
|
1363
1386
|
// leg with its reason and readiness — before anyone signs.
|
|
1364
1387
|
...scopeRouteWords(scope, readiness),
|
|
1365
1388
|
...(evidence === null ? [] : [` why model ${evidence.replace(/^Why this model: /, "")}`]),
|
|
1389
|
+
...chainTermsLines(scope).map((line, at) => at === 0 ? ` fallback ${line}` : ` ${line}`),
|
|
1366
1390
|
// What the build may reach (egress.ts), part of what the yes agrees to.
|
|
1367
1391
|
...(scope.profile === null || scope.profile === undefined ? [] : [` network ${networkWords(scope.profile.network ?? null).replace(/^Network: /, "")}`]),
|
|
1368
1392
|
...(scope.profile === null || scope.profile === undefined ? [] : [` writes ${writesWords(scope.profile.writes)}`]),
|
|
@@ -1414,6 +1438,9 @@ function describeApproval(approval) {
|
|
|
1414
1438
|
// ---- Only a person approves (docs/plans/approval-policy.md, phase 1). Kept last: the freshness tripwire pins places.
|
|
1415
1439
|
export { personProof } from "./person-proof.js";
|
|
1416
1440
|
import { personProof } from "./person-proof.js";
|
|
1441
|
+
import { permissionPlainWords } from "./chat-terms.js";
|
|
1442
|
+
import { limitRecognised } from "./exhaustion.js";
|
|
1443
|
+
import { SUBSCRIPTION_CAPABLE } from "./keys.js";
|
|
1417
1444
|
import { releaseCheckPurposeDigest, releaseTermsDigest } from "./contracts/release-terms.js";
|
|
1418
1445
|
import { CHECK_ACCESS_WIDTH, autoEvidenceOf, carriesUnsandboxed, newestReleaseGeneration, releaseAuthorityOfTask, releaseAutoApproves, releaseSealProblem } from "./release-authority.js";
|
|
1419
1446
|
/** Whether the command running now is a machine's: the lead (its token) or a remote API run. Trusted context only. */
|
|
@@ -1703,3 +1730,27 @@ export function localApprovalEligible(store, by, taskId) {
|
|
|
1703
1730
|
return no("unsandboxed");
|
|
1704
1731
|
return { ok: true };
|
|
1705
1732
|
}
|
|
1733
|
+
// ---- Fallback chains (fallback.ts). Kept last too.
|
|
1734
|
+
/** A chain that may dispatch: version 2, a base and at least one fallback. An older chain is history, never this. */
|
|
1735
|
+
export function liveChainFromJson(json) {
|
|
1736
|
+
const chain = chainFromJson(json, LIVE_CHAIN_VERSION);
|
|
1737
|
+
return chain === null || chain.length < 2 ? null : chain;
|
|
1738
|
+
}
|
|
1739
|
+
// The chain's four providers by name, here rather than provider-auth.ts: this file sits under the contracts, and that one
|
|
1740
|
+
// loads every adapter.
|
|
1741
|
+
const CHAIN_AGENT_NAMES = { claude: "Claude", codex: "Codex", gemini: "Gemini", openrouter: "OpenRouter" };
|
|
1742
|
+
/** The chain in full, the words a yes agrees to: order, agent, model, how it bills, what it may do, and whether Toolroll
|
|
1743
|
+
* can see it run out. Every approval surface shows these lines; empty for a scope with no chain. */
|
|
1744
|
+
export function chainTermsLines(scope) {
|
|
1745
|
+
const chain = liveChainFromJson(scope.proposedChainJson ?? null);
|
|
1746
|
+
if (chain === null)
|
|
1747
|
+
return [];
|
|
1748
|
+
return [
|
|
1749
|
+
"If an agent runs out of its plan, the work moves to the next one here:",
|
|
1750
|
+
...chain.map((entry, at) => {
|
|
1751
|
+
const paid = entry.authMode === "api-key" || SUBSCRIPTION_CAPABLE[entry.profile.provider] !== true;
|
|
1752
|
+
const last = at === chain.length - 1;
|
|
1753
|
+
return `${at + 1}. ${CHAIN_AGENT_NAMES[entry.profile.provider] ?? entry.profile.provider} ${entry.profile.model}, ${paid ? "on an API key, which spends money, and only while the project allows paid fallback" : "its plan"}; ${permissionPlainWords(entry.profile) ?? "agent permissions not known"}. ${last ? "Last in the chain." : limitRecognised(entry.profile.provider, entry.authMode) ? "Moves on when its plan runs out." : "Won't move on: Toolroll can't yet recognise this agent running out."}`;
|
|
1754
|
+
}),
|
|
1755
|
+
];
|
|
1756
|
+
}
|
package/dist/scout.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { ProviderId } from "./contracts/provider.js";
|
|
|
2
2
|
import type { Store } from "./store.js";
|
|
3
3
|
import { type ParsedDecision, type Problem } from "./decision.js";
|
|
4
4
|
import { type ParsedReport, type ReportProblem } from "./scout-report.js";
|
|
5
|
-
import type
|
|
5
|
+
import { type Runner } from "./builder.js";
|
|
6
6
|
import * as browserCheck from "./scout-browser.js";
|
|
7
7
|
/** The scout's own headless browser: the pinned Playwright tool, saving its
|
|
8
8
|
* screenshots straight into the run's image folder. */
|
package/dist/scout.js
CHANGED
|
@@ -27,6 +27,7 @@ import { scoutHandbackSchema } from "./contracts/scout-report.js";
|
|
|
27
27
|
import { parseReport, REPORT_IMAGE_FILE, REPORT_LIMITS, REPORT_VERSION, SCOUT_OUTPUT_JSON_SCHEMA } from "./scout-report.js";
|
|
28
28
|
import { TOKEN_ENVS as TELEGRAM_TOKEN_ENVS } from "./telegram.js";
|
|
29
29
|
import { evidenceRoot, mailboxName, quarantineMailboxes, readMailbox, reportFileName, SCREENSHOT_BYTE_CAP, storeEvidence, validateScreenshotBytes, writeEvidenceFile, } from "./evidence.js";
|
|
30
|
+
import { gitFailureWords } from "./builder.js";
|
|
30
31
|
import { MARKER as LEASE_MARKER } from "./worktree.js";
|
|
31
32
|
import { liveLogContext } from "./live-detail.js";
|
|
32
33
|
import { openLiveLog } from "./live.js";
|
|
@@ -251,7 +252,7 @@ async function scoutWith(store, request, outputDir) {
|
|
|
251
252
|
}
|
|
252
253
|
const revision = await git(GIT, ["--no-optional-locks", "rev-parse", "HEAD"], { cwd: worktree });
|
|
253
254
|
if (revision.code !== 0) {
|
|
254
|
-
return { ok: false, kind: "failure", reason: "git", message: `could not read the base revision in ${worktree}` };
|
|
255
|
+
return { ok: false, kind: "failure", reason: "git", message: `could not read the base revision in ${worktree} (${gitFailureWords(revision)})` };
|
|
255
256
|
}
|
|
256
257
|
const baseRevision = revision.stdout.trim();
|
|
257
258
|
store.stampRun(request.runId, { baseRevision });
|
package/dist/serve.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { secretFolder } from "./tool-secret-store.js";
|
|
2
|
+
import { fallbackSettingOf } from "./fallback.js";
|
|
2
3
|
import { acceptedIdentities, recoverLearning } from "./project-learning.js";
|
|
3
4
|
import { consoleFolderGate, syncProjectIdentities } from "./project-identity.js";
|
|
4
5
|
import { inFolderScope } from "./folder-gate.js";
|
|
@@ -507,6 +508,7 @@ export function createDecisionServer(options) {
|
|
|
507
508
|
builds: projectBuildsOf(repo, who),
|
|
508
509
|
access: projectAccessOf(repo),
|
|
509
510
|
network: { setting: store.projectNetwork(repo), canChange: who.via === "cookie" && who.role === "approver" && !store.isDemo() && store.accountCanAccess(who.name, repo) },
|
|
511
|
+
fallback: { setting: fallbackSettingOf(store, repo), canChange: who.via === "cookie" && who.role === "approver" && !store.isDemo() && store.accountCanAccess(who.name, repo) },
|
|
510
512
|
writes: { setting: store.projectWrites(repo), canChange: who.via === "cookie" && who.role === "approver" && !store.isDemo() && store.accountCanAccess(who.name, repo) },
|
|
511
513
|
sandbox: { setting: store.projectSandbox(repo), canChange: who.via === "cookie" && who.role === "approver" && !store.isDemo() && store.accountCanAccess(who.name, repo) },
|
|
512
514
|
checkAccess: {
|
package/dist/server/chat.js
CHANGED
|
@@ -790,7 +790,7 @@ export function createChatHandlers(runtime) {
|
|
|
790
790
|
}
|
|
791
791
|
// A scope approval's re-check is spent only if the whole confirmation commits (one transaction: the proposal's
|
|
792
792
|
// settlement included). Any other action spends it whatever comes of it (its effects aren't all in the database).
|
|
793
|
-
const confirmIt = () => confirmLeadProposal(store, principal, id, now, { authority: who.authority, chatProvider: teamChatProvider, confirm: body.get("confirm") === "yes", via: "web", evidenceRoot, ...(body.has("nonce") ? { actionReview: { nonce: body.get("nonce") ?? "", password: body.get("token") ?? "" } } : {}) });
|
|
793
|
+
const confirmIt = () => confirmLeadProposal(store, principal, id, now, { authority: who.authority, chatProvider: teamChatProvider, confirm: body.get("confirm") === "yes", via: "web", person: who.via === "cookie", evidenceRoot, ...(body.has("nonce") ? { actionReview: { nonce: body.get("nonce") ?? "", password: body.get("token") ?? "" } } : {}) });
|
|
794
794
|
const proposed = store.getLeadProposal(id);
|
|
795
795
|
const approving = proposed?.kind === "action" && sharedActionPayload(proposed.payload)?.operation === "scope_approve";
|
|
796
796
|
const outcome = approving ? spendingOnlyOnSuccess(confirmIt, result => !result.ok || ("already" in result && result.already === true)) : confirmIt();
|
|
@@ -832,7 +832,7 @@ export function createChatHandlers(runtime) {
|
|
|
832
832
|
}
|
|
833
833
|
if (outcome.ok && outcome.kind === "task_action" && outcome.taskId !== null) {
|
|
834
834
|
const proposal = store.getLeadProposal(id);
|
|
835
|
-
if (proposal.payload["operation"] === "resume") {
|
|
835
|
+
if (proposal.payload["operation"] === "resume" && who.via !== "cookie") {
|
|
836
836
|
return armTaskResume(response, who, outcome.taskId, String(proposal.payload["run"]), "chat", now);
|
|
837
837
|
}
|
|
838
838
|
return redirect(response, `${proposalTaskHref(outcome.taskId)}#task-chat-live`);
|
package/dist/server/console.css
CHANGED
|
@@ -2816,3 +2816,53 @@ html.so-swap::view-transition-old(*):only-child { animation: so-card-out 160ms e
|
|
|
2816
2816
|
.flaky-rows .k-row-a { grid-column: 4; grid-row: 1 / span 2; }
|
|
2817
2817
|
.flaky-rows .k-row-c { grid-row: 1 / span 2; }
|
|
2818
2818
|
}
|
|
2819
|
+
|
|
2820
|
+
/* A Toolroll card (card-render.ts): tables, charts and key facts in chat, on the console's own tokens. */
|
|
2821
|
+
.tc-card { display: grid; gap: var(--so-space-sm); min-width: 0; font-size: 13px; line-height: 1.45; color: var(--so-ink); }
|
|
2822
|
+
.tc-part { display: grid; gap: 6px; min-width: 0; }
|
|
2823
|
+
.tc-part h4 { margin: 0; font-size: 12.5px; font-weight: 600; }
|
|
2824
|
+
.tc-text { margin: 0; }
|
|
2825
|
+
/* The lead's own words in a card: set apart from the records, never styled as one. */
|
|
2826
|
+
.tc-lead { display: grid; gap: 2px; padding-left: var(--so-space-sm); border-left: 2px solid var(--so-line); font-style: italic; }
|
|
2827
|
+
.tc-lead .tc-note { font-style: normal; }
|
|
2828
|
+
.tc-note { margin: 0; font-size: 11.5px; color: var(--so-muted); }
|
|
2829
|
+
.tc-rule { width: 100%; margin: 0; border: 0; border-top: 1px solid var(--so-line); }
|
|
2830
|
+
.tc-facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(96px, 1fr)); gap: var(--so-space-sm); margin: 0; }
|
|
2831
|
+
.tc-facts > div { display: grid; gap: 1px; min-width: 0; }
|
|
2832
|
+
.tc-facts dt { font-size: 11.5px; color: var(--so-muted); }
|
|
2833
|
+
.tc-facts dd { margin: 0; font-weight: 600; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
|
|
2834
|
+
.tc-scroll { max-width: 100%; overflow-x: auto; }
|
|
2835
|
+
.tc-table { width: 100%; border-collapse: collapse; }
|
|
2836
|
+
.tc-table th { font-size: 11.5px; font-weight: 500; color: var(--so-muted); text-align: left; white-space: nowrap; }
|
|
2837
|
+
.tc-table th, .tc-table td { padding: 5px var(--so-space-md) 5px 0; border-bottom: 1px solid var(--so-line); vertical-align: top; }
|
|
2838
|
+
/* Cells break between words, never inside one: a narrow screen scrolls the table (.tc-scroll) instead. */
|
|
2839
|
+
.tc-table td { overflow-wrap: break-word; }
|
|
2840
|
+
.tc-table tr:last-child td { border-bottom: 0; }
|
|
2841
|
+
.tc-table .tc-num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
2842
|
+
.tc-bars { display: grid; gap: 6px; margin: 0; padding: 0; list-style: none; }
|
|
2843
|
+
.tc-bars li { display: grid; grid-template-columns: minmax(0, 8rem) minmax(0, 1fr) auto; gap: var(--so-space-sm); align-items: center; }
|
|
2844
|
+
.tc-bar-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2845
|
+
.tc-bar-track { height: 8px; border-radius: 4px; background: var(--so-raised); }
|
|
2846
|
+
.tc-bar-fill { display: block; height: 100%; border-radius: 4px; background: var(--so-ink); }
|
|
2847
|
+
.tc-bar-value { font-variant-numeric: tabular-nums; }
|
|
2848
|
+
.tc-pie { display: flex; flex-wrap: wrap; align-items: center; gap: var(--so-space-md); }
|
|
2849
|
+
.tc-pie svg { flex: none; width: 88px; height: 88px; }
|
|
2850
|
+
.tc-legend { display: grid; flex: 1 1 140px; gap: 3px; margin: 0; padding: 0; list-style: none; }
|
|
2851
|
+
.tc-legend li { display: flex; align-items: center; gap: 6px; min-width: 0; }
|
|
2852
|
+
.tc-legend strong { margin-left: auto; font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
2853
|
+
.tc-key { flex: none; width: 9px; height: 9px; border-radius: 2px; background: currentColor; }
|
|
2854
|
+
.tc-c0 { color: var(--so-ink); stroke: var(--so-ink); }
|
|
2855
|
+
.tc-c1 { color: var(--so-info); stroke: var(--so-info); }
|
|
2856
|
+
.tc-c2 { color: var(--so-success); stroke: var(--so-success); }
|
|
2857
|
+
.tc-c3 { color: var(--so-warning); stroke: var(--so-warning); }
|
|
2858
|
+
.tc-c4 { color: var(--so-danger); stroke: var(--so-danger); }
|
|
2859
|
+
.tc-c5 { color: var(--so-muted); stroke: var(--so-muted); }
|
|
2860
|
+
.tc-line { margin: 0; display: grid; gap: var(--so-space-xs); }
|
|
2861
|
+
.tc-line svg { width: 100%; height: 90px; }
|
|
2862
|
+
.tc-line figcaption { font-size: 11.5px; color: var(--so-muted); }
|
|
2863
|
+
.tc-stroke { stroke: var(--so-ink); stroke-width: 1.5; }
|
|
2864
|
+
.tc-area { fill: var(--so-raised); }
|
|
2865
|
+
.tc-notes { display: grid; gap: var(--so-space-xs); margin: 0; padding-left: var(--so-space-md); overflow-wrap: anywhere; }
|
|
2866
|
+
|
|
2867
|
+
/* The handover note wraps on a narrow screen rather than scrolling sideways. */
|
|
2868
|
+
.handover-note { white-space: pre-wrap; }
|
|
@@ -2,6 +2,8 @@ import { parseFeedbackImage } from "../result-review.js";
|
|
|
2
2
|
import { assignmentSummaryHtml } from "../assignment-ui.js";
|
|
3
3
|
import {} from "../assignment.js";
|
|
4
4
|
import {} from "../browser-workspace.js";
|
|
5
|
+
import { webCardHtml } from "../card-render.js";
|
|
6
|
+
import { cardOfProposal, cardPlainOf, cardsFirst, isCardProposal } from "../chat-card.js";
|
|
5
7
|
import { closedQuestionReason } from "../decision.js";
|
|
6
8
|
import { CHAT_ACTIONS, sharedActionNeedsReview, sharedActionPayload, sharedActionReviewPath } from "../chat-actions.js";
|
|
7
9
|
import { CHAT_CONTINUITY_SCRIPT } from "../chat-continuity.js";
|
|
@@ -544,6 +546,17 @@ export function proposalCardParts(view, inert, decision, returnTo = null) {
|
|
|
544
546
|
card: { id: view.id, kind: view.kind, label: "Question closed", state: view.state, body: htmlString(body), said: null, links: [], primary: null, dismissable: false, note: null },
|
|
545
547
|
};
|
|
546
548
|
}
|
|
549
|
+
// A Toolroll card: data to read, with its fixed links. It confirms nothing, so it waits on nobody.
|
|
550
|
+
if (isCardProposal(view)) {
|
|
551
|
+
const shown = cardOfProposal(view);
|
|
552
|
+
const body = shown === null ? html `<p>${cardPlainOf(view)}</p>` : webCardHtml(shown);
|
|
553
|
+
const links = shown?.buttons.map(one => ({ label: one.label, href: one.path })) ?? [];
|
|
554
|
+
const label = shown?.title ?? "Card";
|
|
555
|
+
return {
|
|
556
|
+
html: html `<article class="card proposal proposal-data" data-card-kind="data"><div class="proposal-body"><h3>${label}</h3>${body}</div>${links.length === 0 ? "" : html `<footer class="proposal-actions">${joinHtml(links.map(one => html `<a class="button-link" href="${one.href}">${one.label}</a>`))}</footer>`}</article>`,
|
|
557
|
+
card: { id: view.id, kind: view.kind, label, state: "pending", body: htmlString(body), said: null, links, primary: null, dismissable: false, note: null, data: true },
|
|
558
|
+
};
|
|
559
|
+
}
|
|
547
560
|
const presentations = {
|
|
548
561
|
task: { label: payload["report"] === true ? "Scout investigation" : "New task", action: "file task", icon: html `<path d="M12 5v14"/><path d="M5 12h14"/>` },
|
|
549
562
|
next: { label: "Queue priority", action: "move to front", icon: html `<path d="M12 19V5"/><path d="m5 12 7-7 7 7"/>` },
|
|
@@ -855,7 +868,7 @@ export function leadBrowserMessages(rows, back, target = { task: null, project:
|
|
|
855
868
|
activity: message.activity, createdAt: message.createdAt,
|
|
856
869
|
...(() => {
|
|
857
870
|
// Under the lead's message only, as the server-rendered thread does: the person's message shares the turn.
|
|
858
|
-
const parts = message.turn === null || message.role === 'operator' ? [] : rows.proposals.filter(one => one.turn === message.turn)
|
|
871
|
+
const parts = message.turn === null || message.role === 'operator' ? [] : cardsFirst(rows.proposals.filter(one => one.turn === message.turn))
|
|
859
872
|
.map(one => leadProposalCardParts(one, rows.pending !== null, rows.decisions.get(typeof one.payload['decision'] === 'number' ? one.payload['decision'] : -1) ?? null, back));
|
|
860
873
|
return { cardsHtml: htmlString(joinHtml(parts.map(one => one.html))), cards: parts.map(one => one.card) };
|
|
861
874
|
})(),
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type Html } from "../html.js";
|
|
2
|
+
import type { Store } from "../store.js";
|
|
3
|
+
export type FollowThroughView = {
|
|
4
|
+
findings: Html | "";
|
|
5
|
+
findingCount: number;
|
|
6
|
+
openFindings: number;
|
|
7
|
+
repairs: Html | "";
|
|
8
|
+
evidence: Html | "";
|
|
9
|
+
handover: Html | "";
|
|
10
|
+
};
|
|
11
|
+
export declare function followThroughView(store: Store, root: string, taskId: string, taskHref: string, canAct: boolean, now: Date): FollowThroughView;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/** The task page's follow-through sections (docs/plans/fleet-follow-through.md): findings, failed checks, before and after,
|
|
2
|
+
* and the handover note. All of a finding's text is model output or repository text: it renders as text, in `html`'s
|
|
3
|
+
* escaping, and no action here is reachable from it. Every form posts to the one action in tasks.ts. */
|
|
4
|
+
import { checkRepairsOfTask, checkRepairWords } from "../check-repair.js";
|
|
5
|
+
import { evidencePairsOf, evidenceWords, failingFirstOf, failingFirstWords } from "../evidence-pair.js";
|
|
6
|
+
import { findingLine, findingFromWords, findingsOfTask, findingStandingWords } from "../findings.js";
|
|
7
|
+
import { handoverLines, handoverNoteOf } from "../handover-note.js";
|
|
8
|
+
import { html, joinHtml, postForm } from "../html.js";
|
|
9
|
+
import { providerCaptures } from "../provider-capture.js";
|
|
10
|
+
import { btn, disclose, field, input, note, textarea } from "./kit.js";
|
|
11
|
+
const act = (taskHref, name, label, hidden = {}, variant = "outline") => postForm(`${taskHref}/follow-through`, joinHtml([
|
|
12
|
+
html `<input type="hidden" name="act" value="${name}">`,
|
|
13
|
+
...Object.entries(hidden).map(([key, value]) => html `<input type="hidden" name="${key}" value="${String(value)}">`),
|
|
14
|
+
btn(label, { variant, type: "submit" }),
|
|
15
|
+
]));
|
|
16
|
+
function findingRow(one, taskHref, canAct) {
|
|
17
|
+
const answered = one.state === "accepted" || one.state === "rejected";
|
|
18
|
+
const closing = canAct && one.state !== "closed" ? disclose("Close it", postForm(`${taskHref}/follow-through`, joinHtml([
|
|
19
|
+
html `<input type="hidden" name="act" value="close"><input type="hidden" name="finding" value="${String(one.id)}">`,
|
|
20
|
+
field({ id: `finding-close-${one.id}`, label: "Why it can close", control: input({ id: `finding-close-${one.id}`, name: "note" }) }),
|
|
21
|
+
btn("Close finding", { variant: "outline", type: "submit" }),
|
|
22
|
+
]))) : "";
|
|
23
|
+
return html `<li data-finding="${one.state}"><p>${findingLine(one)}</p><p class="meta">${findingStandingWords(one)}</p><p class="meta">From ${findingFromWords(one)}. Requested: ${one.action}</p>${canAct && answered ? act(taskHref, "resend", "Send it back", { finding: one.id }) : ""}${closing}</li>`;
|
|
24
|
+
}
|
|
25
|
+
function sendForm(taskHref) {
|
|
26
|
+
return disclose("Send a finding", postForm(`${taskHref}/follow-through`, joinHtml([
|
|
27
|
+
html `<input type="hidden" name="act" value="send">`,
|
|
28
|
+
field({ id: "finding-note", label: "What's wrong", control: textarea({ id: "finding-note", name: "note", rows: 3 }) }),
|
|
29
|
+
field({ id: "finding-file", label: "File, if there is one", control: input({ id: "finding-file", name: "file" }) }),
|
|
30
|
+
field({ id: "finding-line", label: "Line", control: input({ id: "finding-line", name: "line", type: "number" }) }),
|
|
31
|
+
btn("Send to the agent", { variant: "outline", type: "submit" }),
|
|
32
|
+
])));
|
|
33
|
+
}
|
|
34
|
+
const captureRow = (one, taskHref, canAct) => html `<li><p class="meta">${one.provider}, build #${one.run}${one.label === null ? "" : `, marked ${one.label}`}</p><p class="mono">${one.text.slice(0, 500)}</p>${canAct && one.label === null ? joinHtml([act(taskHref, "mark", "Used-up plan", { capture: one.id, label: "exhausted" }), act(taskHref, "mark", "Something else", { capture: one.id, label: "not-exhausted" })]) : ""}</li>`;
|
|
35
|
+
export function followThroughView(store, root, taskId, taskHref, canAct, now) {
|
|
36
|
+
const findings = findingsOfTask(store, taskId);
|
|
37
|
+
const open = findings.filter(one => one.state === "open" || one.state === "with-person").length;
|
|
38
|
+
const repairs = checkRepairsOfTask(store, taskId).filter(one => one.state !== "closed");
|
|
39
|
+
const pairs = evidencePairsOf(store, root, taskId, now);
|
|
40
|
+
const step = failingFirstOf(store, taskId);
|
|
41
|
+
const stepWords = failingFirstWords(step);
|
|
42
|
+
const hasRun = store.lookupRef(taskId) !== null && store.runsFor(store.lookupRef(taskId).id).some(one => one.role === "builder" && one.finishedAt !== null);
|
|
43
|
+
const family = store.taskFamilyOf(taskId, null, true);
|
|
44
|
+
const familyRuns = new Set((family?.versions.map(one => one.id) ?? [taskId]).flatMap(id => { const ref = store.lookupRef(id); return ref === null ? [] : store.runsFor(ref.id).map(run => run.id); }));
|
|
45
|
+
const captures = providerCaptures(store, null, 200).filter(one => familyRuns.has(one.run)).slice(0, 5);
|
|
46
|
+
const latestFailed = repairs.find(one => one.state !== "fixed");
|
|
47
|
+
const note1 = handoverNoteOf(store, root, taskId, now);
|
|
48
|
+
return {
|
|
49
|
+
findingCount: findings.length, openFindings: open,
|
|
50
|
+
findings: findings.length === 0 && !canAct ? "" : joinHtml([
|
|
51
|
+
findings.length === 0 ? "" : html `<ul class="result-changes">${findings.map(one => findingRow(one, taskHref, canAct))}</ul>`,
|
|
52
|
+
canAct ? sendForm(taskHref) : "",
|
|
53
|
+
]),
|
|
54
|
+
repairs: repairs.length === 0 ? "" : html `<ul class="result-changes">${repairs.map(one => html `<li data-check-repair="${one.state}"><p>${checkRepairWords(one)}</p>${one.repairTask === null ? "" : html `<p class="meta"><a class="k-link" href="/t/${encodeURIComponent(one.repairTask)}">Open the repair</a></p>`}${canAct && one.repairTask === null && (one.state === "waiting" || one.state === "parked") ? act(taskHref, "repair", "Start a repair", { run: one.sourceRun }, "primary") : ""}</li>`)}</ul>`,
|
|
55
|
+
evidence: pairs.length === 0 && stepWords === null && captures.length === 0 && !(canAct && hasRun) ? "" : joinHtml([
|
|
56
|
+
pairs.length === 0 ? "" : html `<ul class="result-changes">${pairs.map(one => html `<li data-evidence="${one.reading}"><p>${evidenceWords(one)}</p><p class="meta"><a class="k-link" href="/r/${String(one.before.run)}">Before</a>${one.after === null ? "" : html ` · <a class="k-link" href="/r/${String(one.after.run)}">After</a>`}${one.before.screenshots.length + (one.after?.screenshots.length ?? 0) === 0 ? "" : ` · ${one.before.screenshots.length + (one.after?.screenshots.length ?? 0)} screenshots`}</p></li>`)}</ul>`,
|
|
57
|
+
stepWords === null ? "" : note(stepWords),
|
|
58
|
+
step?.output == null ? "" : html `<p class="mono">${step.output}</p>`,
|
|
59
|
+
canAct && hasRun ? html `<div class="k-acts">${joinHtml([
|
|
60
|
+
...(latestFailed === undefined ? [] : [act(taskHref, "save-evidence", "Save the failing result", { run: latestFailed.sourceRun })]),
|
|
61
|
+
...(pairs.length === 0 ? [] : [act(taskHref, "rerun-evidence", "Run the same check again")]),
|
|
62
|
+
])}</div>` : "",
|
|
63
|
+
canAct ? act(taskHref, step === null ? "failing-first-on" : "failing-first-off", step === null ? "Ask for a failing test first" : "Turn off failing test first") : "",
|
|
64
|
+
captures.length === 0 ? "" : disclose("What the provider said", html `<ul class="result-changes">${captures.map(one => captureRow(one, taskHref, canAct))}</ul>`),
|
|
65
|
+
]),
|
|
66
|
+
handover: note1 === null ? "" : html `<pre class="mono handover-note">${handoverLines(note1).join("\n")}</pre>`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -1698,7 +1698,7 @@ export function resultPanelParts(detail, o) {
|
|
|
1698
1698
|
summaryParts.push(html `<details class="result-notes" data-result-notes-agent><summary>What the agent reported</summary>${agentAccount}</details>`);
|
|
1699
1699
|
// MEDIUM and LOW never block: suggested follow-ups, on request.
|
|
1700
1700
|
if (automatic !== null && automatic.followUps.length > 0) {
|
|
1701
|
-
summaryParts.push(html `<details class="result-notes" data-review-followups="${automatic.followUps.length}"><summary>Suggested follow-ups · ${automatic.followUps.length}</summary><ul class="result-changes">${automatic.followUps.map(one => html `<li><span class="badge">${one.severity === "MEDIUM" ? "Medium" : "Low"}</span> <span class="mono">${`${one.file}:${one.line}`}</span> ${one.scenario}</li>`)}</ul></details>`);
|
|
1701
|
+
summaryParts.push(html `<details class="result-notes" data-review-followups="${automatic.followUps.length}"><summary>Suggested follow-ups · ${automatic.followUps.length}</summary><ul class="result-changes">${automatic.followUps.map((one, index) => html `<li><span class="badge">${one.severity === "MEDIUM" ? "Medium" : "Low"}</span> <span class="mono">${`${one.file}:${one.line}`}</span> ${one.scenario}${o.csrf === "" ? "" : postForm(`${taskHref(detail.taskId)}/follow-through`, html `<input type="hidden" name="act" value="send"><input type="hidden" name="pointer" value="${`${automatic.run}:${automatic.high.length + index + 1}`}"><button type="submit" class="secondary">Send to the agent</button>`)}</li>`)}</ul></details>`);
|
|
1702
1702
|
}
|
|
1703
1703
|
const evidenceSources = [
|
|
1704
1704
|
...(report !== null ? [report.ok ? `verified report${report.truncated ? " (shortened)" : ""}` : "report (unverifiable)"] : []),
|
|
@@ -9,6 +9,7 @@ import { type FormView } from "../contracts/console-api.js";
|
|
|
9
9
|
import { type DispatchDiagnosis } from "../dispatch.js";
|
|
10
10
|
import { type ReportView } from "../evidence.js";
|
|
11
11
|
import { type Gap } from "../gaps.js";
|
|
12
|
+
import type { FollowThroughView } from "./render-follow-through.js";
|
|
12
13
|
import { type FailureExplanation } from "../needs-you.js";
|
|
13
14
|
import { type RouteOverride, type RouteProjection } from "../phase-routing.js";
|
|
14
15
|
import { type MilestoneState } from "../plan.js";
|
|
@@ -572,6 +573,8 @@ export declare function taskBodyParts(data: {
|
|
|
572
573
|
routeChange?: string | null;
|
|
573
574
|
/** A fast start's result grew past a small change (fast-start.ts): its sentence, and the one act on. */
|
|
574
575
|
fastGrown?: string | null;
|
|
576
|
+
/** Findings, failed checks, before and after, and the handover note (render-follow-through.ts). */
|
|
577
|
+
followThrough?: FollowThroughView | null;
|
|
575
578
|
/** Easy-start C5: the viewer is a laptop's local owner — whether this plan is one it approves without a password. */
|
|
576
579
|
approvalLocal?: LocalApprovalView | null;
|
|
577
580
|
/** Cockpit B2: how the current approval's person was proved (approval proved ledger), when it was a remembered one. */
|