toolroll 0.9.103 → 0.9.104

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.
Files changed (47) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +10 -8
  3. package/dist/assignment.d.ts +32 -0
  4. package/dist/assignment.js +140 -4
  5. package/dist/browser/workspace.js +25 -25
  6. package/dist/browser-workspace.d.ts +13 -0
  7. package/dist/contracts/console-api.d.ts +3 -2
  8. package/dist/contracts/console-api.js +4 -2
  9. package/dist/desktop-update.d.ts +1 -1
  10. package/dist/keymap.d.ts +3 -1
  11. package/dist/keymap.js +3 -0
  12. package/dist/local-owner-acts.d.ts +83 -0
  13. package/dist/local-owner-acts.js +221 -0
  14. package/dist/local-owner.d.ts +12 -1
  15. package/dist/local-owner.js +26 -0
  16. package/dist/operate-remote-arguments.js +4 -2
  17. package/dist/operate.js +76 -1
  18. package/dist/plan-auto.js +5 -0
  19. package/dist/project-knowledge.d.ts +3 -3
  20. package/dist/public-access.d.ts +9 -0
  21. package/dist/public-access.js +15 -0
  22. package/dist/pull-request-flow.js +9 -0
  23. package/dist/release-card.d.ts +15 -1
  24. package/dist/release-card.js +35 -12
  25. package/dist/result-completion.d.ts +6 -2
  26. package/dist/result-completion.js +13 -7
  27. package/dist/scope.d.ts +32 -1
  28. package/dist/scope.js +110 -25
  29. package/dist/serve.js +13 -14
  30. package/dist/server/guards.d.ts +1 -0
  31. package/dist/server/guards.js +11 -1
  32. package/dist/server/remote.d.ts +1 -0
  33. package/dist/server/render-results.d.ts +4 -0
  34. package/dist/server/render-tasks.d.ts +9 -0
  35. package/dist/server/render-tasks.js +4 -4
  36. package/dist/server/route-table.js +4 -0
  37. package/dist/server/runtime.d.ts +1 -0
  38. package/dist/server/task-views.js +3 -0
  39. package/dist/server/tasks.d.ts +25 -0
  40. package/dist/server/tasks.js +46 -6
  41. package/dist/surface.js +12 -2
  42. package/dist/task-outcome-cli.d.ts +18 -0
  43. package/dist/task-outcome-cli.js +60 -2
  44. package/dist/tui-contract.d.ts +61 -3
  45. package/dist/tui-source.js +91 -20
  46. package/dist/tui.js +54 -54
  47. package/package.json +2 -2
@@ -8,7 +8,9 @@
8
8
  * nothing ever prompts on the terminal the screen owns.
9
9
  */
10
10
  import { dirname, join } from "node:path";
11
- import { assignmentOf } from "./assignment.js";
11
+ import { assignmentOf, owedAcceptance } from "./assignment.js";
12
+ import { familyChangedFiles } from "./result-completion.js";
13
+ import { checkBackingOf, receiptChecksOf, requirementWordOf } from "./task-status.js";
12
14
  import { checklistLines, mergeChecklistOf } from "./merge-checklist.js";
13
15
  import { runAsLocalOwner } from "./local-owner.js";
14
16
  import { snapshotLiveRuns } from "./peek-cli.js";
@@ -21,14 +23,18 @@ import { memoryWaiting } from "./lesson-notes.js";
21
23
  import { homeGroupOf } from "./home-groups.js";
22
24
  import { terminalText } from "./terminal-text.js";
23
25
  import { welcomeHere, welcomeLines } from "./welcome-facts.js";
26
+ import { planSourceOf, retryPinsOf } from "./local-owner-acts.js";
24
27
  const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
25
28
  const words = (value, fallback = "") => typeof value === "string" ? value : fallback;
26
29
  /** Text for the screen: whatever wrote it (an agent, a person, a project's name), escapes and controls never reach the
27
30
  * terminal (terminal-text.ts). Every displayed string below goes through here; ids and digests are only acted on. */
28
31
  const shown = (value, fallback = "") => terminalText(words(value, fallback));
29
- /** What a refused write says, and whether it is a local-owner refusal (the screen then leads to a password). */
32
+ /** What a refused write says, and whether it is a local-owner refusal (the screen then leads to a password), or a
33
+ * result that changed since the screen read it. */
30
34
  function refusalOf(envelope) {
31
35
  const message = shown(envelope.message, "That was refused.");
36
+ if (envelope.reason === "stale")
37
+ return { ok: false, message, needsPassword: false, changed: true };
32
38
  return { ok: false, message, needsPassword: envelope.reason === "refused" || envelope.reason === "machine-proof" || envelope.reason === "person-required" || /password/i.test(message) };
33
39
  }
