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/chat-decide.js
CHANGED
|
@@ -4,10 +4,9 @@
|
|
|
4
4
|
* and each button is one opaque token bound to the exact result, receipt, plan or commit the card showed: a card a
|
|
5
5
|
* newer result, a finished task or a changed plan has overtaken says so and acts on nothing.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* card keeps its link. The change requests one youiyoux pass filed come as one card that shows every word of each
|
|
7
|
+
* A person with the authority approves a plan or merges from their own linked chat by default (owner's decision,
|
|
8
|
+
* docs/plans/toolroll-cards.md); their own off or limits (chat-approval.ts) and a signed mode's chatApprove term still
|
|
9
|
+
* hold. A tap is theirs alone, and the card binds it to the exact terms. The change requests one youiyoux pass filed come as one card that shows every word of each
|
|
11
10
|
* task's terms, with Approve all and each one's own Approve (the requests card, below).
|
|
12
11
|
*
|
|
13
12
|
* The same in every chat app: Telegram's bridge and the shared engine Slack, Discord and Teams run on (chat-delivery.ts)
|
|
@@ -23,18 +22,19 @@ import { chatControlHref, chatResultHref } from "./chat-controls.js";
|
|
|
23
22
|
import { applyChatTaskAction, chatTaskStamp } from "./chat-task-actions.js";
|
|
24
23
|
import { chatTitle } from "./chat-voice.js";
|
|
25
24
|
import { readVerifiedArtifact, scanForSecrets } from "./evidence.js";
|
|
26
|
-
import {
|
|
27
|
-
import { chatApproveChatsOf, chatNames, modeTermsFromJson } from "./modes.js";
|
|
25
|
+
import { chatApproveMode } from "./chat-approval.js";
|
|
28
26
|
import { parseExecutionPlanDocument } from "./plan.js";
|
|
29
27
|
import { verifyApproverStanding } from "./principal.js";
|
|
30
28
|
import { pullRequestFollowOf } from "./pull-request-flow.js";
|
|
31
29
|
import { requestResultChanges } from "./result-actions.js";
|
|
32
30
|
import { revisionSourceOf } from "./result-review.js";
|
|
33
|
-
import { APPROVAL_PROOF, approvalOf, approvalProofDetail, deployGateScope, quarantinedAuthor } from "./scope.js";
|
|
31
|
+
import { APPROVAL_PROOF, approvalOf, approvalProofDetail, approve, deployGateScope, quarantinedAuthor } from "./scope.js";
|
|
32
|
+
import { gateWords } from "./approval-policy.js";
|
|
34
33
|
import { releaseCardOf } from "./release-card.js";
|
|
35
34
|
import { releaseCiHoldOf, releaseCiHoldWords } from "./release-ci-hold.js";
|
|
36
35
|
import { phoneText, projectLabel } from "./telegram-status.js";
|
|
37
|
-
import { permissionPlainWords, scopeTermsText, shownTermsDigest } from "./chat-terms.js";
|
|
36
|
+
import { cardTermsDigest, chatLimitProblem, permissionPlainWords, scopeTermsText, shownTermsDigest } from "./chat-terms.js";
|
|
37
|
+
import { chainTermsLines } from "./scope.js";
|
|
38
38
|
import { YOUIYOUX_CARD_KIND, YOUIYOUX_FILED_VIA, YOUIYOUX_HOME_PATH } from "./contracts/youiyoux-request.js";
|
|
39
39
|
import { FILED_PER_PASS, requestKindWord, requestPassOf } from "./youiyoux-intake.js";
|
|
40
40
|
import { discordPlain } from "./reply-shape.js";
|
|
@@ -67,33 +67,7 @@ const sha = (text) => createHash("sha256").update(text).digest("hex");
|
|
|
67
67
|
const isAct = (one) => "act" in one;
|
|
68
68
|
const title = (store, taskId) => phoneText(chatTitle(store, taskId), 60);
|
|
69
69
|
// ---- the chatApprove term ----------------------------------------------------------
|
|
70
|
-
|
|
71
|
-
* (chat-approval.ts), or else a live signed mode carrying the chatApprove term, signed by them, that names this chat
|
|
72
|
-
* app (a mode signed before the term named its apps covers Telegram alone). `digest` names that authority for the
|
|
73
|
-
* ledger; the yes itself is the person's tap and is sealed as theirs, so it doesn't lapse when the authority does. */
|
|
74
|
-
export function chatApproveMode(store, repo, approver, now, channel) {
|
|
75
|
-
if (repo === null)
|
|
76
|
-
return { ok: false, why: "Approving from chat isn't turned on for this project." };
|
|
77
|
-
const setting = effectiveChatApproval(store, repo, approver);
|
|
78
|
-
if (setting.ok)
|
|
79
|
-
return { ok: true, digest: setting.setting.digest, limits: setting.limits, source: "setting" };
|
|
80
|
-
const mode = store.activeMode(repo, now);
|
|
81
|
-
const terms = mode === null ? null : modeTermsFromJson(mode.termsJson);
|
|
82
|
-
if (mode === null || terms === null || !terms.chatApprove)
|
|
83
|
-
return setting;
|
|
84
|
-
if (mode.signedBy !== approver || !store.accountCanAccess(approver, repo))
|
|
85
|
-
return { ok: false, why: "Only the person who signed this project's mode can approve from chat." };
|
|
86
|
-
const chats = chatApproveChatsOf(terms);
|
|
87
|
-
if (!chats.includes(channel))
|
|
88
|
-
return { ok: false, why: `Your mode approves from ${chatNames(chats)} only. Sign it again to approve from ${chatNames([channel])}.` };
|
|
89
|
-
return { ok: true, digest: mode.digest, limits: { fullAccess: terms.permissionDefault === "escalated", capMicrousd: terms.perAttemptBudgetMicrousd }, source: "mode" };
|
|
90
|
-
}
|
|
91
|
-
function fullAccess(profile) {
|
|
92
|
-
if (profile === null || profile === undefined)
|
|
93
|
-
return false;
|
|
94
|
-
return profile.provider === "claude" ? profile.permissionArgv === "bypassPermissions"
|
|
95
|
-
: profile.provider === "gemini" ? profile.approvalArgv === "yolo" : isAcpProfile(profile) ? true : profile.sandboxMode === "danger-full-access";
|
|
96
|
-
}
|
|
70
|
+
export { chatApproveMode };
|
|
97
71
|
const money = (micro) => `$${(micro / 1_000_000).toFixed(2)}`;
|
|
98
72
|
/** A plan this person's chat may approve now: the term, the plan's exact bytes, and none of the reasons it must open
|
|
99
73
|
* Toolroll instead. `digest` binds the scope and the plan document the card shows. */
|
|
@@ -121,19 +95,16 @@ from) {
|
|
|
121
95
|
const family = store.taskFamilyOf(taskId, [ref.repo], false);
|
|
122
96
|
if (family === null || family.problem !== null || family.current.id !== taskId)
|
|
123
97
|
return { ok: false, why: "A newer version of this task is current." };
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
return { ok: false, why: "This plan was written for you, so you approve it in Toolroll." };
|
|
98
|
+
// Terms a machine wrote, or checks that reach past the sandbox: a person reads every word before a yes, so the card shows them whole.
|
|
99
|
+
const whole = ref.coordinatorCid !== null || quarantinedAuthor(scope.proposedVia ?? null);
|
|
127
100
|
if (scope.profileState === "unresolved" || scope.routeEra == null || (scope.termsProblem ?? null) !== null)
|
|
128
101
|
return { ok: false, why: "This plan can't say exactly which agents run, so you approve it in Toolroll." };
|
|
129
|
-
const
|
|
130
|
-
if (
|
|
131
|
-
return { ok: false, why:
|
|
132
|
-
const
|
|
133
|
-
if (
|
|
134
|
-
return { ok: false, why:
|
|
135
|
-
if (store.approvalGate(taskId, approver, "mode").verdict !== "seal")
|
|
136
|
-
return { ok: false, why: "This plan touches protected work, so two people approve it in Toolroll." };
|
|
102
|
+
const limited = chatLimitProblem(scope, mode.limits, mode.source === "setting" ? "your chat approval setting" : "your mode");
|
|
103
|
+
if (limited !== null)
|
|
104
|
+
return { ok: false, why: limited };
|
|
105
|
+
const gate = store.approvalGate(taskId, approver, "person");
|
|
106
|
+
if (gate.verdict === "refuse")
|
|
107
|
+
return { ok: false, why: gateWords(gate) };
|
|
137
108
|
if (store.scopePolicyRefusal(taskId) !== null)
|
|
138
109
|
return { ok: false, why: "Your organisation's policy needs this plan approved in Toolroll." };
|
|
139
110
|
// A revision approves only against notes that still verify, as on the console.
|
|
@@ -162,7 +133,8 @@ from) {
|
|
|
162
133
|
if (!verified)
|
|
163
134
|
return { ok: false, why: "The saved plan can't be verified here, so you approve it in Toolroll." };
|
|
164
135
|
}
|
|
165
|
-
|
|
136
|
+
// Terms shown whole are part of what the card is bound to, so the digest kept on its buttons covers those words.
|
|
137
|
+
return { ok: true, modeDigest: mode.digest, source: mode.source, scope, whole, digest: sha(`${scope.digest}:${plan?.sha256 ?? ""}${whole ? `:${cardTermsDigest(scopeTermsText(scope))}` : ""}`) };
|
|
166
138
|
}
|
|
167
139
|
/** A ready pull request this person's chat may merge now, bound to the exact commit. */
|
|
168
140
|
export function mergeInChat(store, taskId, run, approver, now, channel) {
|
|
@@ -397,10 +369,13 @@ export function planCardText(store, taskId, scope, root, channel) {
|
|
|
397
369
|
const allowing = [permissionPlainWords(scope.profile), scope.budgetMicrousd === null ? "no attempt limit" : `up to ${money(scope.budgetMicrousd)} per attempt`]
|
|
398
370
|
.filter((one) => one !== null);
|
|
399
371
|
const changed = routeChangeOf(store, taskId);
|
|
372
|
+
// Fallback agents are terms: in full on every card, near the top, and never cut.
|
|
373
|
+
const chain = chainTermsLines(scope);
|
|
400
374
|
if (channel === "telegram") {
|
|
401
375
|
return [
|
|
402
376
|
`Plan ready: ${title(store, taskId)}`,
|
|
403
377
|
...(changed === null ? [] : [changed]),
|
|
378
|
+
...chain,
|
|
404
379
|
`Starting allows: ${allowing.join(" · ")}`,
|
|
405
380
|
"",
|
|
406
381
|
...goal,
|
|
@@ -414,6 +389,7 @@ export function planCardText(store, taskId, scope, root, channel) {
|
|
|
414
389
|
return [
|
|
415
390
|
`Plan ready: ${title(store, taskId)}`,
|
|
416
391
|
...(changed === null ? [] : [changed]),
|
|
392
|
+
...chain,
|
|
417
393
|
"",
|
|
418
394
|
...goal,
|
|
419
395
|
"",
|
|
@@ -470,7 +446,7 @@ export function decideOffer(store, target, who, now, channel, root) {
|
|
|
470
446
|
catch {
|
|
471
447
|
head = [title(store, target.taskId)];
|
|
472
448
|
}
|
|
473
|
-
return { text: [...head, "",
|
|
449
|
+
return { text: [...head, "", needsFullCard(plan.scope)].join("\n"), rows: [[{ label: "Approve in Toolroll", path: chatControlHref("approval", target.taskId) }]] };
|
|
474
450
|
}
|
|
475
451
|
// A deploy gate is approved from chat only through its release card (chat-management.md §0a.5).
|
|
476
452
|
if (release) {
|
|
@@ -576,7 +552,7 @@ export function isDecideToken(store, token) {
|
|
|
576
552
|
/** A release card's acts carry its card digest (release-card.ts), never a plan's. */
|
|
577
553
|
const RELEASE_DIGEST = "release:";
|
|
578
554
|
/** Why a password-only scope (passwordOnlySeal) isn't approved from chat, for a release or a plan. */
|
|
579
|
-
const
|
|
555
|
+
const needsFullCard = (scope) => `This ${deployGateScope(scope) ? "release" : "plan"}'s checks run outside the sandbox, so this card can't approve it. Approve it in Toolroll, or ask your lead for its approval card, which shows every term.`;
|
|
580
556
|
/** Whether only a person's password ceremony seals this scope (store.ts sealScopeApproval): checks that reach beyond the
|
|
581
557
|
* sandbox (sign-ins, or none at all) or extra macOS services, sealed into its profile. */
|
|
582
558
|
const passwordOnlySeal = (scope) => {
|
|
@@ -615,7 +591,7 @@ seat, root) {
|
|
|
615
591
|
if (!deployGateScope(plan.scope))
|
|
616
592
|
return { ok: false, why: "This plan isn't a release." };
|
|
617
593
|
if (passwordOnlySeal(plan.scope))
|
|
618
|
-
return { ok: false, why:
|
|
594
|
+
return { ok: false, why: needsFullCard(plan.scope) };
|
|
619
595
|
const ref = store.lookupRef(taskId);
|
|
620
596
|
if (ref === null || nightlyTaskRef(store, ref.id))
|
|
621
597
|
return { ok: false, why: NIGHTLY_CHECK_ONLY };
|
|
@@ -709,7 +685,7 @@ function staleWhy(store, row, who, now, channel, root) {
|
|
|
709
685
|
if (deployGateScope(plan.scope))
|
|
710
686
|
return "This is a release: approve it from its release card.";
|
|
711
687
|
if (passwordOnlySeal(plan.scope))
|
|
712
|
-
return
|
|
688
|
+
return needsFullCard(plan.scope);
|
|
713
689
|
return plan.digest === row.digest ? null : "The plan changed since this card was sent.";
|
|
714
690
|
}
|
|
715
691
|
case "merge": {
|
|
@@ -868,7 +844,10 @@ export function applyDecideTap(store, seat, input) {
|
|
|
868
844
|
const spec = { act: "approve", run: null, digest: row.digest };
|
|
869
845
|
const yes = mint(store, seat, input.message, spec, row.taskId, "yes", now);
|
|
870
846
|
const later = mint(store, seat, input.message, { act: "not-now", run: null, digest: row.digest }, row.taskId, "offer", now);
|
|
871
|
-
|
|
847
|
+
// Terms a machine wrote go on the card word for word, or the yes isn't to what they say.
|
|
848
|
+
if (plan.whole && scopeTermsText(plan.scope).length > PLATFORM_LIMITS[seat.channel] - ROOM)
|
|
849
|
+
return stale(`This plan's ${TOO_LONG}, so you approve it in Toolroll.`);
|
|
850
|
+
return { ack: "Read the plan, then approve.", edit: { text: `${plan.whole ? `${name}\n\n${scopeTermsText(plan.scope)}` : planCardText(store, row.taskId, plan.scope, input.root, seat.channel)}\n\n${QUESTIONS.approve(name)} This approves exactly this plan.`,
|
|
872
851
|
rows: [[{ label: DECIDE_LABELS.approve, token: yes }], [{ label: DECIDE_LABELS.edit, link: { label: DECIDE_LABELS.edit, path: chatControlHref("approval", row.taskId) } }, { label: DECIDE_LABELS["not-now"], token: later }]], tokens: [yes, later] } };
|
|
873
852
|
}
|
|
874
853
|
// The first tap asks; only the yes acts, bound to exactly what this button was.
|
|
@@ -929,9 +908,19 @@ export function applyDecideTap(store, seat, input) {
|
|
|
929
908
|
if (deployGateScope(plan.scope))
|
|
930
909
|
return notDone("This is a release: approve it from its release card.");
|
|
931
910
|
if (passwordOnlySeal(plan.scope))
|
|
932
|
-
return notDone(
|
|
933
|
-
|
|
911
|
+
return notDone(needsFullCard(plan.scope));
|
|
912
|
+
// The one approval every road shares (scope.ts approve): the rules, two-person votes and the proof, as this person.
|
|
913
|
+
// plan.digest matched this button's kept digest above (staleWhy), so these are the words its card showed.
|
|
914
|
+
const words = scopeTermsText(plan.scope);
|
|
915
|
+
if (plan.whole && (plan.digest !== row.digest || words.length > PLATFORM_LIMITS[seat.channel] - ROOM))
|
|
916
|
+
return notDone("this plan changed. Open it in Toolroll.");
|
|
917
|
+
const approved = approve(store, row.taskId, who.name, now, plan.scope.digest, "", {}, undefined, { modeDigest: plan.modeDigest, shown: plan.whole ? { expect: cardTermsDigest(words), terms: words } : null });
|
|
918
|
+
if (!approved.ok && approved.reason === "second-approver")
|
|
919
|
+
return done(gateWords({ verdict: "vote", have: approved.have, need: 2, already: approved.already }));
|
|
920
|
+
if (!approved.ok)
|
|
934
921
|
return notDone("the plan couldn't be approved from chat. Open it in Toolroll.");
|
|
922
|
+
store.recordAction({ at: now.toISOString(), actor: who.name, repo: ref.repo, taskId: row.taskId, runId: null, action: "plan approved in chat", outcome: "approved",
|
|
923
|
+
source: "request", detail: `via ${seat.channel} · chat binding #${seat.binding} · ${plan.source} ${plan.modeDigest}` });
|
|
935
924
|
return done("✓ Approved. Work starts when a worker is free.");
|
|
936
925
|
}
|
|
937
926
|
case "merge": {
|
package/dist/chat-delivery.js
CHANGED
|
@@ -10,6 +10,9 @@ import { ceilingDigestOf, verifyApproverStanding } from "./principal.js";
|
|
|
10
10
|
import { ChatState, ChatDeliveryError, chatHash, savedChatPart, } from "./chat-delivery-state.js";
|
|
11
11
|
import { readChatActionBody, readChatMessageBody, readChatPairBody, readChatPart } from "./contracts/chat-content.js";
|
|
12
12
|
import { readProposalActionRow } from "./contracts/chat-callback-rows.js";
|
|
13
|
+
import { cardContent, cardPart } from "./chat-card-part.js";
|
|
14
|
+
import { flowDoneCard } from "./flow-result-card.js";
|
|
15
|
+
import { cardsFirst, isCardProposal } from "./chat-card.js";
|
|
13
16
|
import { answerChatFlowPrompt, answerChoiceMessage, applyChatFlowTap, flowDecisionParts, flowSendParts } from "./chat-flow.js";
|
|
14
17
|
import { connectChannel, FLOW_WORDS, takeChannelMessage, watchedChannel } from "./chat-inbox.js";
|
|
15
18
|
import { triggerConfigOf } from "./flow-triggers.js";
|
|
@@ -484,10 +487,9 @@ export async function processChatEvent(options) {
|
|
|
484
487
|
task: image.taskId,
|
|
485
488
|
run: image.run,
|
|
486
489
|
});
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
.
|
|
490
|
-
parts.push({ text: "", proposal: proposal.id });
|
|
490
|
+
// Cards first, so what a card leads to sits right under it. A card is an ordinary part: it confirms nothing.
|
|
491
|
+
for (const proposal of cardsFirst(store.listLeadProposals(turn.thread, ["pending"]).filter((p) => p.turn === turn.id)))
|
|
492
|
+
parts.push(isCardProposal(proposal) ? cardPart(proposal) : { text: "", proposal: proposal.id });
|
|
491
493
|
state.plan(event.id, parts, nowOf(options));
|
|
492
494
|
return true;
|
|
493
495
|
}
|
|
@@ -655,6 +657,7 @@ export function applyChatAction(options, event, binding, repos) {
|
|
|
655
657
|
// A paired chat seat speaks for its account's full authority (pairing is a full session's act).
|
|
656
658
|
authority: "full",
|
|
657
659
|
via: options.state.provider,
|
|
660
|
+
binding: binding.id,
|
|
658
661
|
evidenceRoot: options.evidenceRoot,
|
|
659
662
|
confirm: phase === "yes",
|
|
660
663
|
deferSignal: (signal) => signals.push(signal),
|
|
@@ -1053,6 +1056,12 @@ function offerNotice(options, notification, binding, registry) {
|
|
|
1053
1056
|
ts: "", thread: "", payload: {}, created: now.toISOString() });
|
|
1054
1057
|
state.plan(id, flowSendParts(store, notification, state.provider), now);
|
|
1055
1058
|
}
|
|
1059
|
+
else if (personal && flowDoneCard(store, notification, now) !== null) {
|
|
1060
|
+
// A card that reached Done: what it went through, as a card with its links.
|
|
1061
|
+
state.enqueue({ id, installation: identity.installation, binding: binding.id, kind: "notice", channel: binding.channel, member: binding.member,
|
|
1062
|
+
ts: "", thread: "", payload: {}, created: now.toISOString() });
|
|
1063
|
+
state.plan(id, [cardContent(flowDoneCard(store, notification, now))], now);
|
|
1064
|
+
}
|
|
1056
1065
|
else if (notification.kind === "flow-card" && questionParts(store, notification, binding) !== null) {
|
|
1057
1066
|
// A subagent's question (v93): its options and "Answer in words" on the notice, for the person it asks.
|
|
1058
1067
|
state.enqueue({ id, installation: identity.installation, binding: binding.id, kind: "notice", channel: binding.channel, member: binding.member,
|
package/dist/chat-flow.d.ts
CHANGED
|
@@ -1,30 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Flow decisions in Slack, Discord and Teams (v88): the Approve / Edit /
|
|
3
|
-
* Send back that Telegram has (telegram-flow.ts), on the notice itself.
|
|
4
|
-
*
|
|
5
|
-
* - The notice carries the draft (split over as many parts as it needs, so
|
|
6
|
-
* what is approved is what was read) and the buttons on its last part.
|
|
7
|
-
* - Approve decides through the same door as the console (decideFlowCard),
|
|
8
|
-
* as the paired person, for exactly the visit the notice was about, then
|
|
9
|
-
* repaints the notice with what happened and no buttons.
|
|
10
|
-
* - Edit and Send back open a prompt: the person's next message in their
|
|
11
|
-
* chat with Toolroll is the new draft (which comes back with fresh
|
|
12
|
-
* buttons) or the note. "cancel" leaves it, and a prompt lapses after
|
|
13
|
-
* 30 minutes. Chat apps can't all name the message a reply answers, so
|
|
14
|
-
* the prompt says plainly what the next message does.
|
|
15
|
-
*
|
|
16
|
-
* Every button is one opaque token for one visit of one card, on the part it
|
|
17
|
-
* rides: a tap on any other message, from anyone but the paired person, or
|
|
18
|
-
* for a card that moved on, changes nothing.
|
|
19
|
-
*
|
|
20
|
-
* A "Send to me" visit (flow-send.ts) is one notice with its links; a "Person
|
|
21
|
-
* chooses" visit is the same with the zone's own options as buttons. Instead
|
|
22
|
-
* of a tap, a reply in the notice's thread is the note for where replies go;
|
|
23
|
-
* the first other message in their chat is asked about once ("Use this as
|
|
24
|
-
* your note?"), and everything else goes to the lead as usual. Choices are
|
|
25
|
-
* made through chooseFlowCard, by that person alone, for exactly that visit,
|
|
26
|
-
* and spend that visit's buttons in every chat app.
|
|
27
|
-
*/
|
|
28
1
|
import type { ChatProvider } from "./contracts/chat-tables.js";
|
|
29
2
|
import { type ChatBinding, type ChatContent, type ChatEvent, type ChatState } from "./chat-delivery-state.js";
|
|
30
3
|
import type { Store } from "./store.js";
|
package/dist/chat-flow.js
CHANGED
|
@@ -1,3 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flow decisions in Slack, Discord and Teams (v88): the Approve / Edit /
|
|
3
|
+
* Send back that Telegram has (telegram-flow.ts), on the notice itself.
|
|
4
|
+
*
|
|
5
|
+
* - The notice carries the draft (split over as many parts as it needs, so
|
|
6
|
+
* what is approved is what was read) and the buttons on its last part.
|
|
7
|
+
* - Approve decides through the same door as the console (decideFlowCard),
|
|
8
|
+
* as the paired person, for exactly the visit the notice was about, then
|
|
9
|
+
* repaints the notice with what happened and no buttons.
|
|
10
|
+
* - Edit and Send back open a prompt: the person's next message in their
|
|
11
|
+
* chat with Toolroll is the new draft (which comes back with fresh
|
|
12
|
+
* buttons) or the note. "cancel" leaves it, and a prompt lapses after
|
|
13
|
+
* 30 minutes. Chat apps can't all name the message a reply answers, so
|
|
14
|
+
* the prompt says plainly what the next message does.
|
|
15
|
+
*
|
|
16
|
+
* Every button is one opaque token for one visit of one card, on the part it
|
|
17
|
+
* rides: a tap on any other message, from anyone but the paired person, or
|
|
18
|
+
* for a card that moved on, changes nothing.
|
|
19
|
+
*
|
|
20
|
+
* A "Send to me" visit (flow-send.ts) is one notice with its links; a "Person
|
|
21
|
+
* chooses" visit is the same with the zone's own options as buttons. Instead
|
|
22
|
+
* of a tap, a reply in the notice's thread is the note for where replies go;
|
|
23
|
+
* the first other message in their chat is asked about once ("Use this as
|
|
24
|
+
* your note?"), and everything else goes to the lead as usual. Choices are
|
|
25
|
+
* made through chooseFlowCard, by that person alone, for exactly that visit,
|
|
26
|
+
* and spend that visit's buttons in every chat app.
|
|
27
|
+
*/
|
|
28
|
+
import { cardContent } from "./chat-card-part.js";
|
|
1
29
|
import { LIMITS } from "./decision.js";
|
|
2
30
|
import { keptDraft } from "./flow-draft.js";
|
|
3
31
|
import { decideFlowCard, flowCardHref, flowDefinitionOf } from "./flow-engine.js";
|
|
@@ -66,11 +94,12 @@ export function flowSendParts(store, notification, app) {
|
|
|
66
94
|
const content = kept === null ? null : readFlowSend(kept.contentJson);
|
|
67
95
|
if (content === null)
|
|
68
96
|
return null;
|
|
97
|
+
const card = content.card === undefined ? [] : [cardContent(content.card)];
|
|
69
98
|
const voiced = itemsText(content, notification.subject, app);
|
|
70
99
|
if (voiced !== null)
|
|
71
|
-
return voiced.map((text, index) => index < voiced.length - 1 ? { text, voice: true } : { text, voice: true, ...sendLinks(flowSendPaths(content)) });
|
|
100
|
+
return [...voiced.map((text, index) => index < voiced.length - 1 ? { text, voice: true } : { text, voice: true, ...sendLinks(flowSendPaths(content)) }), ...card];
|
|
72
101
|
const all = pieces(chatFlowText(`${notification.subject}\n\n${notification.body}`));
|
|
73
|
-
return all.map((piece, index) => index < all.length - 1 ? { text: piece } : { text: piece, ...sendLinks(flowSendPaths(content)) });
|
|
102
|
+
return [...all.map((piece, index) => index < all.length - 1 ? { text: piece } : { text: piece, ...sendLinks(flowSendPaths(content)) }), ...card];
|
|
74
103
|
}
|
|
75
104
|
/**
|
|
76
105
|
* A "flow-decision" notice as chat parts, or null when it isn't one (or the
|
package/dist/chat-quiet.js
CHANGED
|
@@ -19,7 +19,7 @@ import { YOUIYOUX_CARD_KIND } from "./contracts/youiyoux-request.js";
|
|
|
19
19
|
* (a sign-in or plan-limit pause, an evening digest). */
|
|
20
20
|
const NEEDED_KINDS = new Set([
|
|
21
21
|
"acceptance-ready", "acceptance-evidence", "flow-decision", "flow-card", "plan-ready", "report-ready",
|
|
22
|
-
"stale-approval", "security-release", "secret-detected",
|
|
22
|
+
"stale-approval", "security-release", "secret-detected", "protected-storage",
|
|
23
23
|
]);
|
|
24
24
|
/** What still wants a person who pairs after it landed, in every chat app: an open decision or an unresolved attention
|
|
25
25
|
* notice. Routine history is never replayed to a chat that just arrived (store.ts WANTED_BEFORE_PAIRING, the same rule). */
|
|
@@ -7,8 +7,8 @@ export declare const CHAT_TASK_ACTIONS: {
|
|
|
7
7
|
readonly detail: "Pauses this task once the run acknowledges the stop. Saved work remains; other tasks continue.";
|
|
8
8
|
};
|
|
9
9
|
readonly resume: {
|
|
10
|
-
readonly label: "
|
|
11
|
-
readonly detail: "
|
|
10
|
+
readonly label: "Resume task";
|
|
11
|
+
readonly detail: "Lifts this stop’s hold and continues from saved work. Other holds and approval requirements still apply.";
|
|
12
12
|
};
|
|
13
13
|
readonly retry: {
|
|
14
14
|
readonly label: "Try again";
|
|
@@ -48,6 +48,7 @@ export declare function chatTaskRun(store: Store, task: string, operation: "stop
|
|
|
48
48
|
export declare function applyChatTaskAction(store: Store, who: VerifiedApprover, payload: Record<string, unknown>, now: Date, web: boolean, controls?: Pick<StopRequest, "deferSignal"> & {
|
|
49
49
|
via?: StopRequest["via"];
|
|
50
50
|
evidenceRoot?: string;
|
|
51
|
+
person?: boolean;
|
|
51
52
|
}): {
|
|
52
53
|
ok: true;
|
|
53
54
|
taskId: string;
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { verifiedAuthor } from "./store.js";
|
|
2
|
-
import {
|
|
2
|
+
import { chatApproveMode, effectiveChatApproval } from "./chat-approval.js";
|
|
3
|
+
import { requestTaskStop, resumeTaskStop, taskControlOf } from "./task-control.js";
|
|
3
4
|
import { authorizePlanUnderMode } from "./plan-auto.js";
|
|
4
5
|
import { fileAddTestsTask, requestFollowUpChecks } from "./result-follow-ups.js";
|
|
5
6
|
export const CHAT_TASK_ACTIONS = {
|
|
6
7
|
// Build #1604 feedback: one short consequence that still says saved work
|
|
7
8
|
// remains, other tasks continue, and the pause waits for acknowledgement.
|
|
8
9
|
stop: { label: "Stop task", detail: "Pauses this task once the run acknowledges the stop. Saved work remains; other tasks continue." },
|
|
9
|
-
resume: { label: "
|
|
10
|
+
resume: { label: "Resume task", detail: "Lifts this stop’s hold and continues from saved work. Other holds and approval requirements still apply." },
|
|
10
11
|
retry: { label: "Try again", detail: "Queues another attempt. Existing approvals and holds still apply." },
|
|
11
12
|
plan: { label: "Plan first", detail: "Requests a plan. Building still requires approval." },
|
|
12
13
|
wait_for: { label: "Add dependency", detail: "Waits for the selected task to finish." },
|
|
@@ -52,9 +53,22 @@ export function applyChatTaskAction(store, who, payload, now, web, controls = {}
|
|
|
52
53
|
const run = chatTaskRun(store, task, operation, now);
|
|
53
54
|
if (run === null || run !== payload["run"])
|
|
54
55
|
return { ok: false, message: "This attempt changed. Review the current task before confirming." };
|
|
55
|
-
//
|
|
56
|
-
if (operation === "resume")
|
|
57
|
-
|
|
56
|
+
// Resuming is an approval: a person's own tap or signed-in click lifts the stop; any other road only asks for it.
|
|
57
|
+
if (operation === "resume") {
|
|
58
|
+
if (controls.person !== true)
|
|
59
|
+
return { ok: true, taskId: task, said: `Resume requested for ${task}. Finish it in your chat or on the task.` };
|
|
60
|
+
// Resuming is an approval: a person's own off holds here as on every other one.
|
|
61
|
+
const allowed = effectiveChatApproval(store, ref.repo, who.name);
|
|
62
|
+
if (!allowed.ok && controls.via !== "web") {
|
|
63
|
+
const mode = controls.via === undefined || controls.via === "cli" ? allowed : chatApproveMode(store, ref.repo, who.name, now, controls.via);
|
|
64
|
+
if (!mode.ok)
|
|
65
|
+
return { ok: false, message: mode.why };
|
|
66
|
+
}
|
|
67
|
+
if (!allowed.ok && controls.via === "web")
|
|
68
|
+
return { ok: false, message: allowed.why };
|
|
69
|
+
const resumed = resumeTaskStop(store, { taskId: task, runId: run, by: verifiedAuthor(who.name), via: controls.via ?? "web" }, now);
|
|
70
|
+
return resumed.ok ? { ok: true, taskId: task, said: `Stop released. ${resumed.gate ? "Other requirements still prevent the next attempt." : "The next attempt may start."}` } : { ok: false, message: resumed.detail };
|
|
71
|
+
}
|
|
58
72
|
const { via, evidenceRoot: _root, ...rest } = controls;
|
|
59
73
|
const result = requestTaskStop(store, { taskId: task, runId: run, by: verifiedAuthor(who.name), via: via ?? (web ? "web" : "cli"), ...rest }, now);
|
|
60
74
|
return result.ok ? { ok: true, taskId: task, said: `Stop requested for ${task}, run #${run}.` }
|
package/dist/chat-terms.d.ts
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Scope } from "./scope.js";
|
|
2
2
|
/** What a plan's yes allows an agent to do, in a few plain words (the approval sheet's "You're allowing"). */
|
|
3
3
|
export declare function permissionPlainWords(profile: Scope["profile"] | null | undefined): string | null;
|
|
4
|
-
export type ShownScope = Pick<Scope, "goal" | "outOfScope" | "touches" | "acceptance" | "profile" | "budgetMicrousd"
|
|
4
|
+
export type ShownScope = Pick<Scope, "goal" | "outOfScope" | "touches" | "acceptance" | "profile" | "budgetMicrousd"> & Partial<Pick<Scope, "proposedChainJson">>;
|
|
5
5
|
/** Every word of the terms a yes agrees to, as the card shows them: the goal, what's excluded, the files (or plainly any
|
|
6
6
|
* file), each requirement with the evidence it needs, and what the agent may do and spend. Nothing is shortened. */
|
|
7
7
|
export declare function scopeTermsText(scope: ShownScope): string;
|
|
8
8
|
/** The digest of the terms shown, bound to the scope's own digest: a changed word, limit or route is another digest. */
|
|
9
9
|
export declare function shownTermsDigest(scope: ShownScope & Pick<Scope, "digest">): string;
|
|
10
|
+
/** The digest of the exact words a card showed, taken when the card was made and kept with it. */
|
|
11
|
+
export declare function cardTermsDigest(terms: string): string;
|
|
12
|
+
/** Whether these words show this scope as it is now: its whole terms text, word for word, and what only a person's own
|
|
13
|
+
* yes may agree to (checks past the sandbox, extra services). Part of the terms, or an earlier version's, is not. */
|
|
14
|
+
export declare function cardShowsScope(scope: ShownScope, terms: string): boolean;
|
|
15
|
+
/** Whether a scope lets its agent do anything, nothing asking first. */
|
|
16
|
+
export declare function scopeFullAccess(profile: Scope["profile"] | null | undefined): boolean;
|
|
17
|
+
/** Why a person's own limits refuse this scope, or null. */
|
|
18
|
+
export declare function chatLimitProblem(scope: Pick<Scope, "profile" | "budgetMicrousd">, limits: {
|
|
19
|
+
fullAccess: boolean;
|
|
20
|
+
capMicrousd: number | null;
|
|
21
|
+
}, yours: string): string | null;
|
package/dist/chat-terms.js
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
8
|
import { isAcpProfile } from "./acp-agents.js";
|
|
9
|
+
import { checkAccessWords } from "./check-access.js";
|
|
10
|
+
import { sandboxTermsWords } from "./project-sandbox.js";
|
|
11
|
+
import { chainTermsLines } from "./scope.js";
|
|
9
12
|
/** What a plan's yes allows an agent to do, in a few plain words (the approval sheet's "You're allowing"). */
|
|
10
13
|
export function permissionPlainWords(profile) {
|
|
11
14
|
if (profile === null || profile === undefined)
|
|
@@ -38,9 +41,41 @@ export function scopeTermsText(scope) {
|
|
|
38
41
|
...(scope.acceptance.length === 0 ? ["You decide when you review the result."] : scope.acceptance.map(one => `• ${one.statement} (${one.evidence.join(", ")})`)),
|
|
39
42
|
"",
|
|
40
43
|
`Starting allows: ${allows.join(" · ")}`,
|
|
44
|
+
...((chain) => chain.length === 0 ? [] : ["", "Fallback agents:", ...chain])(chainTermsLines(scope)),
|
|
41
45
|
].join("\n");
|
|
42
46
|
}
|
|
43
47
|
/** The digest of the terms shown, bound to the scope's own digest: a changed word, limit or route is another digest. */
|
|
44
48
|
export function shownTermsDigest(scope) {
|
|
45
49
|
return createHash("sha256").update(JSON.stringify({ v: 1, scope: scope.digest, shown: scopeTermsText(scope) })).digest("hex");
|
|
46
50
|
}
|
|
51
|
+
/** The digest of the exact words a card showed, taken when the card was made and kept with it. */
|
|
52
|
+
export function cardTermsDigest(terms) {
|
|
53
|
+
return createHash("sha256").update(JSON.stringify({ v: 2, shown: terms })).digest("hex");
|
|
54
|
+
}
|
|
55
|
+
/** Whether these words show this scope as it is now: its whole terms text, word for word, and what only a person's own
|
|
56
|
+
* yes may agree to (checks past the sandbox, extra services). Part of the terms, or an earlier version's, is not. */
|
|
57
|
+
export function cardShowsScope(scope, terms) {
|
|
58
|
+
if (!terms.includes(scopeTermsText(scope)))
|
|
59
|
+
return false;
|
|
60
|
+
const profile = scope.profile;
|
|
61
|
+
if (profile === null || profile === undefined)
|
|
62
|
+
return true;
|
|
63
|
+
const access = profile.checkAccess, mach = sandboxTermsWords(profile).mach;
|
|
64
|
+
return (access === undefined || terms.includes(checkAccessWords(access))) && (mach === null || terms.includes(mach));
|
|
65
|
+
}
|
|
66
|
+
/** Whether a scope lets its agent do anything, nothing asking first. */
|
|
67
|
+
export function scopeFullAccess(profile) {
|
|
68
|
+
if (profile === null || profile === undefined)
|
|
69
|
+
return false;
|
|
70
|
+
return profile.provider === "claude" ? profile.permissionArgv === "bypassPermissions"
|
|
71
|
+
: profile.provider === "gemini" ? profile.approvalArgv === "yolo" : isAcpProfile(profile) ? true : profile.sandboxMode === "danger-full-access";
|
|
72
|
+
}
|
|
73
|
+
/** Why a person's own limits refuse this scope, or null. */
|
|
74
|
+
export function chatLimitProblem(scope, limits, yours) {
|
|
75
|
+
if (scopeFullAccess(scope.profile) && !limits.fullAccess)
|
|
76
|
+
return `This plan asks for more access than ${yours} allows.`;
|
|
77
|
+
const cap = limits.capMicrousd;
|
|
78
|
+
if (cap !== null && (scope.budgetMicrousd === null || scope.budgetMicrousd > cap))
|
|
79
|
+
return `This plan has ${scope.budgetMicrousd === null ? "no attempt limit" : `a ${money(scope.budgetMicrousd)} attempt limit`}, more than ${yours === "your mode" ? "your mode's" : "your chat approval limit of"} ${money(cap)}.`;
|
|
80
|
+
return null;
|
|
81
|
+
}
|
package/dist/check-failure.d.ts
CHANGED
|
@@ -64,6 +64,8 @@ export declare function ranNoCheck(store: Store, row: {
|
|
|
64
64
|
head: unknown;
|
|
65
65
|
checkRow: unknown;
|
|
66
66
|
}): boolean;
|
|
67
|
+
/** The ledger entry the build writes when it refuses a prepared candidate before any process (builder.ts). */
|
|
68
|
+
export declare const NOTHING_STARTED_ACTION = "attempt refused before anything ran";
|
|
67
69
|
/**
|
|
68
70
|
* Whether a passing check of a release task stands, given every earlier attempt on the same candidate and signed scope:
|
|
69
71
|
* each earlier failure must be confirmed infrastructure the release itself reran (at most two), and nothing in the
|
package/dist/check-failure.js
CHANGED
|
@@ -208,11 +208,43 @@ export function setupReplayWords(first) {
|
|
|
208
208
|
* got as far as its check keeps every rule below.
|
|
209
209
|
*/
|
|
210
210
|
export function ranNoCheck(store, row) {
|
|
211
|
-
if (row.outcome !== "failed" || row.reason !== "interrupted" || row.head !== null || row.checkRow !== null)
|
|
211
|
+
if (row.outcome !== "failed" || (row.reason !== "interrupted" && row.reason !== "no-op") || row.head !== null || row.checkRow !== null)
|
|
212
212
|
return false;
|
|
213
213
|
if (checkAttemptOf(store, row.id) !== null)
|
|
214
214
|
return false;
|
|
215
|
-
|
|
215
|
+
if (store.artifactsFor(row.id).some(artifact => artifact.kind === "check-log" || isVerificationReceipt(artifact)))
|
|
216
|
+
return false;
|
|
217
|
+
return row.reason === "interrupted" || refusedBeforeAnything(store, row.id);
|
|
218
|
+
}
|
|
219
|
+
/** The ledger entry the build writes when it refuses a prepared candidate before any process (builder.ts). */
|
|
220
|
+
export const NOTHING_STARTED_ACTION = "attempt refused before anything ran";
|
|
221
|
+
/** Every table a process, an agent or a check of this run writes. */
|
|
222
|
+
const RUN_TRACES = [["artifact", "run"], ["run_process", "run"], ["run_process_summary", "run"], ["run_tool", "run"], ["run_activity", "run"],
|
|
223
|
+
["check_stamp", "run_id"], ["release_check_attempt", "run"], ["run_note", "run"], ["agent_turn", "run"], ["run_checkpoint", "run"], ["run_decision", "run"]];
|
|
224
|
+
/**
|
|
225
|
+
* An attempt the build refused before anything ran (run 3332: Git couldn't read HEAD while it proved the candidate; the
|
|
226
|
+
* next attempt passed). A failed attempt with reason `no-op` proves nothing alone: an agent's empty result and a check's
|
|
227
|
+
* refusal end that way too. So the build's own ledger entry says so, written only where nothing has run, and the run's
|
|
228
|
+
* records agree: no base stamped, no provider start, session, tokens or commit, and not one row of any process, agent or
|
|
229
|
+
* check. All of them, or it stands as a failure, and a record that can't be read (an older database) never excuses one.
|
|
230
|
+
*/
|
|
231
|
+
function refusedBeforeAnything(store, runId) {
|
|
232
|
+
try {
|
|
233
|
+
const said = store.handle.prepare("SELECT COUNT(*) AS n FROM action_ledger WHERE run_id = ? AND action = ? AND outcome = 'refused'").get(runId, NOTHING_STARTED_ACTION)?.["n"];
|
|
234
|
+
if (Number(said) !== 1)
|
|
235
|
+
return false;
|
|
236
|
+
const run = store.handle.prepare("SELECT base_revision, head_revision, provider_started_at, session_id, tokens_in, tokens_out, cost_usd, committed FROM run WHERE id = ?").get(runId);
|
|
237
|
+
if (run === undefined || Object.values(run).some(value => value !== null))
|
|
238
|
+
return false;
|
|
239
|
+
// check_fact is a learning-loops table (eval-hooks.ts): read here in its own literal statement, never through the
|
|
240
|
+
// table list, so no SQL text that could name it is assembled outside its sole writer.
|
|
241
|
+
if (store.handle.prepare("SELECT 1 FROM check_fact WHERE run = ? LIMIT 1").get(runId) !== undefined)
|
|
242
|
+
return false;
|
|
243
|
+
return RUN_TRACES.every(([table, column]) => store.handle.prepare(`SELECT 1 FROM ${table} WHERE ${column} = ? LIMIT 1`).get(runId) === undefined);
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
216
248
|
}
|
|
217
249
|
/** The task's finished builder runs under `digest`, oldest first, each with whether its check passed or never ran. */
|
|
218
250
|
function builderRuns(store, taskRef, digest) {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Store } from "./store.js";
|
|
2
|
+
export type CheckRepairState = "waiting" | "repairing" | "rerunning" | "with-person" | "parked" | "fixed" | "closed";
|
|
3
|
+
export type CheckRepair = {
|
|
4
|
+
id: number;
|
|
5
|
+
familyRoot: string;
|
|
6
|
+
ownerTask: string;
|
|
7
|
+
repo: string;
|
|
8
|
+
revision: string;
|
|
9
|
+
sourceRun: number;
|
|
10
|
+
state: CheckRepairState;
|
|
11
|
+
why: string;
|
|
12
|
+
repairTask: string | null;
|
|
13
|
+
attempt: number | null;
|
|
14
|
+
createdAt: string;
|
|
15
|
+
updatedAt: string;
|
|
16
|
+
};
|
|
17
|
+
export declare function checkRepairsOfTask(store: Store, taskId: string): CheckRepair[];
|
|
18
|
+
export declare function checkRepairOfRun(store: Store, runId: number): CheckRepair | null;
|
|
19
|
+
/** What makes this failure this failure: the command, how it ended, and which files failed. Never the log's own bytes,
|
|
20
|
+
* which differ on every run. */
|
|
21
|
+
export declare function failureKeyOf(receipt: string, log: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* Route one failed check. Runs at disposition. A failure already on record for this family and revision changes nothing,
|
|
24
|
+
* unless it was the one rerun of an unrelated failure, which now goes to a person through the ledger's own rerun stop.
|
|
25
|
+
*/
|
|
26
|
+
export declare function routeFailedCheck(store: Store, repo: string, root: string, runId: number, now: Date): CheckRepair | null;
|
|
27
|
+
export type StartRepair = {
|
|
28
|
+
ok: true;
|
|
29
|
+
draft: string;
|
|
30
|
+
attempt: number;
|
|
31
|
+
approved: boolean;
|
|
32
|
+
} | {
|
|
33
|
+
ok: false;
|
|
34
|
+
reason: "unknown" | "refused";
|
|
35
|
+
message: string;
|
|
36
|
+
};
|
|
37
|
+
/** A person asks for the repair. The draft takes the normal plan approval: this is not that approval. */
|
|
38
|
+
export declare function startCheckRepair(store: Store, root: string, runId: number, now: Date): StartRepair;
|
|
39
|
+
/** A passing result in the family ends its failed-check rows. Rows the person already holds end too: the code passes now. */
|
|
40
|
+
export declare function settleCheckRepairs(store: Store, taskId: string, verdict: string, now: Date): void;
|
|
41
|
+
/** Each sweep: a parked failure whose task is free again takes its next step, unless a newer attempt replaced it. */
|
|
42
|
+
export declare function resumeParkedRepairs(store: Store, root: string, now: Date): number;
|
|
43
|
+
/** A build was accepted as finished: count what it answered, then route its check if it failed. Bookkeeping that never
|
|
44
|
+
* costs the build its result: a problem here leaves the finding open and the failed check on the result. */
|
|
45
|
+
export declare function followBuiltRun(store: Store, repo: string, root: string, runId: number, worktree: string, now: Date): void;
|
|
46
|
+
/** How a failed check stands, for a person, in one line. */
|
|
47
|
+
export declare function checkRepairWords(row: CheckRepair): string;
|