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/surface.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { KNOWLEDGE_DESCRIPTORS } from "./knowledge-cli.js";
|
|
2
2
|
import { MEMORY_DESCRIPTORS } from "./memory-cli.js";
|
|
3
|
+
import { CARD_DESCRIPTOR } from "./card-cli.js";
|
|
3
4
|
import { FLAKY_DESCRIPTORS } from "./flaky-cli.js";
|
|
4
5
|
import { MODELS_DESCRIPTORS } from "./models-cli.js";
|
|
5
6
|
import { MAP_DESCRIPTORS } from "./code-map-cli.js";
|
|
@@ -119,6 +120,7 @@ const GUIDE_ROWS = [
|
|
|
119
120
|
operator("demo", "a seeded throwaway sandbox"),
|
|
120
121
|
...KNOWLEDGE_DESCRIPTORS.map(spec => ({ invocation: `knowledge ${spec.action}`, synopsis: spec.synopsis, audience: "audience" in spec ? spec.audience : "agent", agentMayInvoke: !("audience" in spec), mutation: spec.mutation, flags: spec.flags, ...(spec.takesQuery ? { positionals: [{ name: "query", required: true, meaning: "search text or source file for impact" }] } : {}) })),
|
|
121
122
|
...MEMORY_DESCRIPTORS.map(spec => ({ invocation: `memory ${spec.action}`, synopsis: spec.synopsis, audience: "audience" in spec ? spec.audience : "agent", agentMayInvoke: !("audience" in spec), mutation: spec.mutation, flags: spec.flags, ...(spec.takesQuery ? { positionals: [{ name: "query", required: true, meaning: "search text, a decision id, or the decision sentence" }] } : {}) })),
|
|
123
|
+
{ invocation: CARD_DESCRIPTOR.invocation, synopsis: CARD_DESCRIPTOR.synopsis, audience: "agent", agentMayInvoke: true, mutation: "none", flags: CARD_DESCRIPTOR.flags, positionals: [{ name: "source", required: true, meaning: "which data to show" }] },
|
|
122
124
|
...FLAKY_DESCRIPTORS.map(spec => ({ invocation: `flaky ${spec.action}`, synopsis: spec.synopsis, audience: "agent", agentMayInvoke: true, mutation: spec.mutation, flags: spec.flags, ...(spec.takesQuery ? { positionals: [{ name: "identity", required: true, meaning: "a row's key from flaky list" }] } : {}) })),
|
|
123
125
|
// Flow writes carry an approver's credential: an agent runs them only as the person asked, after showing the preview.
|
|
124
126
|
// Letting a flow run on its own raises authority: the owner's own password ceremony, never an agent's act.
|
|
@@ -327,7 +329,19 @@ const GUIDE_ROWS = [
|
|
|
327
329
|
{ name: "digest", takesValue: true, meaning: "the exact receipt from assignment show; accepts and finishes exactly that result" },
|
|
328
330
|
{ name: "despite", takesValue: true, meaning: "accept with this reason, without finishing; never combines with --finish or --digest (--note is its alias)" }],
|
|
329
331
|
notableReasons: ["usage", "unknown-task", "no-run", "not-ready"] },
|
|
330
|
-
operator("task repair", "inspect or approve
|
|
332
|
+
operator("task repair", "inspect or approve a repair draft; --yes starts the repair a failed check is waiting for, under the normal plan approval"),
|
|
333
|
+
operatorRead("task handover", "where a task stands, from Toolroll's records: revision, decisions, rejected approaches, open findings, checks and the next action"),
|
|
334
|
+
operator("task before-after", "before-and-after evidence: read it, save a failing result, or rerun the same check on the latest commit with no agent"),
|
|
335
|
+
operator("task failing-first", "ask a bug fix's next attempt to write a failing test first (on|off); shown in the result as the agent's report"),
|
|
336
|
+
operatorRead("finding list", "findings still waiting on someone, or every finding of --task <id>"),
|
|
337
|
+
operatorRead("finding show", "one finding: its text, evidence and how it stands"),
|
|
338
|
+
operator("finding send", "send a finding to the task that owns the code; the owner must accept it or reject it with a reason"),
|
|
339
|
+
operator("finding resend", "send an answered finding back, one more round; past the limit it goes to a person"),
|
|
340
|
+
operator("finding close", "a person's decision on a finding"),
|
|
341
|
+
operatorRead("captures list", "what providers said when attempts failed, the ones mentioning limits first; nothing routes on these"),
|
|
342
|
+
operatorRead("captures show", "one saved provider failure"),
|
|
343
|
+
operatorRead("captures export", "one saved provider failure as a fixture"),
|
|
344
|
+
operator("captures mark", "a person's reading of a capture: exhausted or not-exhausted"),
|
|
331
345
|
operator("task route", "which configured agents plan, build and repair this task; phase and size changes require renewed approval"),
|
|
332
346
|
operator("task reopen", "resume external work its tracker closed and has been SEEN open again"),
|
|
333
347
|
operator("task stop", "stop ONE exact live attempt (--run <id>): durable before any process is signalled, answers 'stopping' until its own processes are established gone; work, branch, and evidence preserved; no strike, no retry"),
|
|
@@ -509,6 +523,11 @@ const REMOTE_NO = [
|
|
|
509
523
|
// Reads this machine owner's own Claude and Codex session files (~/.claude, ~/.codex).
|
|
510
524
|
"memory propose",
|
|
511
525
|
];
|
|
526
|
+
const REMOTE_FOLLOW_THROUGH = [
|
|
527
|
+
// On the server's machine or in the console: each writes through a verified person, and they read saved model text.
|
|
528
|
+
"task handover", "task before-after", "task failing-first", "finding list", "finding show", "finding send", "finding resend", "finding close",
|
|
529
|
+
"captures list", "captures show", "captures export", "captures mark",
|
|
530
|
+
];
|
|
512
531
|
const REMOTE_STEP_UP = [
|
|
513
532
|
"proposals", "decide", "task regate", "task approve", "task accept", "task merge", "run settle",
|
|
514
533
|
// Putting tasks away and restoring them: a person's act, in the console or on the server's machine.
|
|
@@ -533,7 +552,7 @@ const REMOTE_YES = [
|
|
|
533
552
|
"knowledge search", "knowledge impact", "knowledge refresh",
|
|
534
553
|
"map", "map area", "map file", "map search", "map refresh",
|
|
535
554
|
"memory search", "memory decisions", "memory show", "memory review", "memory status",
|
|
536
|
-
"flaky list", "flaky show",
|
|
555
|
+
"flaky list", "flaky show", "card",
|
|
537
556
|
"flows list", "flows show", "flows export", "flows create", "flows edit", "flows trigger add", "flows trigger pause", "flows trigger resume",
|
|
538
557
|
"flows trigger remove", "flows card add", "flows archive",
|
|
539
558
|
"models status", "models list",
|
|
@@ -541,11 +560,12 @@ const REMOTE_YES = [
|
|
|
541
560
|
/** Every row's remote policy, by invocation. Exported for the contract test, which holds it to the guide exactly. */
|
|
542
561
|
export const REMOTE_POLICY = new Map([
|
|
543
562
|
...REMOTE_NO.map(invocation => [invocation, "no"]),
|
|
563
|
+
...REMOTE_FOLLOW_THROUGH.map(invocation => [invocation, "no"]),
|
|
544
564
|
...REMOTE_STEP_UP.map(invocation => [invocation, "step-up"]),
|
|
545
565
|
...REMOTE_YES.map(invocation => [invocation, "yes"]),
|
|
546
566
|
]);
|
|
547
567
|
/** How many times each invocation was classified — the contract test refuses a row stated twice. */
|
|
548
|
-
export const REMOTE_POLICY_COUNT = REMOTE_NO.length + REMOTE_STEP_UP.length + REMOTE_YES.length;
|
|
568
|
+
export const REMOTE_POLICY_COUNT = REMOTE_NO.length + REMOTE_FOLLOW_THROUGH.length + REMOTE_STEP_UP.length + REMOTE_YES.length;
|
|
549
569
|
/**
|
|
550
570
|
* Easy-start C3: the commands a laptop's local owner may run through the terminal UI (local-owner.ts runAsLocalOwner),
|
|
551
571
|
* each with the act it is; every other row (and every command with no row) is refused to the local owner before any
|
|
@@ -554,7 +574,7 @@ export const REMOTE_POLICY_COUNT = REMOTE_NO.length + REMOTE_STEP_UP.length + RE
|
|
|
554
574
|
const LOCAL_ALLOW = {
|
|
555
575
|
// Reads.
|
|
556
576
|
pulls: "read", graph: "read", repos: "read", map: "read", "map area": "read", "map file": "read", "map search": "read", status: "read", ready: "read", gaps: "read", contract: "read", "skills list": "read", "skills get": "read",
|
|
557
|
-
"flaky list": "read", "flaky show": "read", "memory review": "read", "memory status": "read", "memory decisions": "read", "memory show": "read",
|
|
577
|
+
"flaky list": "read", "flaky show": "read", card: "read", "memory review": "read", "memory status": "read", "memory decisions": "read", "memory show": "read",
|
|
558
578
|
// Learning loops (D3): a lesson note or a clean-up, decided by a person at the laptop (the writers check the act).
|
|
559
579
|
"memory apply": "memory-review", "memory tidy": "memory-review", "memory restore": "memory-review", "memory unmerge": "memory-review",
|
|
560
580
|
"task list": "read", "task show": "read", "task state": "read", "task wait": "read", "check-progress": "read", "review show": "read", welcome: "read",
|
package/dist/task-control.d.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* hold, recovery, and invocation-gateway machinery. This
|
|
20
20
|
* module only names the door and the fence.
|
|
21
21
|
*/
|
|
22
|
+
import type { StopVia } from "./store.js";
|
|
22
23
|
import { type DispatchDiagnosis } from "./dispatch.js";
|
|
23
24
|
import type { Run, RunStop, Store } from "./store.js";
|
|
24
25
|
/** How often a running attempt re-reads its stop row while a child runs.
|
|
@@ -86,7 +87,7 @@ export type ResumeRequest = {
|
|
|
86
87
|
taskId: string;
|
|
87
88
|
runId: number;
|
|
88
89
|
by: string;
|
|
89
|
-
via:
|
|
90
|
+
via: StopVia;
|
|
90
91
|
/** The workspace occupancy probe (the worktree pool's `inUse`): a
|
|
91
92
|
* checkout some process still holds is not quiescent, whatever the
|
|
92
93
|
* rows say — the concrete gate names the pid. */
|
package/dist/task-status.js
CHANGED
|
@@ -385,6 +385,9 @@ export function pullRequestFactOf(publication) {
|
|
|
385
385
|
/** The assignment's stage. The work status, when the caller read one, names
|
|
386
386
|
* the exact reason (a running planner, a pause, a failed attempt, a sign-in). */
|
|
387
387
|
export function assignmentStageOf(assignment, work, planning = false) {
|
|
388
|
+
// A finding held for a person outlives the stage the work is in, a saved result included: only a person closes it.
|
|
389
|
+
if (work?.token === "finding-held" && assignment.state !== "cancelled")
|
|
390
|
+
return { stage: "needs-you", need: "finding" };
|
|
388
391
|
// The assignment's own reading (a build Toolroll can't confirm stopped) comes first.
|
|
389
392
|
const own = assignment.need ?? null;
|
|
390
393
|
if (own !== null && (assignment.state === "working" || assignment.state === "needs-decision"))
|
|
@@ -407,6 +410,8 @@ export function assignmentStageOf(assignment, work, planning = false) {
|
|
|
407
410
|
return { stage: "needs-you", need: "answer" };
|
|
408
411
|
if (action === "inspect-decisions")
|
|
409
412
|
return { stage: "needs-you", need: "questions" };
|
|
413
|
+
if (action === "inspect-findings")
|
|
414
|
+
return { stage: "needs-you", need: "finding" };
|
|
410
415
|
const token = work?.token ?? "";
|
|
411
416
|
// A failed attempt is Failed. A saved result that still needs a decision
|
|
412
417
|
// (its scope, a hold, a process exit) is Needs you, even if its checks
|
package/dist/teardown-git.d.ts
CHANGED
|
@@ -50,6 +50,8 @@ export type GitBinding = {
|
|
|
50
50
|
* real `.git` folder, holding no `commondir`. No pointer file or `commondir` an agent can write decides it, and no git
|
|
51
51
|
* runs (a page visit reads this). Throws with the reason otherwise. */
|
|
52
52
|
export declare function recordedCommonDir(repo: string): string;
|
|
53
|
+
/** Where git incidents are recorded: beside the fenced views folder (git-incident.ts). */
|
|
54
|
+
export declare function gitIncidentRecord(): string;
|
|
53
55
|
/** Settings fixed on every write through a view: `worktree add` records absolute paths (relative ones would need a
|
|
54
56
|
* config write, which a view keeps); no transport at all (a caller that fetches allows its one protocol by name, as the
|
|
55
57
|
* nightly check's local fetch does with protocol.file.allow); and no split index (its shared index files would be
|
package/dist/teardown-git.js
CHANGED
|
@@ -191,7 +191,7 @@ function nestedRepository(path) {
|
|
|
191
191
|
* repository (boundDirs; `binding.repo` when the caller has the project's), is refused; so is a command whose
|
|
192
192
|
* repository changed under it while git ran.
|
|
193
193
|
*/
|
|
194
|
-
async function inView(cwd, binding, work, after = true, lockTimeoutMs, gitFile = "git") {
|
|
194
|
+
async function inView(cwd, binding, work, after = true, lockTimeoutMs, doing, gitFile = "git") {
|
|
195
195
|
const root = checkoutRootOf(cwd);
|
|
196
196
|
if (root === null)
|
|
197
197
|
return NOT_A_REPOSITORY;
|
|
@@ -207,7 +207,7 @@ async function inView(cwd, binding, work, after = true, lockTimeoutMs, gitFile =
|
|
|
207
207
|
return await withCheckoutLock(realpathSync(homedir()), key, async () => {
|
|
208
208
|
let view;
|
|
209
209
|
try {
|
|
210
|
-
view = gitView(root, "write", binding, gitFile);
|
|
210
|
+
view = gitView(root, "write", binding, gitFile, undefined, doing);
|
|
211
211
|
}
|
|
212
212
|
catch (error) {
|
|
213
213
|
return refused(error instanceof Error ? error.message : "the checkout couldn't be read");
|
|
@@ -255,6 +255,7 @@ export function teardownGit(runner = run, binding = {}, lockTimeoutMs, gitFile =
|
|
|
255
255
|
const forced = removal && args.includes("--force");
|
|
256
256
|
const inspection = args.includes("status") || args.includes("diff");
|
|
257
257
|
const target = removal ? resolve(cwd, args.at(-1)) : cwd;
|
|
258
|
+
const doing = teardownDoing(args);
|
|
258
259
|
const safeArgs = [...args];
|
|
259
260
|
if (inspection && !safeArgs.includes("--ignore-submodules=all")) {
|
|
260
261
|
const command = safeArgs.findIndex(arg => arg === "status" || arg === "diff");
|
|
@@ -288,8 +289,22 @@ export function teardownGit(runner = run, binding = {}, lockTimeoutMs, gitFile =
|
|
|
288
289
|
// the target's view, inspected above, so it judges the target by the target's own settings.
|
|
289
290
|
return ran(removal && !forced ? { ...view.env, GIT_COMMON_DIR: targetView.env["GIT_COMMON_DIR"] } : view.env);
|
|
290
291
|
// A removed target has no git directory left to prove: the folder it was removed from is proven instead.
|
|
291
|
-
}, !removal, lockTimeoutMs, gitFile);
|
|
292
|
-
}, true, lockTimeoutMs, gitFile);
|
|
292
|
+
}, !removal, lockTimeoutMs, doing, gitFile);
|
|
293
|
+
}, true, lockTimeoutMs, teardownDoing(args), gitFile);
|
|
294
|
+
}
|
|
295
|
+
/** What teardown git is doing, for an incident's notice: removing a checkout, or looking one over to tidy it. */
|
|
296
|
+
function teardownDoing(args) {
|
|
297
|
+
return args.includes("worktree") && args.includes("remove") ? "removing an old checkout" : "tidying an old checkout";
|
|
298
|
+
}
|
|
299
|
+
/** What a build's git is doing, for an incident's notice. */
|
|
300
|
+
function checkoutDoing(args) {
|
|
301
|
+
if (args.includes("worktree"))
|
|
302
|
+
return args.includes("remove") ? "removing a checkout" : args.includes("add") ? "making a checkout" : "listing checkouts";
|
|
303
|
+
if (args.some(arg => arg === "commit" || arg === "add" || arg === "merge" || arg === "rebase" || arg === "reset" || arg === "checkout" || arg === "switch"))
|
|
304
|
+
return "saving a build's work";
|
|
305
|
+
if (args.some(arg => arg === "fetch" || arg === "push"))
|
|
306
|
+
return "updating a repository";
|
|
307
|
+
return "reading a build's checkout";
|
|
293
308
|
}
|
|
294
309
|
/** A setup call's time limit: its own five seconds, or less when a caller's deadline leaves less (never below 1 ms;
|
|
295
310
|
* a caller checks the deadline itself before calling). */
|
|
@@ -330,7 +345,7 @@ function regularFile(path) {
|
|
|
330
345
|
}
|
|
331
346
|
/** A checkout's git directory and common directory, as git finds them from its `.git` (a folder, or a one-line
|
|
332
347
|
* `gitdir: ` file); null when the pointer isn't one git would read the same way. */
|
|
333
|
-
function gitDirsOf(checkout) {
|
|
348
|
+
function gitDirsOf(checkout, vouched = NO_VOUCHED) {
|
|
334
349
|
let gitdir = join(checkout, ".git");
|
|
335
350
|
try {
|
|
336
351
|
if (!lstatSync(gitdir).isDirectory()) {
|
|
@@ -350,11 +365,14 @@ function gitDirsOf(checkout) {
|
|
|
350
365
|
// Where the checkout's own files point (a build can write both): read, in the console, only while that folder
|
|
351
366
|
// answered a look lately, and never vouched for by the checkout pool's spelling (folder-gate.ts). Held back: not one
|
|
352
367
|
// git would read the same way, so every caller refuses.
|
|
353
|
-
|
|
368
|
+
// `vouched`: the very folders this view was made from, read again after git ran (settle). They answered moments ago
|
|
369
|
+
// and git just ran in them; the gate's staleness is no evidence about them. Anything they now point elsewhere is
|
|
370
|
+
// gated as ever, so a re-pointed `.git` or `commondir` is never read on the console's thread on its say.
|
|
371
|
+
if (gitdir !== join(checkout, ".git") && !vouched.has(gitdir) && !folderAnswers(gitdir, true))
|
|
354
372
|
return null;
|
|
355
373
|
const named = regularFile(join(gitdir, "commondir"))?.trim();
|
|
356
374
|
const common = named === undefined || named === "" ? gitdir : resolve(gitdir, named);
|
|
357
|
-
if (common !== gitdir && !folderAnswers(common, true))
|
|
375
|
+
if (common !== gitdir && !vouched.has(common) && !folderAnswers(common, true))
|
|
358
376
|
return null;
|
|
359
377
|
return { gitdir, common };
|
|
360
378
|
}
|
|
@@ -382,7 +400,8 @@ function includeIn(path, gitFile = "git", limit) {
|
|
|
382
400
|
return named.toLowerCase();
|
|
383
401
|
return /^\s*\[\s*include(if)?\b/im.test(text) ? "include" : null;
|
|
384
402
|
}
|
|
385
|
-
|
|
403
|
+
const NO_VOUCHED = new Set();
|
|
404
|
+
function boundDirs(checkout, binding = {}, vouched = NO_VOUCHED) {
|
|
386
405
|
const fail = (why) => { throw new Error(`the checkout's .git ${why}`); };
|
|
387
406
|
let top;
|
|
388
407
|
try {
|
|
@@ -403,12 +422,12 @@ function boundDirs(checkout, binding = {}) {
|
|
|
403
422
|
if (held.isDirectory()) {
|
|
404
423
|
if (existsSync(join(dotgit, "commondir")))
|
|
405
424
|
fail("names another repository (commondir)");
|
|
406
|
-
dirs = { checkout: top, gitdir: dotgit, common: dotgit, linked: false };
|
|
425
|
+
dirs = { checkout: top, gitdir: dotgit, common: dotgit, linked: false, spelled: [dotgit] };
|
|
407
426
|
}
|
|
408
427
|
else {
|
|
409
428
|
if (!held.isFile())
|
|
410
429
|
fail("is a link");
|
|
411
|
-
const named = gitDirsOf(top);
|
|
430
|
+
const named = gitDirsOf(top, vouched);
|
|
412
431
|
if (named === null)
|
|
413
432
|
return fail("couldn't be read as git reads it");
|
|
414
433
|
let gitdir;
|
|
@@ -444,7 +463,7 @@ function boundDirs(checkout, binding = {}) {
|
|
|
444
463
|
}
|
|
445
464
|
if (backReal !== dotgit)
|
|
446
465
|
fail("belongs to a git directory that names another checkout");
|
|
447
|
-
dirs = { checkout: top, gitdir, common, linked: true };
|
|
466
|
+
dirs = { checkout: top, gitdir, common, linked: true, spelled: [named.gitdir, named.common, gitdir, common] };
|
|
448
467
|
}
|
|
449
468
|
// The common folder is a main checkout's own `.git` folder, a real one.
|
|
450
469
|
if (basename(dirs.common) !== ".git")
|
|
@@ -490,7 +509,12 @@ export function recordedCommonDir(repo) {
|
|
|
490
509
|
const ADMIN_LINKED = ["index", "logs", "refs", "reftable"];
|
|
491
510
|
/** What git keeps in a common directory (its path.c common_list, and reftable), linked whether or not it exists yet. */
|
|
492
511
|
const COMMON_NAMES = ["branches", "gc.pid", "logs", "lost-found", "modules", "objects", "packed-refs", "reftable", "refs", "remotes", "rr-cache", "shallow", "svn", "worktrees"];
|
|
493
|
-
|
|
512
|
+
/** Where git incidents are recorded: beside the fenced views folder (git-incident.ts). */
|
|
513
|
+
export function gitIncidentRecord() {
|
|
514
|
+
return join(dirname(ensureGitViewsRoot(realpathSync(homedir()))), "git-incidents.jsonl");
|
|
515
|
+
}
|
|
516
|
+
/** `doing`: what the plane was doing, in a person's words ("tidying an old checkout"), for an incident's notice. */
|
|
517
|
+
function gitView(checkout, kind, binding = {}, gitFile = "git", limit, doing) {
|
|
494
518
|
const dirs = boundDirs(checkout, binding);
|
|
495
519
|
const root = ensureGitViewsRoot(realpathSync(homedir()));
|
|
496
520
|
const view = realpathSync(mkdtempSync(join(root, `${kind}-`)));
|
|
@@ -658,8 +682,10 @@ function gitView(checkout, kind, binding = {}, gitFile = "git", limit) {
|
|
|
658
682
|
const settle = () => {
|
|
659
683
|
const now = pinned(dirs);
|
|
660
684
|
let bound = false;
|
|
685
|
+
// The same proof as before git ran, of the same folders: re-read past the console's folder gate only where they are
|
|
686
|
+
// the very ones this view was made from (vouched), so a gate whose answer went stale mid-run is never read as a swap.
|
|
661
687
|
try {
|
|
662
|
-
const again = boundDirs(checkout, binding);
|
|
688
|
+
const again = boundDirs(checkout, binding, new Set(dirs.spelled));
|
|
663
689
|
bound = again.gitdir === dirs.gitdir && again.common === dirs.common;
|
|
664
690
|
}
|
|
665
691
|
catch {
|
|
@@ -669,7 +695,7 @@ function gitView(checkout, kind, binding = {}, gitFile = "git", limit) {
|
|
|
669
695
|
// Too late to prevent: recorded and told (git-incident.ts), never rolled back where the swap leads.
|
|
670
696
|
if (kind === "write") {
|
|
671
697
|
gitIncident({ checkout: dirs.checkout, repository: dirs.common, swapped: now.swapped.length > 0 ? now.swapped.map(({ path, leadsTo }) => ({ path, leadsTo })) : [{ path: join(dirs.checkout, ".git"), leadsTo: null }],
|
|
672
|
-
|
|
698
|
+
cause: now.swapped.length > 0 ? "swapped" : "rebound", ...(doing === undefined ? {} : { doing }), record: gitIncidentRecord() });
|
|
673
699
|
}
|
|
674
700
|
return "the checkout's repository changed while git ran";
|
|
675
701
|
}
|
|
@@ -683,7 +709,7 @@ function gitView(checkout, kind, binding = {}, gitFile = "git", limit) {
|
|
|
683
709
|
catch (error) {
|
|
684
710
|
if (!(error instanceof AnchorMoved))
|
|
685
711
|
return "the checkout's git state couldn't be carried back after git ran";
|
|
686
|
-
gitIncident({ checkout: dirs.checkout, repository: dirs.common, swapped: [{ path: error.message, leadsTo: null }],
|
|
712
|
+
gitIncident({ checkout: dirs.checkout, repository: dirs.common, swapped: [{ path: error.message, leadsTo: null }], cause: "moved", ...(doing === undefined ? {} : { doing }), record: gitIncidentRecord() });
|
|
687
713
|
return "the checkout's repository changed while git ran";
|
|
688
714
|
}
|
|
689
715
|
if (left.length > 0)
|
|
@@ -711,8 +737,8 @@ const WORKTREE_STATE_DIRS = ["sequencer", "rebase-merge", "rebase-apply"];
|
|
|
711
737
|
* state, a split index's shared files); and the object store (the common `objects`, and a submodule git directory's
|
|
712
738
|
* own under modules/): its fan-out folders' entries for links, `info` and `pack` all the way down, but not each loose
|
|
713
739
|
* object (a big repository has very many, and hard-linked objects are legitimate). Any link is refused, so is a folder
|
|
714
|
-
* that can't be listed, and so is any file walked deep with a second hard link (git appends to a log in place)
|
|
715
|
-
*
|
|
740
|
+
* that can't be listed, and so is any file walked deep with a second hard link (git appends to a log in place), but for
|
|
741
|
+
* another checkout's index (siblingIndexes: still never a link). The exemptions are by place only: an `objects` anywhere else (under refs/, logs/, worktrees/) is walked like the rest. For each one found, where it leads (for an incident's record).
|
|
716
742
|
*
|
|
717
743
|
* Checked when the view is made and again after git ran; between the two, a process that outlived the agent's turn
|
|
718
744
|
* can still swap one of these for a link and have git write through it mid-command. That window is an accepted
|
|
@@ -723,6 +749,7 @@ function pinned(dirs) {
|
|
|
723
749
|
const seen = new Set();
|
|
724
750
|
let budget = 400_000;
|
|
725
751
|
const stores = objectStores(dirs.common);
|
|
752
|
+
const siblings = siblingIndexes(dirs);
|
|
726
753
|
/** `deep`: every entry all the way down, each file held to one hard link. `shallow`: the folder's own entries
|
|
727
754
|
* checked for links only. `store`: an object store (objects/: its fan-out folders shallow, `info` and `pack` deep
|
|
728
755
|
* without the hard-link test). A link, or a folder that can't be listed, is swapped. */
|
|
@@ -750,7 +777,10 @@ function pinned(dirs) {
|
|
|
750
777
|
swapped.push({ path, leadsTo: real });
|
|
751
778
|
return;
|
|
752
779
|
}
|
|
753
|
-
|
|
780
|
+
// Another checkout's own index, by exact place (siblingIndexes): held to being no link, but a second name is not
|
|
781
|
+
// counted. A tool in that checkout may hard-link it for a moment (Claude Code's security-guidance plugin does, to
|
|
782
|
+
// diff against a copy), and git never writes another checkout's index, nor any index in place.
|
|
783
|
+
if (held.isFile() && mode === "deep" && held.nlink > 1 && !siblings.has(path)) {
|
|
754
784
|
swapped.push({ path, leadsTo: null });
|
|
755
785
|
return;
|
|
756
786
|
}
|
|
@@ -860,6 +890,32 @@ function objectStores(common) {
|
|
|
860
890
|
visit(join(common, "modules"), 0);
|
|
861
891
|
return stores;
|
|
862
892
|
}
|
|
893
|
+
/**
|
|
894
|
+
* The index files of the repository's other checkouts, by exact place, enumerated before the walk: `index` directly in
|
|
895
|
+
* each worktree folder of the common `worktrees` that isn't this checkout's own git directory, and, for a linked
|
|
896
|
+
* checkout, the main checkout's `index` in the common folder. The walk still refuses any of them that is a link; it
|
|
897
|
+
* only doesn't count a second hard link to one (pinned). The view links only this checkout's own index, and git writes
|
|
898
|
+
* an index only by renaming a new file over it, so a hard link at another checkout's can't take a write of the plane's
|
|
899
|
+
* anywhere. Measured (Nov 2026): Claude Code's security-guidance plugin hard-links the index of the checkout a session
|
|
900
|
+
* runs in to a temporary copy for up to seconds at a time, and a release gate that ran meanwhile in another checkout of
|
|
901
|
+
* the repository was stopped as an incident naming that checkout's index.
|
|
902
|
+
*/
|
|
903
|
+
function siblingIndexes(dirs) {
|
|
904
|
+
const indexes = new Set();
|
|
905
|
+
if (dirs.linked)
|
|
906
|
+
indexes.add(join(dirs.common, "index"));
|
|
907
|
+
let ids = [];
|
|
908
|
+
try {
|
|
909
|
+
ids = readdirSync(join(dirs.common, "worktrees"));
|
|
910
|
+
}
|
|
911
|
+
catch { /* none (an unlistable folder is the walk's to refuse) */ }
|
|
912
|
+
for (const id of ids) {
|
|
913
|
+
const folder = join(dirs.common, "worktrees", id);
|
|
914
|
+
if (folder !== dirs.gitdir)
|
|
915
|
+
indexes.add(join(folder, "index"));
|
|
916
|
+
}
|
|
917
|
+
return indexes;
|
|
918
|
+
}
|
|
863
919
|
function anchorOf(path) {
|
|
864
920
|
const anchor = [];
|
|
865
921
|
for (let at = path;; at = dirname(at)) {
|
|
@@ -1110,7 +1166,7 @@ export function checkoutGit(inner = run, mode = "build", binding = {}) {
|
|
|
1110
1166
|
const once = async () => {
|
|
1111
1167
|
let view;
|
|
1112
1168
|
try {
|
|
1113
|
-
view = gitView(cwd, mode === "read" ? "read" : "write", binding);
|
|
1169
|
+
view = gitView(cwd, mode === "read" ? "read" : "write", binding, "git", undefined, checkoutDoing(args));
|
|
1114
1170
|
}
|
|
1115
1171
|
catch (error) {
|
|
1116
1172
|
return refused(error instanceof Error ? error.message : "the checkout couldn't be read");
|
package/dist/telegram-lead.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { telegramCardMessage } from "./card-render.js";
|
|
2
|
+
import { cardOfProposal, cardPlainOf, cardsFirst, cardWithin, isCardProposal } from "./chat-card.js";
|
|
1
3
|
import { CHAT_ACTIONS, sharedActionAllowsChallenge, sharedActionPayload } from "./chat-actions.js";
|
|
2
|
-
import { taskInCeiling, mirrorToTaskChat, couldNotAnswerText, channelRepos as telegramConversationRepos, resolveChannelLead as resolveTelegramLead, NO_PHONE_LINK, NO_TASK_LINK, phoneProposalLink, confirmedLink, phoneProposalPreview, proposalOutcomeText, handoffCardText, confirmedCardText } from "./chat-channel.js";
|
|
4
|
+
import { armedQuestion, taskInCeiling, mirrorToTaskChat, couldNotAnswerText, channelRepos as telegramConversationRepos, resolveChannelLead as resolveTelegramLead, NO_PHONE_LINK, NO_TASK_LINK, phoneProposalLink, confirmedLink, phoneProposalPreview, proposalOutcomeText, handoffCardText, confirmedCardText } from "./chat-channel.js";
|
|
3
5
|
export { channelRepos as telegramConversationRepos, resolveChannelLead as resolveTelegramLead, tooLongText, whichTaskText, replyContextFor, NO_PHONE_LINK, NO_TASK_LINK, proposalLink, confirmedLink, proposalPreview, proposalOutcomeText, handoffCardText, confirmedCardText, CHAT_ACTION_PARITY as TELEGRAM_ACTION_PARITY, parityGaps } from "./chat-channel.js";
|
|
4
6
|
/**
|
|
5
7
|
* The paired phone as one more way to use the same assistant.
|
|
@@ -260,7 +262,7 @@ export function applyProposalTap(store, binding, token, message, repos, options,
|
|
|
260
262
|
const body = preview.text.split("\n\nConfirm or Dismiss below")[0];
|
|
261
263
|
if (challenged) {
|
|
262
264
|
ack("confirm it");
|
|
263
|
-
edit(`${body}\n\
|
|
265
|
+
edit(`${body}\n\n${armedQuestion(proposal)}`, [
|
|
264
266
|
[telegramButton(`✓ Yes, ${CHAT_ACTIONS[sharedAction.operation].label.toLowerCase()}`, yes)],
|
|
265
267
|
[telegramButton("Cancel", cancel)],
|
|
266
268
|
]);
|
|
@@ -288,6 +290,7 @@ export function applyProposalTap(store, binding, token, message, repos, options,
|
|
|
288
290
|
// The paired phone speaks for its account's full authority (pairing is a full session's act).
|
|
289
291
|
authority: "full",
|
|
290
292
|
via: "telegram",
|
|
293
|
+
binding: binding.id,
|
|
291
294
|
evidenceRoot: options.evidenceRoot,
|
|
292
295
|
confirm: action.phase === "yes",
|
|
293
296
|
deferSignal: signal => effects.push({ kind: "signal", run: signal }),
|
|
@@ -549,17 +552,26 @@ async function runTelegramConversation(row, args) {
|
|
|
549
552
|
const origin = proposal === null ? null : options.phoneOrigin?.() ?? null;
|
|
550
553
|
const wanted = proposal === null ? null : phoneProposalLink(store, proposal, repos);
|
|
551
554
|
const keyboard = keyboardWith(part.keyboard, phoneLinkButton(origin, wanted));
|
|
555
|
+
// A Toolroll card: read again now, shown only where every project behind it is still reachable. An unreadable
|
|
556
|
+
// one goes out as its saved plain words.
|
|
557
|
+
const isCard = proposal !== null && isCardProposal(proposal);
|
|
558
|
+
const rich = isCard ? cardOfProposal(proposal) : null;
|
|
559
|
+
const shown = rich !== null && cardWithin(rich, repos) ? telegramCardMessage(rich) : null;
|
|
560
|
+
const links = rich === null ? [] : rich.buttons.flatMap(one => phoneLinkButton(origin, one) ?? []).map(one => [one]);
|
|
552
561
|
// The lead's own words carry their bold anchors and labelled links as entities, never as Markdown characters.
|
|
553
562
|
const voiced = part.kind === "reply" ? telegramReply(part.text) : null;
|
|
554
|
-
const text =
|
|
563
|
+
const text = isCard ? shown?.text ?? (rich === null ? part.text : "This card's projects changed, so it wasn't sent.")
|
|
564
|
+
: proposal !== null && part.keyboard === null ? handoffCardText(part.text, linkNote(origin, wanted)) : voiced?.text ?? part.text;
|
|
565
|
+
const entities = isCard ? shown?.entities ?? [] : voiced?.entities ?? [];
|
|
566
|
+
const buttons = isCard ? (shown === null || links.length === 0 ? undefined : links) : keyboard;
|
|
555
567
|
method = "sendMessage";
|
|
556
568
|
send = () => transport("sendMessage", {
|
|
557
569
|
chat_id: chatId,
|
|
558
570
|
text,
|
|
559
|
-
...(
|
|
571
|
+
...(entities.length === 0 ? {} : { entities }),
|
|
560
572
|
link_preview_options: { is_disabled: true },
|
|
561
573
|
...(part.replyTo === null ? {} : { reply_parameters: { message_id: Number(part.replyTo) } }),
|
|
562
|
-
...(
|
|
574
|
+
...(buttons === undefined ? {} : { reply_markup: { inline_keyboard: buttons } }),
|
|
563
575
|
});
|
|
564
576
|
}
|
|
565
577
|
// The first time the answer itself fails to go out, the person hears so once,
|
|
@@ -626,7 +638,12 @@ async function runTelegramConversation(row, args) {
|
|
|
626
638
|
const selected = store.getLeadTurn(turn)?.state === "answered" ? store.listLeadTurnEvidence(turn) : [];
|
|
627
639
|
for (const image of selected)
|
|
628
640
|
parts.push({ kind: "image", text: image.caption, taskId: image.taskId, run: image.run, artifact: image.artifact, sha256: image.sha256 });
|
|
629
|
-
|
|
641
|
+
// Cards first, so what a card leads to sits right under it. A card mints no tokens: it confirms nothing.
|
|
642
|
+
for (const proposal of cardsFirst(proposals)) {
|
|
643
|
+
if (isCardProposal(proposal)) {
|
|
644
|
+
parts.push({ kind: "card", text: cardPlainOf(proposal), proposal: proposal.id, keyboard: null });
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
630
647
|
const preview = phoneProposalPreview(store, proposal, repos);
|
|
631
648
|
const keyboard = preview.buttons ? mintCardTokens(store, binding, proposal.id, now).keyboard : null;
|
|
632
649
|
parts.push({ kind: "card", text: preview.text, proposal: proposal.id, keyboard });
|
package/dist/telegram.js
CHANGED
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
* BotFather token plus `bridge telegram unpair` is the recovery, and both
|
|
19
19
|
* are one command.
|
|
20
20
|
*/
|
|
21
|
+
import { telegramCardMessage } from "./card-render.js";
|
|
22
|
+
import { flowDoneCard } from "./flow-result-card.js";
|
|
21
23
|
import { applyTelegramQuestionReply, applyTelegramQuestionTap, openQuestionOf, telegramQuestionButtons } from "./subagent-question.js";
|
|
22
24
|
import { acceptanceEvidenceText } from "./chat-acceptance.js";
|
|
23
25
|
import { verifyApproverStanding } from "./principal.js";
|
|
@@ -1288,6 +1290,15 @@ async function deliverOne(store, botId, binding, sender, notification, clock, ph
|
|
|
1288
1290
|
parts = voiced.map(one => one.text);
|
|
1289
1291
|
entities = voiced.map(one => one.entities);
|
|
1290
1292
|
}
|
|
1293
|
+
// A flow's result as a card (flow-result-card.ts): one more message, its tables as monospaced text.
|
|
1294
|
+
const resultCard = offer === null ? sent?.card ?? flowDoneCard(store, notification, clock()) : null;
|
|
1295
|
+
if (resultCard != null) {
|
|
1296
|
+
const message = telegramCardMessage(resultCard);
|
|
1297
|
+
entities = [...parts.map((_, at) => entities[at] ?? []), message.entities];
|
|
1298
|
+
parts = [...parts, message.text];
|
|
1299
|
+
}
|
|
1300
|
+
// Its links stay on the words: the card follows them.
|
|
1301
|
+
const keyedPart = parts.length - (resultCard != null ? 2 : 1);
|
|
1291
1302
|
// A subagent's question (v93): its options and "Answer in words", for the person it asks.
|
|
1292
1303
|
const asked = flowKeys === null && choiceKeys === null ? openQuestionOf(store, notification.dedupeKey) : null;
|
|
1293
1304
|
const questionKeys = asked === null ? null : telegramQuestionButtons(store, binding, asked, clock());
|
|
@@ -1301,7 +1312,7 @@ async function deliverOne(store, botId, binding, sender, notification, clock, ph
|
|
|
1301
1312
|
}
|
|
1302
1313
|
}
|
|
1303
1314
|
for (const [index, part] of parts.entries()) {
|
|
1304
|
-
const final = index ===
|
|
1315
|
+
const final = index === keyedPart;
|
|
1305
1316
|
const decideKeys = decided === null ? [] : decideKeyboard(phoneOrigin, decided.rows);
|
|
1306
1317
|
const keyboard = !final ? undefined : keys !== null ? [...keys.keyboard, ...(button === null ? [] : [button])] : decideKeys.length > 0 ? decideKeys : button !== null ? [button] : undefined;
|
|
1307
1318
|
const sent = await sender(part, keyboard, undefined, entities[index]?.length ? entities[index] : undefined);
|
package/dist/toolroll-update.js
CHANGED
|
@@ -427,6 +427,8 @@ const HISTORY_RULES = {
|
|
|
427
427
|
// v152: a saved project's code map reads on, with no enrollment yet; the code map's tables are new.
|
|
428
428
|
// v153: a saved task reads no fast start and a saved project no fast-start setting (NULL); its tables are new.
|
|
429
429
|
// v154 adds a table only (check_stamp): no saved row changes, so no rule is needed for it.
|
|
430
|
+
// v155 adds tables only (fleet-schema.ts): no saved row changes, so no rule is needed for it.
|
|
431
|
+
// v156 adds tables only (fallback chains): likewise.
|
|
430
432
|
...Object.fromEntries(Object.entries(addedColumns(V119_ADDED_COLUMNS, V120_ADDED_COLUMNS, V121_ADDED_COLUMNS, V122_ADDED_COLUMNS, V123_ADDED_COLUMNS, V125_ADDED_COLUMNS, V126_ADDED_COLUMNS, V128_ADDED_COLUMNS, V129_ADDED_COLUMNS, V134_ADDED_COLUMNS, V135_ADDED_COLUMNS, V138_ADDED_COLUMNS, V143_ADDED_COLUMNS, V147_ADDED_COLUMNS, V148_ADDED_COLUMNS, V150_ADDED_COLUMNS, V151_ADDED_COLUMNS, V152_ADDED_COLUMNS, V153_ADDED_COLUMNS))
|
|
431
433
|
.map(([table, rule]) => [table, { ...EARLIER_RULES[table], ...rule }])),
|
|
432
434
|
};
|
package/dist/work-index.js
CHANGED
|
@@ -19,7 +19,7 @@ import { isCheckLevel, requiredCheckCommandFor } from './check-levels.js';
|
|
|
19
19
|
import { MARKER } from './worktree.js';
|
|
20
20
|
import { withFollowUps } from './result-follow-ups.js';
|
|
21
21
|
import { LANDED_ACTOR, landedReasonOf } from './result-completion.js';
|
|
22
|
-
import { ASKS, CODE_READING, NEED_ASK, NEEDS, askChipOf, failedAttemptSentence, isWaitCode, processNeedOf, resultHoldUpSentence } from './needs-you.js';
|
|
22
|
+
import { ASKS, CODE_READING, STORAGE_HOLD_PREFIX, NEED_ASK, NEEDS, askChipOf, failedAttemptSentence, isWaitCode, processNeedOf, resultHoldUpSentence } from './needs-you.js';
|
|
23
23
|
import { retryWhenWords } from './dispatch.js';
|
|
24
24
|
import { LEAD_CLAIM_SQL, LEAD_IDLE_MS } from './lead-voice.js';
|
|
25
25
|
export const WORK_INDEX_PAGE_LIMIT = 40;
|
|
@@ -45,9 +45,11 @@ const GATE_PAUSE_ON_TASK = pauseOnTaskProviders('p', 'c.ref_id');
|
|
|
45
45
|
const INDEX_NEED = Object.fromEntries(['earlier-active', 'process-needs-attention', 'invalid-scope', 'decision-queue', 'history-problem']
|
|
46
46
|
.map(code => [code, CODE_READING[code].need]));
|
|
47
47
|
/** Every index code that can wait on a person. A wait (stopping, waiting its turn, a retry later) is never one. */
|
|
48
|
-
const PERSON_CODES = ['history-problem', 'earlier-active', 'ready-to-check', 'waiting-decision', 'decision-queue', 'result-needs-attention',
|
|
48
|
+
const PERSON_CODES = ['finding-held', 'history-problem', 'earlier-active', 'ready-to-check', 'waiting-decision', 'decision-queue', 'result-needs-attention',
|
|
49
49
|
'process-needs-attention', 'failed', 'vanished-run', 'stopped', 'held', 'waiting-incident', 'terminal-dependency', 'needs-project',
|
|
50
|
-
'needs-scope', 'invalid-scope', 'needs-agent-profile', 'needs-approval', 'missing-requirement', 'no-worker-registered', 'no-worker-online', 'signed-out'];
|
|
50
|
+
'needs-scope', 'invalid-scope', 'needs-agent-profile', 'needs-approval', 'missing-requirement', 'no-worker-registered', 'no-worker-online', 'signed-out', 'storage-blocked'];
|
|
51
|
+
/** A family's findings held for a person (findings.ts). A database not yet at v155 has no such table: see registerValidators. */
|
|
52
|
+
const HELD_FINDINGS = `(SELECT COUNT(*) FROM finding fd WHERE fd.family_root=root.id AND fd.state='with-person')`;
|
|
51
53
|
/** The index codes that wait on nobody: finished or running work, and every wait (needs-you.ts CODE_READING). */
|
|
52
54
|
export const WAITING_CODES = ['cancelled', 'complete', 'running', 'queued',
|
|
53
55
|
...['stopping', 'updating', 'retry-scheduled', 'waiting-dependency', 'worker-at-capacity', 'planning-ready', 'scouting-ready'].filter(code => isWaitCode(code))];
|
|
@@ -179,6 +181,7 @@ const PROJECTION = `WITH RECURSIVE admitted AS MATERIALIZED (
|
|
|
179
181
|
(SELECT run FROM run_stop WHERE task_ref=c.ref_id AND resumed_at IS NULL ORDER BY requested_at DESC LIMIT 1) stop_run,
|
|
180
182
|
(SELECT settled_at FROM run_stop WHERE task_ref=c.ref_id AND resumed_at IS NULL ORDER BY requested_at DESC LIMIT 1) stop_settled,
|
|
181
183
|
COALESCE((SELECT substr(actor,13) FROM action_ledger WHERE task_id=root.id AND action='assignment claimed' AND source='work' ORDER BY id DESC LIMIT 1),root.coordinator_cid) owner_id,
|
|
184
|
+
${HELD_FINDINGS} held_findings,
|
|
182
185
|
pub.pr_url publication_url
|
|
183
186
|
FROM ordered c JOIN families f USING(root_ref) JOIN admitted root ON root.ref_id=c.root_ref
|
|
184
187
|
LEFT JOIN run result INDEXED BY work_result ON result.task_ref=c.ref_id AND result.id=c.result_id
|
|
@@ -216,6 +219,7 @@ const PROJECTION = `WITH RECURSIVE admitted AS MATERIALIZED (
|
|
|
216
219
|
WHEN state='cancelled' THEN 'cancelled'
|
|
217
220
|
WHEN earlier_active>0 AND state='done' THEN 'earlier-active'
|
|
218
221
|
WHEN stop_run IS NOT NULL THEN CASE WHEN stop_settled IS NULL THEN 'stopping' ELSE 'stopped' END
|
|
222
|
+
WHEN held_findings>0 AND state<>'failed' THEN 'finding-held'
|
|
219
223
|
WHEN ready THEN CASE WHEN checked_id IS NULL THEN 'ready-to-check' ELSE 'complete' END
|
|
220
224
|
WHEN state='done' AND question_id IS NOT NULL AND hold_id IS NULL THEN 'waiting-decision'
|
|
221
225
|
WHEN state='done' THEN 'result-needs-attention'
|
|
@@ -224,6 +228,7 @@ const PROJECTION = `WITH RECURSIVE admitted AS MATERIALIZED (
|
|
|
224
228
|
WHEN live_lease IS NOT NULL THEN CASE WHEN question_id IS NOT NULL OR agent_ask IS NOT NULL THEN 'waiting-decision' ELSE 'running' END
|
|
225
229
|
WHEN state='running' OR unfinished IS NOT NULL THEN 'vanished-run'
|
|
226
230
|
WHEN EXISTS(SELECT 1 FROM sqlite_master WHERE type='trigger' AND name GLOB 'so_desktop_update_*') THEN 'updating'
|
|
231
|
+
WHEN hold_kind='backoff' AND substr(hold_reason,1,${STORAGE_HOLD_PREFIX.length})='${STORAGE_HOLD_PREFIX}' THEN 'storage-blocked'
|
|
227
232
|
WHEN hold_kind='backoff' THEN 'retry-scheduled'
|
|
228
233
|
WHEN hold_kind='decision' THEN 'waiting-decision'
|
|
229
234
|
WHEN hold_kind='incident' THEN 'waiting-incident'
|
|
@@ -304,8 +309,11 @@ function registerValidators(store) {
|
|
|
304
309
|
const asks = db.prepare("SELECT 1 AS hit FROM sqlite_master WHERE type='table' AND name='agent_question'").get() !== undefined;
|
|
305
310
|
const projection = asks ? indexed : indexed.replace(AGENT_ASK, 'NULL')
|
|
306
311
|
.replace(`'agent_ask_run',(SELECT run FROM agent_question WHERE id=page.agent_ask),'agent_ask_text',(SELECT substr(question,1,300) FROM agent_question WHERE id=page.agent_ask),`, '');
|
|
307
|
-
|
|
308
|
-
|
|
312
|
+
// Nor, before v155, any held findings: readingFleet's rule for a list read.
|
|
313
|
+
const findings = db.prepare("SELECT 1 AS hit FROM sqlite_master WHERE type='table' AND name='finding'").get() !== undefined;
|
|
314
|
+
const read = findings ? projection : projection.replace(HELD_FINDINGS, '0');
|
|
315
|
+
registered.set(db, read);
|
|
316
|
+
return read;
|
|
309
317
|
}
|
|
310
318
|
/** The list can't see processes: each task whose build Toolroll can't yet
|
|
311
319
|
* confirm stopped, as the store reads it now (the task page's own reading).
|
|
@@ -430,7 +438,7 @@ function readWorkIndexPage(store, now, access, options, taskId = null) {
|
|
|
430
438
|
'proof_verdict',(SELECT COALESCE(machine_verdict,verdict) FROM proof_verdict WHERE run=page_result_id),
|
|
431
439
|
'proof_failed_check',(SELECT verdict='refuted' AND reasons_json LIKE '%approved verification command exited%' FROM proof_verdict WHERE run=page_result_id),'code',code,'needs',needs,'person_need',person_need,'lead_claim',lead_claim,'family_running',family_running,'rank',rank,'ask_rank',ask_rank,'list_order',${ORDER},'sort_at',sort_at,
|
|
432
440
|
'checked_actor',checked_actor,'checked_at',checked_at,'checked_digest',checked_digest,'checked_detail',checked_detail,'question_id',question_id,
|
|
433
|
-
'dependency_id',dependency_id,'dependency_state',dependency_state,'hold_until',hold_until,'attempt_run',(SELECT id FROM run WHERE task_ref IN (SELECT ref_id FROM members WHERE root_ref=page.root_ref) AND (finished_at IS NOT NULL OR outcome IS NOT NULL) AND role NOT IN ('planner','reviewer') ORDER BY id DESC LIMIT 1),'custody_unresolved',custody_unresolved,'hold_kind',hold_kind,'hold_reason',substr(hold_reason,1,
|
|
441
|
+
'dependency_id',dependency_id,'dependency_state',dependency_state,'hold_until',hold_until,'attempt_run',(SELECT id FROM run WHERE task_ref IN (SELECT ref_id FROM members WHERE root_ref=page.root_ref) AND (finished_at IS NOT NULL OR outcome IS NOT NULL) AND role NOT IN ('planner','reviewer') ORDER BY id DESC LIMIT 1),'custody_unresolved',custody_unresolved,'hold_kind',hold_kind,'hold_reason',substr(hold_reason,1,600),'hold_id',hold_id,'live_lease',live_lease IS NOT NULL,'approved',COALESCE(approved,0),
|
|
434
442
|
'plan_changed',scope_digest IS NOT NULL AND digest IS NOT scope_digest,'head_ok',length(head_revision)=40 AND head_revision NOT GLOB '*[^a-f0-9]*','stop_run',stop_run,'unfinished',unfinished,'signed_out',signed_out,
|
|
435
443
|
'question_run',(SELECT run FROM decision WHERE id=page.question_id),'agent_ask',page.agent_ask,
|
|
436
444
|
'agent_ask_run',(SELECT run FROM agent_question WHERE id=page.agent_ask),'agent_ask_text',(SELECT substr(question,1,300) FROM agent_question WHERE id=page.agent_ask),'replaced_by',(SELECT successor FROM task_replacement WHERE task_ref=page.ref_id),
|
|
@@ -612,6 +620,13 @@ function itemOf(row, principal, probe, leadName, readiness, readable = false, no
|
|
|
612
620
|
}
|
|
613
621
|
if (code === 'stopped')
|
|
614
622
|
detail = 'Paused by a person. The work so far is kept.';
|
|
623
|
+
// The storage check couldn't finish (dispose.ts holdForStorage): its own words, when it retries, and Try again.
|
|
624
|
+
if (code === 'storage-blocked') {
|
|
625
|
+
const until = s(row, 'hold_until');
|
|
626
|
+
detail = `${(s(row, 'hold_reason') ?? '').slice(STORAGE_HOLD_PREFIX.length)}${until === null ? '' : ` It tries again on its own ${retryWhenWords(until, now)}.`}`;
|
|
627
|
+
actionCode = 'unhold';
|
|
628
|
+
actionLabel = NEEDS.storage.action.label;
|
|
629
|
+
}
|
|
615
630
|
if (code === 'cancelled')
|
|
616
631
|
detail = 'This task was cancelled.';
|
|
617
632
|
// The same failure again and again: what failed, and the two ways on.
|
|
@@ -671,7 +686,7 @@ function itemOf(row, principal, probe, leadName, readiness, readable = false, no
|
|
|
671
686
|
actionCode = code === 'running' ? 'inspect-run' : 'inspect-task';
|
|
672
687
|
actionLabel = code === 'running' ? 'Watch the build' : 'View task details';
|
|
673
688
|
}
|
|
674
|
-
const read = actionCode !== null && ['inspect-task', 'inspect-run', 'inspect-stop', 'inspect-decisions', 'open-result'].includes(actionCode);
|
|
689
|
+
const read = actionCode !== null && ['inspect-task', 'inspect-findings', 'inspect-run', 'inspect-stop', 'inspect-decisions', 'open-result'].includes(actionCode);
|
|
675
690
|
const primaryAction = actionCode === null ? null : { code: actionCode, label: actionLabel,
|
|
676
691
|
target: { taskId: successor ?? (code === 'earlier-active' ? s(row, 'earlier_id') ?? id : actionCode === 'answer-decision' ? s(row, 'question_task') ?? id : id),
|
|
677
692
|
runId: actionCode === 'answer-decision' ? n(row, 'question_run') || null : actionCode === 'answer-agent' ? n(row, 'agent_ask_run') || null : actionCode === 'inspect-run' && code === 'running' ? n(row, 'live_run') || null : actionCode === 'open-result' || actionCode === 'inspect-run' ? n(row, 'result_id') || null
|
package/dist/work-summary.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export type WorkTarget = {
|
|
|
10
10
|
export type WorkAction = {
|
|
11
11
|
/** `recheck-stopped`: Toolroll looks again whether a finished build stopped (nobody vouches for it);
|
|
12
12
|
* `sign-in`: the agent's sign-in, in Settings; `answer-agent`: Reply to a running agent's question on the Agents board. */
|
|
13
|
-
code: DispatchAction | "inspect-task" | "inspect-run" | "inspect-stop" | "inspect-decisions" | "reconcile-run" | "open-pr" | "recheck-stopped" | "sign-in" | "answer-agent";
|
|
13
|
+
code: DispatchAction | "inspect-task" | "inspect-findings" | "inspect-run" | "inspect-stop" | "inspect-decisions" | "reconcile-run" | "open-pr" | "recheck-stopped" | "sign-in" | "answer-agent";
|
|
14
14
|
label: string;
|
|
15
15
|
target: WorkTarget;
|
|
16
16
|
access: "read" | "operator-control" | "proposal-only" | "operator-handoff";
|
package/dist/work-summary.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* State, decisions, approval, process custody and proof still belong to
|
|
3
3
|
* their existing owners. Action hints are navigation/proposal guidance,
|
|
4
4
|
* never credentials or permission to bypass an owning operation. */
|
|
5
|
+
import { heldFindingCount } from "./findings.js";
|
|
5
6
|
import { machineChecked } from "./machine-check.js";
|
|
6
7
|
import { resultCheckLevel } from "./result-follow-ups.js";
|
|
7
8
|
import { diagnoseTaskDispatch } from "./dispatch.js";
|
|
@@ -28,7 +29,7 @@ export function workDecisionAction(taskId, decision, principal) {
|
|
|
28
29
|
access: principal === "operator" ? "operator-control" : "proposal-only", retry: "refresh-before-acting" };
|
|
29
30
|
}
|
|
30
31
|
const PROPOSABLE = new Set(["answer-decision", "write-scope", "unhold"]);
|
|
31
|
-
const READ_ACTIONS = new Set(["open-result", "open-pr", "inspect-task", "inspect-run", "inspect-stop", "inspect-decisions"]);
|
|
32
|
+
const READ_ACTIONS = new Set(["open-result", "open-pr", "inspect-task", "inspect-findings", "inspect-run", "inspect-stop", "inspect-decisions"]);
|
|
32
33
|
/** Pure counterpart for callers already holding the same facts. */
|
|
33
34
|
export function workSummaryOf(facts, principal, resultDisplay) {
|
|
34
35
|
const status = workStatusOf(facts, resultDisplay);
|
|
@@ -48,6 +49,8 @@ export function workSummaryOf(facts, principal, resultDisplay) {
|
|
|
48
49
|
primary = make("resume-run", status.action.label, { runId: facts.control.run });
|
|
49
50
|
else if (status.token === "waiting-decision" && questionAction !== null)
|
|
50
51
|
primary = questionAction;
|
|
52
|
+
else if (status.token === "finding-held")
|
|
53
|
+
primary = make("inspect-findings", status.action.label);
|
|
51
54
|
else if (facts.dispatch?.code === "vanished-run")
|
|
52
55
|
primary = make("reconcile-run", "Check the unfinished attempt", { runId: facts.unfinishedRunId ?? null });
|
|
53
56
|
else if (status.action.kind === "open-result" || status.action.kind === "open-review")
|
|
@@ -98,7 +101,7 @@ export function taskWorkSummaryOf(store, taskId, now, access) {
|
|
|
98
101
|
...(machineChecked(store, task.id) ? { machine: true } : {}),
|
|
99
102
|
dispatch: diagnoseTaskDispatch(store, taskId, now, access.dependencyRepos ?? null), liveRunId: live?.id ?? null,
|
|
100
103
|
unfinishedRunId: runs.find(one => one.outcome === null && one.role !== "reviewer")?.id ?? null,
|
|
101
|
-
control: taskControlOf(store, ref.id, now), openDecision: openWorkDecisionOf(store, ref.id, now),
|
|
104
|
+
control: taskControlOf(store, ref.id, now), openDecision: openWorkDecisionOf(store, ref.id, now), heldFindings: heldFindingCount(store, taskId),
|
|
102
105
|
result: result === null ? null : {
|
|
103
106
|
runId: result.id, role: result.role, outcome: result.outcome,
|
|
104
107
|
verdict: proof?.verdict ?? null, reasons: proof?.reasons ?? [], accepted: store.proofAcceptance(result.id) !== null,
|