toolroll 0.9.61 → 0.9.62

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 CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.62 — 2026-10-12
4
+
5
+ - **Release checks in minutes.** The upgrade path runs on a sample, CI's
6
+ passing run on the exact commit is reused, and the first failure stops it.
7
+ - **Storage sweep waits for quiet.** It runs when nothing is building, and
8
+ Settings → Storage says when it last ran and why it waits.
9
+ - **Landing proof you can trust.** A task completes on its own only for a
10
+ merged pull request or the project's recorded remote main, never a local
11
+ branch a build could move. Projects added before this need it once: you
12
+ (the owner) or an instance operator's lead runs
13
+ `toolroll repos add <project> --remote <url>`. Until then, their tasks wait
14
+ for you.
15
+ - **Cleanup follow-ups.** Only branches Toolroll created are removed, counted
16
+ from when it made them; scout branches stay, and duplicate teardowns merge.
17
+ - **Fresh base.** A task with no commits starts from current main; the task
18
+ page shows its base.
19
+
3
20
  ## 0.9.61 — 2026-10-12
4
21
 
5
22
  - **Agents board.** One place to check on running agents: Needs you,
package/dist/cli.js CHANGED
@@ -506,7 +506,14 @@ async function runReposCommand(argv, write, onboard) {
506
506
  const tokenAt = argv.indexOf("--token");
507
507
  const tokenFlag = tokenAt === -1 ? undefined : argv[tokenAt + 1];
508
508
  const bare = argv.filter((argument, index) => argument !== "--json" && (tokenAt === -1 || (index !== tokenAt && index !== tokenAt + 1)));
509
- const [action, ...paths] = bare;
509
+ const remoteAt = bare.indexOf("--remote");
510
+ const remoteUrl = remoteAt === -1 ? undefined : bare[remoteAt + 1];
511
+ const [action, ...paths] = bare.filter((_, i) => remoteAt === -1 || (i !== remoteAt && i !== remoteAt + 1));
512
+ if (remoteAt !== -1 && (action !== "add" || bare.filter(v => v === "--remote").length !== 1 || remoteUrl === undefined || remoteUrl.startsWith("-") || paths.length > 1)) {
513
+ const message = "Use repos add <project> --remote <url> for one project.";
514
+ write(json ? envelopeJson({ ok: false, command: "repos", reason: "usage", message }) : message);
515
+ return USAGE_EXIT;
516
+ }
510
517
  const databaseFile = databasePath(process.env, homedir());
511
518
  const file = registryBeside(databaseFile);
512
519
  if (tokenAt !== -1 && (tokenFlag === undefined || tokenFlag.startsWith("-"))) {
@@ -529,11 +536,18 @@ async function runReposCommand(argv, write, onboard) {
529
536
  return 1;
530
537
  }
531
538
  if (action === undefined) {
532
- if (json) {
533
- write(envelopeJson({ ok: true, command: "repos", count: loaded.repos.length, repos: loaded.repos, file }));
534
- return 0;
539
+ const store = openStore(databaseFile);
540
+ try {
541
+ const projects = loaded.repos.map(repo => ({ repo, remoteUrl: store.projectRemote(repo) }));
542
+ if (json) {
543
+ write(envelopeJson({ ok: true, command: "repos", count: loaded.repos.length, repos: loaded.repos, projects, file }));
544
+ return 0;
545
+ }
546
+ return listRepos(loaded.repos, file, write, new Map(projects.map(p => [p.repo, p.remoteUrl])));
547
+ }
548
+ finally {
549
+ store.close();
535
550
  }
536
- return listRepos(loaded.repos, file, write);
537
551
  }
538
552
  if (action !== "add" && action !== "remove") {
539
553
  const message = `unknown command \`repos ${action}\` — try \`repos\`, \`repos add\`, \`repos remove\`, or \`repos add-from-github <owner/name>\``;
@@ -545,7 +559,7 @@ async function runReposCommand(argv, write, onboard) {
545
559
  const lines = [];
546
560
  const said = {};
547
561
  const say = json ? (line) => lines.push(line) : write;
548
- const code = await withAdmission({ databaseFile, registryFile: file, ...(leadToken === undefined ? {} : { leadToken }) }, say, said, admission => action === "add" ? addToRepos(admission, targets, say, said) : removeFromRepos(admission, targets, say, said));
562
+ const code = await withAdmission({ databaseFile, registryFile: file, ...(leadToken === undefined ? {} : { leadToken }) }, say, said, admission => action === "add" ? addToRepos(admission, targets, say, said, remoteUrl) : removeFromRepos(admission, targets, say, said));
549
563
  if (json) {
550
564
  write(envelopeJson({
551
565
  ok: code === 0,
@@ -582,7 +596,7 @@ async function withAdmission(where, write, said, body) {
582
596
  store.close();
583
597
  }
584
598
  }
585
- function listRepos(repos, file, write) {
599
+ function listRepos(repos, file, write, remotes) {
586
600
  if (repos.length === 0) {
587
601
  write("No repositories connected. Toolroll reports on everything it can find.");
588
602
  write("");
@@ -592,8 +606,10 @@ function listRepos(repos, file, write) {
592
606
  return 0;
593
607
  }
594
608
  write(`${repos.length === 1 ? "1 repository" : `${repos.length} repositories`} connected:`);
595
- for (const repo of repos)
609
+ for (const repo of repos) {
596
610
  write(` ${repo}${existsSync(repo) ? "" : " (missing)"}`);
611
+ write(` Recorded remote: ${remotes.get(repo) ?? "None"}`);
612
+ }
597
613
  write("");
598
614
  write(" toolroll repos add <path> add another; it is in the console at once");
599
615
  write(" toolroll repos remove <path> take one out of the console; its tasks stay");
@@ -606,7 +622,7 @@ function listRepos(repos, file, write) {
606
622
  function admissionExit(reason) {
607
623
  return reason === "not-git" || reason === "unavailable" ? USAGE_EXIT : 1;
608
624
  }
609
- async function addToRepos(admission, targets, write, said) {
625
+ async function addToRepos(admission, targets, write, said, remoteUrl) {
610
626
  // Every folder is proved before anything is written: one that is not a
611
627
  // repository fails here, not later and further away.
612
628
  let refused = false;
@@ -624,7 +640,7 @@ async function addToRepos(admission, targets, write, said) {
624
640
  const running = consoleRunning(admission.store, now);
625
641
  const projects = [];
626
642
  for (const path of targets) {
627
- const admitted = await admitProject(admission.store, { registryFile: admission.registryFile, path, actor: admission.actor, now });
643
+ const admitted = await admitProject(admission.store, { registryFile: admission.registryFile, path, actor: admission.actor, now, captureOrigin: true, ...(remoteUrl === undefined ? {} : { remoteUrl }) });
628
644
  if (!admitted.ok) {
629
645
  // The updater's own taxonomy survives (verification finding 4): a held
630
646
  // lock or a malformed registry is an operational fact, never "usage".
@@ -633,8 +649,9 @@ async function addToRepos(admission, targets, write, said) {
633
649
  write(admitted.message);
634
650
  return admissionExit(admitted.reason);
635
651
  }
636
- projects.push({ repo: admitted.repo, name: admitted.name, added: admitted.added });
652
+ projects.push({ repo: admitted.repo, name: admitted.name, added: admitted.added, remoteUrl: admitted.remote.url });
637
653
  write(addedWords(admitted.name, admitted.added, running));
654
+ write(`Recorded remote: ${admitted.remote.url ?? "None. Use repos add <project> --remote <url> to record one."}`);
638
655
  }
639
656
  said.projects = projects;
640
657
  return 0;
@@ -1,4 +1,5 @@
1
1
  import { type ExecResult } from "./exec.js";
2
+ import type { RemoteMainRead, RemoteMains } from "./remote-main.js";
2
3
  import type { Store } from "./store.js";
3
4
  import { type Runner, type WorktreePool } from "./worktree.js";
4
5
  /** `storage clean --landed` takes only what has waited at least this long. */
@@ -13,10 +14,12 @@ export declare const AUTO_ITEMS = 40;
13
14
  export type Git = (cwd: string, args: readonly string[]) => Promise<ExecResult>;
14
15
  export declare function gitOf(runner?: Runner): Git;
15
16
  export type LandedHow = "merged" | "in-main" | "content-in-main";
17
+ /** `trusted`: it may complete a task automatically (a recorded merge, or the recorded remote's main). */
16
18
  export type LandedProof = {
17
19
  landed: true;
18
20
  how: LandedHow;
19
21
  reason: string;
22
+ trusted: boolean;
20
23
  } | {
21
24
  landed: false;
22
25
  why: string;
@@ -28,10 +31,17 @@ export type PullRequestFact = {
28
31
  number: number | null;
29
32
  };
30
33
  /**
31
- * Whether this commit's work landed, read from what Toolroll recorded and the local repository only. Fails closed:
32
- * every doubt is "not proven", with the reason in a few words.
34
+ * Whether this commit's work landed, read from what Toolroll recorded and the local repository. Fails closed: every
35
+ * doubt is "not proven", with the reason in a few words. Only a recorded merge is trusted to complete a task: a build
36
+ * can write the local main.
33
37
  */
34
38
  export declare function landedProof(git: Git, repo: string, head: string | null, pullRequest?: PullRequestFact | null): Promise<LandedProof>;
39
+ /**
40
+ * The proof automatic completion trusts: a recorded merge of this exact commit, or its work in the main of the
41
+ * project's recorded remote, compared only in the private directory that main was fetched into. `remote` is read only
42
+ * when no recorded merge settles it.
43
+ */
44
+ export declare function remoteLandedProof(remote: () => Promise<RemoteMainRead>, repo: string, head: string | null, pullRequest?: PullRequestFact | null): Promise<LandedProof>;
35
45
  export type LandedTarget = {
36
46
  kind: "checkout" | "branch";
37
47
  target: string;
@@ -54,6 +64,8 @@ export type LandedCandidate = {
54
64
  completes: boolean;
55
65
  how: LandedHow;
56
66
  reason: string;
67
+ /** Proven by a recorded merge or the recorded remote's main: only then may it complete. */
68
+ trusted: boolean;
57
69
  targets: LandedTarget[];
58
70
  keeps: LandedKeep[];
59
71
  };
@@ -62,7 +74,7 @@ export type LandedCandidate = {
62
74
  * `limit` caps how many results are read against git (reconcile's bound; results read before and unchanged since
63
75
  * don't count); `before` takes only results finished before it (the one-time pass's seven days).
64
76
  */
65
- export declare function landedResults(store: Store, git: Git, repo: string, root: string, now: Date, options?: {
77
+ export declare function landedResults(store: Store, git: Git, remotes: RemoteMains, repo: string, root: string, now: Date, options?: {
66
78
  limit?: number;
67
79
  before?: string;
68
80
  memo?: boolean;
@@ -104,9 +116,10 @@ export type LandedStep = {
104
116
  /**
105
117
  * Finish what teardowns intended: pending items, oldest first (checkouts before branches). Each is checked again
106
118
  * right before it goes, and recorded right after. Doubt leaves it pending for the next pass; a branch that moved, or
107
- * a result that has newer work, is kept for good. `only` limits it to these teardowns.
119
+ * a result that has newer work, is kept for good. `limit` caps the items settled (removed, already gone or kept), so
120
+ * items left waiting never crowd out later ones. `only` limits it to these teardowns.
108
121
  */
109
- export declare function finishLanded(store: Store, pool: WorktreePool, git: Git, now: () => Date, options?: {
122
+ export declare function finishLanded(store: Store, pool: WorktreePool, git: Git, remotes: RemoteMains, now: () => Date, options?: {
110
123
  repo?: string;
111
124
  limit?: number;
112
125
  only?: ReadonlySet<number>;
@@ -115,7 +128,7 @@ export declare function finishLanded(store: Store, pool: WorktreePool, git: Git,
115
128
  * Reconcile's part: finish teardowns a crash cut short, then find newly landed results (a bounded few), mark them
116
129
  * complete and tear them down.
117
130
  */
118
- export declare function reconcileLanded(store: Store, pool: WorktreePool, git: Git, repo: string, root: string, now: () => Date): Promise<{
131
+ export declare function reconcileLanded(store: Store, pool: WorktreePool, git: Git, remotes: RemoteMains, repo: string, root: string, now: () => Date): Promise<{
119
132
  completed: string[];
120
133
  steps: LandedStep[];
121
134
  }>;
@@ -142,15 +155,15 @@ export type LandedPlan = {
142
155
  };
143
156
  /**
144
157
  * What `storage clean --landed` would do now, across every project: landed results finished at least a week ago,
145
- * and Toolroll's own branches (toolroll/, toolroll-plan/, standing-orders/) no task uses whose last commit is at
146
- * least a week old and landed in main. Reads only.
158
+ * and branches Toolroll created (toolroll/, toolroll-plan/, standing-orders/) at least a week ago that no task uses
159
+ * and that landed in main. Reads only.
147
160
  */
148
- export declare function landedBacklog(store: Store, git: Git, root: string, now: Date, options?: {
161
+ export declare function landedBacklog(store: Store, git: Git, remotes: RemoteMains, root: string, now: Date, options?: {
149
162
  repos?: readonly string[];
150
163
  }): Promise<LandedPlan>;
151
164
  /** Act on a preview: record every teardown first, then carry them out. Refuses nothing it previewed silently: what
152
165
  * changed since is reported as kept. */
153
- export declare function cleanLandedBacklog(store: Store, pool: WorktreePool, git: Git, root: string, now: () => Date, plan: LandedPlan, actor: string): Promise<{
166
+ export declare function cleanLandedBacklog(store: Store, pool: WorktreePool, git: Git, remotes: RemoteMains, root: string, now: () => Date, plan: LandedPlan, actor: string): Promise<{
154
167
  completed: string[];
155
168
  steps: LandedStep[];
156
169
  refused: LandedKeep[];