34
40
  const routeLines = (store, id) => { const words = routeChangeOf(store, id); return words === null ? [] : [words]; };
@@ -116,6 +122,8 @@ export function createTuiSource(options) {
116
122
  return {
117
123
  id, title: shown(task["title"], id), status: shown(status["label"], words(work["state"])), detail: shown(status["detail"]),
118
124
  plan, liveRunId: typeof work["liveRunId"] === "number" ? work["liveRunId"] : null, hasResult, decisions, ready,
125
+ planAsk: store === null ? null : planAskOf(store, id),
126
+ retry: (() => { const pins = store === null ? null : retryPinsOf(store, id); return pins === null ? null : { runId: pins.run, digest: pins.digest }; })(),
119
127
  };
120
128
  },
121
129
  live(id) {
@@ -141,9 +149,14 @@ export function createTuiSource(options) {
141
149
  let argv;
142
150
  let done;
143
151
  switch (act.kind) {
152
+ // A task too big to build straight away: filed, then its plan asked for at once (local-owner slice 2), below.
144
153
  case "new":
145
154
  argv = ["task", "add", act.text, ...(act.repo === null ? [] : ["--repo", act.repo])];
146
- done = "Filed. Its plan will be ready for you to approve.";
155
+ done = "Filed.";
156
+ break;
157
+ case "plan":
158
+ argv = ["task", "plan", act.id, "--digest", act.source];
159
+ done = "Asked for a plan. You'll approve it before anything is built.";
147
160
  break;
148
161
  case "build":
149
162
  argv = ["task", "new", "--build", act.preview, "--digest", act.digest];
@@ -165,33 +178,49 @@ export function createTuiSource(options) {
165
178
  argv = ["decide", String(act.decision), "--choose", act.option];
166
179
  done = "Answered.";
167
180
  break;
168
- // Retry is today's requeue (the console's Retry), the same command and the same refusals.
181
+ // Retry: exactly the build and approved plan the screen read (local-owner slice 3), never the current ones.
169
182
  case "retry":
170
- argv = ["task", "retry", act.id];
171
- done = "Queued again. A builder starts it next.";
183
+ argv = ["task", "requeue", act.id, "--run", String(act.runId), "--digest", act.digest];
184
+ done = "Retrying. A builder starts it next.";
172
185
  break;
173
- case "complete": {
174
- const store = read();
175
- let digest;
176
- try {
177
- digest = store === null ? undefined : assignmentOf(store, act.id, clock(), { principal: "operator", repos: null })?.receipt?.digest;
178
- }
179
- catch {
180
- digest = undefined;
181
- }
182
- if (digest === undefined)
183
- return { ok: false, message: "This task has no finished result to accept.", needsPassword: false };
184
- argv = ["task", "complete", act.id, "--digest", digest];
185
- done = "Accepted.";
186
+ // Accept: exactly the result the Accept screen read, never the one current when this is sent (the command
187
+ // refuses a result that changed meanwhile). The person's yes to their own check, or a reason, only when given.
188
+ case "complete":
189
+ argv = ["task", "complete", act.id, "--run", String(act.runId), "--digest", act.digest, ...(act.checked ? ["--checked"] : []), ...(act.reason === null ? [] : ["--reason", act.reason])];
190
+ done = "Accepted. The task is done.";
186
191
  break;
187
- }
188
192
  }
189
193
  const answer = await command(argv);
190
194
  if (answer.ok !== true)
191
195
  return refusalOf(answer);
196
+ if (act.kind === "complete" && answer["replayed"] === true)
197
+ return { ok: true, message: "Already accepted. The task is done." };
198
+ if (act.kind === "plan" && answer["replayed"] === true)
199
+ return { ok: true, message: "Already asked for a plan." };
200
+ if (act.kind === "retry" && answer["replayed"] === true)
201
+ return { ok: true, message: "Already retrying. A builder starts it next." };
192
202
  const filed = act.kind === "new" ? words(record(answer["task"])["id"], words(answer["id"])) : act.kind === "build" ? words(answer["task"]) : "";
203
+ // Filed in a project: ask for its plan now, bound to the task just filed (the screen promised a plan).
204
+ if (act.kind === "new" && filed !== "" && act.repo !== null) {
205
+ const store = read();
206
+ const ask = store === null ? null : planAskOf(store, filed);
207
+ const asked = ask === null ? null : await command(["task", "plan", filed, "--digest", ask.source]);
208
+ const message = asked?.ok === true ? "Filed. Planning: you'll approve the plan before anything is built." : `Filed. It wasn't sent for a plan: ${shown(asked?.message, "it can't be planned now.")}`;
209
+ return { ok: true, message, id: filed };
210
+ }
193
211
  return { ok: true, message: done, ...(filed === "" ? {} : { id: filed }) };
194
212
  },
213
+ async result(id) {
214
+ const store = read();
215
+ if (store === null)
216
+ return null;
217
+ try {
218
+ return resultOf(store, id, clock(), evidenceRoot);
219
+ }
220
+ catch {
221
+ return null;
222
+ }
223
+ },
195
224
  async fastTerms(text, repo) {
196
225
  if (repo === null)
197
226
  return null;
@@ -242,3 +271,45 @@ function learningLine(store, repos, now) {
242
271
  const parts = [flaky === 0 ? "" : `${flaky} flaky test${flaky === 1 ? "" : "s"}: open the project's Flaky tests in the browser`, memory === 0 ? "" : `${memory} project memory suggestion${memory === 1 ? "" : "s"}: open Settings → Knowledge`].filter(Boolean);
243
272
  return parts.length === 0 ? null : parts.join(" · ");
244
273
  }
274
+ /** The Accept screen's one read of a task's current result (local-owner slice 1): which build and receipt it pins, and
275
+ * what the result page would show about it — the agent's summary, the files changed, each requirement and how it is
276
+ * shown, the actual check status, and what accepting it needs from the person. Text from outside is cleaned here. */
277
+ function resultOf(store, id, now, evidenceRoot) {
278
+ const assignment = assignmentOf(store, id, now, { principal: "operator", repos: null }, evidenceRoot);
279
+ const receipt = assignment?.receipt ?? null;
280
+ if (assignment === null || receipt === null)
281
+ return null;
282
+ const blockers = assignment.readiness?.blockers ?? [];
283
+ const owed = assignment.state === "ready-to-check" ? owedAcceptance(receipt, blockers) : null;
284
+ // An exception's own blocker is what its reason answers; any other one stands in the way of accepting.
285
+ const standing = blockers.filter(one => !(owed === "exception" && one.key === "criteria")).map(one => terminalText(one.message));
286
+ const notNow = assignment.state === "complete" ? "This result is already accepted." : assignment.state === "ready-to-check" ? null : terminalText(assignment.detail || "This result isn't ready to accept.");
287
+ const changed = familyChangedFiles(store, receipt.taskId, receipt.runId, evidenceRoot);
288
+ const backing = checkBackingOf(receiptChecksOf(receipt));
289
+ let checks = [];
290
+ try {
291
+ const list = mergeChecklistOf(store, id, now, { principal: "operator", repos: null }, evidenceRoot);
292
+ checks = list === null ? [] : checklistLines(list).map(line => terminalText(line));
293
+ }
294
+ catch {
295
+ checks = [];
296
+ }
297
+ return {
298
+ id, runId: receipt.runId, digest: receipt.digest, title: terminalText(assignment.title),
299
+ ready: notNow === null && standing.length === 0, blockers: notNow === null ? standing : [notNow],
300
+ summary: (receipt.agentReport ?? "").split("\n").map(line => terminalText(line).trimEnd()).filter(line => line.trim() !== "").slice(0, 8),
301
+ files: changed === null ? null : { names: changed.slice(0, 10).map(name => terminalText(name)), more: Math.max(0, changed.length - 10) },
302
+ requirements: (receipt.proof?.matrix ?? []).map(row => ({ word: requirementWordOf(row, receipt.proof?.verdict ?? null, backing), statement: terminalText(row.statement ?? row.id ?? "") })),
303
+ checks, owed,
304
+ };
305
+ }
306
+ /** Whether a plan can be asked for this task now, and the planning source a request binds to (local-owner slice 2): a
307
+ * queued task with no plan asked or drafted and no approved plan. What the screen offers only; the command decides. */
308
+ function planAskOf(store, id) {
309
+ const ref = store.lookupRef(id);
310
+ const scope = store.getScope(id);
311
+ if (ref === null || ref.plan !== null || store.getTask(id)?.state !== "queued" || (scope !== null && scope.approvedAt !== null && scope.approvedDigest === scope.digest))
312
+ return null;
313
+ const source = planSourceOf(store, id);
314
+ return source === null ? null : { source };
315
+ }