toolroll 0.9.75 → 0.9.76

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 (55) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/dist/agent-fence.js +8 -0
  3. package/dist/approval-authority.js +6 -2
  4. package/dist/approval-rules-ui.d.ts +11 -0
  5. package/dist/approval-rules-ui.js +13 -1
  6. package/dist/batch-checks.js +3 -1
  7. package/dist/builder.js +1 -1
  8. package/dist/coding-workspace.js +7 -5
  9. package/dist/contracts/console-api.d.ts +1 -0
  10. package/dist/contracts/console-api.js +1 -0
  11. package/dist/deploy-gate.d.ts +3 -0
  12. package/dist/deploy-gate.js +3 -2
  13. package/dist/desktop-update.d.ts +1 -1
  14. package/dist/discover.js +6 -1
  15. package/dist/first-run.js +3 -1
  16. package/dist/git-incident.d.ts +49 -0
  17. package/dist/git-incident.js +69 -0
  18. package/dist/git-views.d.ts +20 -0
  19. package/dist/git-views.js +120 -0
  20. package/dist/grant.js +3 -1
  21. package/dist/operate.js +7 -1
  22. package/dist/planner.js +5 -1
  23. package/dist/project-knowledge.js +1 -1
  24. package/dist/project-learning.js +22 -2
  25. package/dist/pull-request-flow.js +4 -0
  26. package/dist/release-adapters.d.ts +24 -2
  27. package/dist/release-adapters.js +136 -10
  28. package/dist/release-authority.d.ts +46 -0
  29. package/dist/release-authority.js +64 -0
  30. package/dist/release-auto.d.ts +153 -0
  31. package/dist/release-auto.js +496 -0
  32. package/dist/release-cli.js +56 -2
  33. package/dist/release-coverage.js +1 -1
  34. package/dist/release.d.ts +105 -1
  35. package/dist/release.js +229 -9
  36. package/dist/repository-context.js +4 -6
  37. package/dist/result-follow-ups.js +3 -1
  38. package/dist/scope.d.ts +31 -2
  39. package/dist/scope.js +44 -3
  40. package/dist/scout.js +5 -1
  41. package/dist/server/render-tasks.d.ts +10 -0
  42. package/dist/server/render-tasks.js +23 -13
  43. package/dist/server/route-table.js +3 -0
  44. package/dist/server/settings.js +29 -2
  45. package/dist/server/task-views.js +8 -0
  46. package/dist/store.d.ts +1 -1
  47. package/dist/store.js +19 -4
  48. package/dist/task-base.js +1 -1
  49. package/dist/teardown-git.d.ts +38 -10
  50. package/dist/teardown-git.js +894 -108
  51. package/dist/test-infra.d.ts +26 -0
  52. package/dist/test-infra.js +321 -0
  53. package/dist/worktree.d.ts +4 -0
  54. package/dist/worktree.js +13 -7
  55. package/package.json +1 -1
@@ -1,10 +1,8 @@
1
1
  /** Optional repository context. Callers admit project access; this module grants no authority.
2
2
  * The index is disposable. Every read hashes the current bounded tracked source set;
3
3
  * a missing/changed/unreadable index falls back to ordinary text search, never a task gate. */
4
- import { inReadOnlyRequest } from './read-only-request.js';
5
4
  import { checkoutGitSync } from './teardown-git.js';
6
5
  import { createHash, randomUUID } from 'node:crypto';
7
- import { execFileSync } from 'node:child_process';
8
6
  import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
9
7
  import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
10
8
  import { scanForSecrets } from './evidence.js';
@@ -14,10 +12,10 @@ const CODE = /\.(?:[cm]?[jt]sx?)$/i;
14
12
  const TEXT = /\.(?:[cm]?[jt]sx?|md|txt|css|html|json)$/i;
15
13
  const MAX_NODES = 16_000, MAX_EDGES = 12_000;
16
14
  const sha = (value) => createHash('sha256').update(value).digest('hex');
17
- // Read-only and bounded: no index refresh, no hooks, no fsmonitor program.
18
- // On a page read (Settings -> Knowledge's search), the hardened checkout git: no filter, hook, program or network.
19
- const git = (repo, args) => inReadOnlyRequest() ? checkoutGitSync(repo, args, { timeoutMs: 10_000, maxBuffer: 2_000_000 }).trimEnd()
20
- : execFileSync('git', ['--no-optional-locks', '-c', 'core.fsmonitor=false', '-c', 'core.hooksPath=/dev/null', '-C', repo, ...args], { encoding: 'utf8', maxBuffer: 2_000_000, timeout: 10_000, stdio: ['ignore', 'pipe', 'pipe'] }).trimEnd();
15
+ // Read-only and bounded, and always the hardened checkout git (a crew worktree is agent-written, and a page read or a
16
+ // lead's refresh reads it alike): no filter, hook, program or network, its config and info frozen in a view of the
17
+ // plane's own before they're inspected (teardown-git.ts checkoutGitSync).
18
+ const git = (repo, args) => checkoutGitSync(repo, args, { timeoutMs: 10_000, maxBuffer: 2_000_000 }).trimEnd();
21
19
  const inside = (root, path) => { const r = relative(root, path); return r !== '..' && !r.startsWith('..' + sep) && !isAbsolute(r); };
22
20
  function capture(request) {
23
21
  const repo = realpathSync(request.repo);
@@ -4,6 +4,7 @@ import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { belongsToRelease } from "./release-authority.js";
6
6
  import { run as execRun } from "./exec.js";
7
+ import { hardenedGit } from "./teardown-git.js";
7
8
  import { approvedCommandShell, SETUP_ENV_ALLOWLIST, SETUP_ENV_DENYLIST } from "./builder.js";
8
9
  import { runWithIsolatedDatabase } from "./child-database.js";
9
10
  import { boundStreamHeadTail, readVerifiedArtifact, redactSecretLines, scanForSecrets, storeEvidence } from "./evidence.js";
@@ -161,7 +162,8 @@ export function waitingRequestsOfAnyKind(store, now) {
161
162
  export async function runFollowUpCheck(store, root, request, deps = {}) {
162
163
  const now = deps.now ?? (() => new Date());
163
164
  const runner = deps.runner ?? execRun;
164
- const git = deps.git ?? execRun;
165
+ // The project's repository and a checkout of a build's commit, both agent-writable: the hardened git (teardown-git.ts).
166
+ const git = deps.git ?? hardenedGit(execRun);
165
167
  const asked = store.handle.prepare("SELECT run_id FROM action_ledger WHERE id = ? AND action = ?").get(request, CHECKS_REQUESTED);
166
168
  if (asked === undefined)
167
169
  return null;
package/dist/scope.d.ts CHANGED
@@ -418,7 +418,10 @@ export type ScopeAuthor = "mate" | "coordinator" | "scout" | "lead";
418
418
  /** The authorities a seal can stand on (approval-policy plan §1.5). Anything else stands on nothing. */
419
419
  /** `carried`: a later commit of a release carried under a person's release approval (release-once.md); only the carry
420
420
  * transaction ever writes it. */
421
- export type ApprovalBasis = "password" | "mode" | "chat" | "grant" | "schedule" | "lead" | "carried";
421
+ /** `auto-check` / `auto-green`: a release task the plane approved itself under the project's "Approve automatically when
422
+ * checks pass" (docs/plans/auto-approve-green.md) — `auto-check` lets its check run, `auto-green` is the release
423
+ * approval once CI and that check passed. Only release-auto.ts writes them; neither is ever a person's. */
424
+ export type ApprovalBasis = "password" | "mode" | "chat" | "grant" | "schedule" | "lead" | "carried" | "auto-check" | "auto-green";
422
425
  export declare const APPROVAL_BASES: readonly ApprovalBasis[];
423
426
  export type Approval = {
424
427
  approved: true;
@@ -865,7 +868,7 @@ export declare function releaseTermsSentence(terms: ReleaseTerms): string;
865
868
  export declare function releaseAsksAgainWords(terms: ReleaseTerms): string[];
866
869
  export { personProof } from "./person-proof.js";
867
870
  import { type ReleaseTerms } from "./contracts/release-terms.js";
868
- import { type ReleaseAuthority, type ReleaseCarry } from "./release-authority.js";
871
+ import { type AutoEvidence, type ReleaseAuthority, type ReleaseCarry } from "./release-authority.js";
869
872
  /** Whether the command running now is a machine's: the lead (its token) or a remote API run. Trusted context only. */
870
873
  export declare const machineActing: () => boolean;
871
874
  /** A proof row's detail, either form: the legacy text (`<at> · generation <n> · digest <d>`) or the versioned record
@@ -952,3 +955,29 @@ export declare function mainDebtProblem(store: Store, repo: string, taskId: stri
952
955
  * chat it is approved only through the release card.
953
956
  */
954
957
  export declare function deployGateScope(scope: Pick<Scope, "candidate" | "releaseTerms">): boolean;
958
+ /**
959
+ * The plane's own automatic approval of a release task (docs/plans/auto-approve-green.md), read the way the dispatch
960
+ * belts read it (release-authority.ts autoSealSql): sealed `kind` on the current digest, its evidence row naming exactly
961
+ * this task, kind, digest and candidate, a release task (candidate and terms signed in), the project's setting on now,
962
+ * and the person who turned it on still an approver there. Never a person's approval.
963
+ */
964
+ export declare function autoReleaseApprovalOf(store: Store, taskId: string, kind: "auto-check" | "auto-green"): {
965
+ ok: true;
966
+ by: string;
967
+ at: string;
968
+ evidence: AutoEvidence;
969
+ } | {
970
+ ok: false;
971
+ why: string;
972
+ };
973
+ /** A release's approval: a person's (personApprovalOf, unchanged), or the plane's own `auto-green`. Only release
974
+ * readers ask this (liveReleaseOf, and the deploy gates inside `toolroll release`); everything else asks for a person. */
975
+ export declare function releaseApprovalOf(store: Store, taskId: string, _now?: Date): {
976
+ ok: true;
977
+ by: string;
978
+ at: string;
979
+ method: string;
980
+ } | {
981
+ ok: false;
982
+ why: string;
983
+ };
package/dist/scope.js CHANGED
@@ -571,7 +571,7 @@ export function canonicalAcceptance(criteria) {
571
571
  .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
572
572
  .map(c => ({ id: c.id, statement: c.statement.trim(), evidence: [...c.evidence].sort() }));
573
573
  }
574
- export const APPROVAL_BASES = ["password", "mode", "chat", "grant", "schedule", "lead", "carried"];
574
+ export const APPROVAL_BASES = ["password", "mode", "chat", "grant", "schedule", "lead", "carried", "auto-check", "auto-green"];
575
575
  /**
576
576
  * The exact bytes an operator agreed to.
577
577
  *
@@ -1359,7 +1359,7 @@ function describeApproval(approval) {
1359
1359
  export { personProof } from "./person-proof.js";
1360
1360
  import { personProof } from "./person-proof.js";
1361
1361
  import { releaseTermsDigest } from "./contracts/release-terms.js";
1362
- import { CHECK_ACCESS_WIDTH, carriesUnsandboxed, newestReleaseGeneration, releaseAuthorityOfTask, releaseSealProblem } from "./release-authority.js";
1362
+ import { CHECK_ACCESS_WIDTH, autoEvidenceOf, carriesUnsandboxed, newestReleaseGeneration, releaseAuthorityOfTask, releaseAutoApproves, releaseSealProblem } from "./release-authority.js";
1363
1363
  /** Whether the command running now is a machine's: the lead (its token) or a remote API run. Trusted context only. */
1364
1364
  export const machineActing = () => currentActor()?.lead === true || activeRemote() !== null;
1365
1365
  /** A proof row's detail, either form: the legacy text (`<at> · generation <n> · digest <d>`) or the versioned record
@@ -1400,6 +1400,8 @@ export function personApprovalOf(store, taskId) {
1400
1400
  const basis = scope.approvalBasis ?? null;
1401
1401
  if (basis === "lead")
1402
1402
  return { ok: false, why: "approved by the lead, not by a person" };
1403
+ if (basis === "auto-check" || basis === "auto-green")
1404
+ return { ok: false, why: "approved automatically, not by a person" };
1403
1405
  if (basis !== "password" && basis !== "chat")
1404
1406
  return { ok: false, why: `approved by ${basis === null ? "an unknown road" : basis === "grant" ? "a flow" : basis}, not by a person` };
1405
1407
  const origin = sealOriginOf(store, taskId, scope.approvedBy, scope.approvedDigest, scope.approvedAt);
@@ -1474,7 +1476,8 @@ export function liveReleaseOf(store, taskId, action, now = new Date()) {
1474
1476
  if (a.tipTask !== taskId)
1475
1477
  return refuse("this is not the release's current commit");
1476
1478
  const anchor = store.getScope(a.anchorTask);
1477
- const person = personApprovalOf(store, a.anchorTask);
1479
+ // A person's yes, or the plane's own automatic one while the project still approves releases automatically.
1480
+ const person = releaseApprovalOf(store, a.anchorTask, now);
1478
1481
  if (!person.ok)
1479
1482
  return refuse(`the release approval no longer stands: ${person.why}`);
1480
1483
  if (anchor === null || anchor.approvedDigest !== a.anchorDigest || anchor.digest !== a.anchorDigest)
@@ -1539,3 +1542,41 @@ export function mainDebtProblem(store, repo, taskId) {
1539
1542
  export function deployGateScope(scope) {
1540
1543
  return (scope.candidate ?? null) !== null || (scope.releaseTerms ?? null) !== null;
1541
1544
  }
1545
+ /**
1546
+ * The plane's own automatic approval of a release task (docs/plans/auto-approve-green.md), read the way the dispatch
1547
+ * belts read it (release-authority.ts autoSealSql): sealed `kind` on the current digest, its evidence row naming exactly
1548
+ * this task, kind, digest and candidate, a release task (candidate and terms signed in), the project's setting on now,
1549
+ * and the person who turned it on still an approver there. Never a person's approval.
1550
+ */
1551
+ export function autoReleaseApprovalOf(store, taskId, kind) {
1552
+ const scope = store.getScope(taskId);
1553
+ const ref = store.lookupRef(taskId);
1554
+ if (scope === null || scope.approvedAt === null || scope.approvedBy === null || scope.approvedDigest === null)
1555
+ return { ok: false, why: "not approved yet" };
1556
+ if (scope.approvedDigest !== scope.digest)
1557
+ return { ok: false, why: "the approval is for different scope text" };
1558
+ if (scope.approvalBasis !== kind)
1559
+ return { ok: false, why: kind === "auto-green" ? "not approved automatically" : "not checking automatically" };
1560
+ if ((scope.candidate ?? null) === null || (scope.releaseTerms ?? null) === null || ref === null || ref.repo === null)
1561
+ return { ok: false, why: "only a release task is approved automatically" };
1562
+ const evidence = autoEvidenceOf(store, taskId, scope.modeDigest, kind);
1563
+ if (evidence === null || evidence.digest !== scope.approvedDigest || evidence.candidate !== scope.candidate || evidence.by !== scope.approvedBy)
1564
+ return { ok: false, why: "no record shows what approved it automatically" };
1565
+ if (!releaseAutoApproves(store, ref.repo))
1566
+ return { ok: false, why: "approving releases automatically was turned off" };
1567
+ const account = store.accountOf(scope.approvedBy);
1568
+ if (account === null || account.revokedAt !== null || account.role !== "approver" || !store.accountCanAccess(scope.approvedBy, ref.repo))
1569
+ return { ok: false, why: `${scope.approvedBy}, who turned on automatic release approval, can no longer approve in this project` };
1570
+ return { ok: true, by: scope.approvedBy, at: scope.approvedAt, evidence };
1571
+ }
1572
+ /** A release's approval: a person's (personApprovalOf, unchanged), or the plane's own `auto-green`. Only release
1573
+ * readers ask this (liveReleaseOf, and the deploy gates inside `toolroll release`); everything else asks for a person. */
1574
+ export function releaseApprovalOf(store, taskId, _now = new Date()) {
1575
+ const person = personApprovalOf(store, taskId);
1576
+ if (person.ok)
1577
+ return person;
1578
+ if (store.getScope(taskId)?.approvalBasis !== "auto-green")
1579
+ return person;
1580
+ const auto = autoReleaseApprovalOf(store, taskId, "auto-green");
1581
+ return auto.ok ? { ok: true, by: auto.by, at: auto.at, method: "auto-green" } : auto;
1582
+ }
package/dist/scout.js CHANGED
@@ -18,6 +18,7 @@ import { mkdtempSync, realpathSync, rmSync, unlinkSync } from "node:fs";
18
18
  import { homedir, tmpdir } from "node:os";
19
19
  import { dirname, join, relative, isAbsolute } from "node:path";
20
20
  import { run } from "./exec.js";
21
+ import { checkoutGit } from "./teardown-git.js";
21
22
  import { auditOf } from "./provider.js";
22
23
  import { currentClaim, heartbeat } from "./claim.js";
23
24
  import { heartbeat as runnerHeartbeat } from "./runner.js";
@@ -225,7 +226,10 @@ export async function scout(store, request) {
225
226
  }
226
227
  }
227
228
  async function scoutWith(store, request, outputDir) {
228
- const { taskId, taskRef, runner, worktree, branch, now, agent, git = run, timeoutMs = DEFAULT_SCOUT_TIMEOUT_MS, maxTurns = DEFAULT_SCOUT_TURNS, } = request;
229
+ const { taskId, taskRef, runner, worktree, branch, now, agent, git: givenGit = run, timeoutMs = DEFAULT_SCOUT_TIMEOUT_MS, maxTurns = DEFAULT_SCOUT_TURNS, } = request;
230
+ // The plane's git on the scout's checkout (agent-written, and its repository shared with every build) runs hardened, against
231
+ // a frozen view of the repository's config and info (teardown-git.ts checkoutGit); an injected runner is used as given.
232
+ const git = givenGit === run ? checkoutGit(run, "build", { repo: store.getWorktree(worktree)?.repo ?? null }) : givenGit;
229
233
  const claim = currentClaim(store, taskRef, now);
230
234
  if (claim === null || claim.runner !== runner || claim.leaseId !== request.leaseId) {
231
235
  return { ok: false, kind: "failure", reason: "not-yours", message: `${taskId} is not held under ${request.leaseId}` };
@@ -232,6 +232,11 @@ export declare function agentsStripHtml(view: RouteView | null, taskId: string):
232
232
  /** A ceremony nonce as stored: the sha256 hex of the value the form carried. */
233
233
  export declare function nonceHashOf(value: string): string;
234
234
  /** The approve POST's refusal, in words a person can act on. */
235
+ /** A release task's automatic approval, in words (docs/plans/auto-approve-green.md). */
236
+ export declare function autoReleaseWords(auto: {
237
+ kind: "checking" | "approved";
238
+ candidate: string;
239
+ }): string;
235
240
  /** " (password remembered from 10:42)" when the approval stood on a remembered password or sign-in; nothing otherwise. */
236
241
  export declare function approvalProofWords(proof: {
237
242
  method: string;
@@ -523,6 +528,11 @@ export declare function taskBodyParts(data: {
523
528
  method: string;
524
529
  at: string;
525
530
  } | null;
531
+ /** A release task the plane approved (or is checking) itself (docs/plans/auto-approve-green.md): its candidate. */
532
+ autoRelease?: {
533
+ kind: "checking" | "approved";
534
+ candidate: string;
535
+ } | null;
526
536
  /** One approval per release (release-once.md): the release this task belongs to, and whether this reader may stop it. */
527
537
  release?: ReleaseView | null;
528
538
  canStopRelease?: boolean;
@@ -746,6 +746,11 @@ export function nonceHashOf(value) {
746
746
  return createHash("sha256").update(value).digest("hex");
747
747
  }
748
748
  /** The approve POST's refusal, in words a person can act on. */
749
+ /** A release task's automatic approval, in words (docs/plans/auto-approve-green.md). */
750
+ export function autoReleaseWords(auto) {
751
+ return auto.kind === "approved" ? `Approved automatically: CI and release check passed on ${auto.candidate.slice(0, 7)}`
752
+ : `Checking automatically: approves itself once CI and the release check pass on ${auto.candidate.slice(0, 7)}`;
753
+ }
749
754
  /** " (password remembered from 10:42)" when the approval stood on a remembered password or sign-in; nothing otherwise. */
750
755
  export function approvalProofWords(proof) {
751
756
  if (proof == null)
@@ -1497,11 +1502,13 @@ export function taskBodyParts(data) {
1497
1502
  html `<p><strong>Quality</strong> ${qualityModeTitle(scope.qualityMode ?? "default")}</p>`,
1498
1503
  acceptanceCeremonyHtml(scope.acceptance),
1499
1504
  scope.releaseTerms == null ? "" : html `<p><strong>Release</strong></p><p class="recap">${releaseTermsSentence(scope.releaseTerms)}</p>${scope.releaseTerms.policy.carry ? html `<details><summary>What asks again</summary><ul>${joinHtml(releaseAsksAgainWords(scope.releaseTerms).map(line => html `<li>${line}</li>`), "")}</ul></details>` : ""}`,
1500
- approval.approved && data.release?.kind === "carried"
1501
- ? html `<p class="meta scope-seal">Carried from ${data.release.approver ?? "a person"}'s release approval · <a href="${taskHref(data.release.anchorTask)}">Release ${data.release.version}</a> · <span class="seal">signs ${shortDigest(scope.digest)}</span></p>`
1502
- : approval.approved
1503
- ? html `<p class="meta scope-seal">Approved by ${approval.by}${approvalProofWords(data.approvalProof)} · ${whenTime(approval.at)} · <span class="seal">signs ${shortDigest(scope.digest)}</span><span class="so-sr-only"> — approval binds to this exact wording</span></p>`
1504
- : html `<p class="meta"><span class="seal">signs ${shortDigest(scope.digest)}</span> — approval binds to this exact wording</p><p class="meta">Not approved${approval.reason === "changed" ? " — approved once, then rewritten" : ""}</p>`,
1505
+ approval.approved && data.autoRelease != null
1506
+ ? html `<p class="meta scope-seal">${autoReleaseWords(data.autoRelease)} · <span class="seal">signs ${shortDigest(scope.digest)}</span></p>`
1507
+ : approval.approved && data.release?.kind === "carried"
1508
+ ? html `<p class="meta scope-seal">Carried from ${data.release.approver ?? "a person"}'s release approval · <a href="${taskHref(data.release.anchorTask)}">Release ${data.release.version}</a> · <span class="seal">signs ${shortDigest(scope.digest)}</span></p>`
1509
+ : approval.approved
1510
+ ? html `<p class="meta scope-seal">Approved by ${approval.by}${approvalProofWords(data.approvalProof)} · ${whenTime(approval.at)} · <span class="seal">signs ${shortDigest(scope.digest)}</span><span class="so-sr-only"> — approval binds to this exact wording</span></p>`
1511
+ : html `<p class="meta"><span class="seal">signs ${shortDigest(scope.digest)}</span> — approval binds to this exact wording</p><p class="meta">Not approved${approval.reason === "changed" ? " — approved once, then rewritten" : ""}</p>`,
1505
1512
  html `</div>`,
1506
1513
  ], "\n");
1507
1514
  // The scout's report (mate arc §10): title, summary, the document inert,
@@ -1792,11 +1799,13 @@ export function taskBodyParts(data) {
1792
1799
  : "";
1793
1800
  const scopeRow = scope === null
1794
1801
  ? prop("scope", "none yet")
1795
- : approval.approved && data.release?.kind === "carried"
1796
- ? prop("approved scope", html `<span class="seal">signs ${shortDigest(scope.digest)}</span> · carried from ${data.release.approver ?? "a person"}'s release approval · <a href="${taskHref(data.release.anchorTask)}">Release ${data.release.version}</a>`)
1797
- : approval.approved
1798
- ? prop("approved scope", html `<span class="seal">signs ${shortDigest(scope.digest)}</span> · ${qualityModeTitle(scope.qualityMode ?? "default")} · approved by ${approval.by}${approvalProofWords(data.approvalProof)} · ${whenTime(approval.at)}`)
1799
- : prop("scope", approval.reason === "changed" ? "rewritten since its approval — needs a new yes" : "not approved");
1802
+ : approval.approved && data.autoRelease != null
1803
+ ? prop("approved scope", html `<span class="seal">signs ${shortDigest(scope.digest)}</span> · ${autoReleaseWords(data.autoRelease)}`)
1804
+ : approval.approved && data.release?.kind === "carried"
1805
+ ? prop("approved scope", html `<span class="seal">signs ${shortDigest(scope.digest)}</span> · carried from ${data.release.approver ?? "a person"}'s release approval · <a href="${taskHref(data.release.anchorTask)}">Release ${data.release.version}</a>`)
1806
+ : approval.approved
1807
+ ? prop("approved scope", html `<span class="seal">signs ${shortDigest(scope.digest)}</span> · ${qualityModeTitle(scope.qualityMode ?? "default")} · approved by ${approval.by}${approvalProofWords(data.approvalProof)} · ${whenTime(approval.at)}`)
1808
+ : prop("scope", approval.reason === "changed" ? "rewritten since its approval — needs a new yes" : "not approved");
1800
1809
  const strikesRow = data.strikes > 0 ? prop("strikes", `${data.strikes} failed attempt(s)`) : "";
1801
1810
  const propsCard = html `<div class="card props">${workerRow}${queueRow}${scopeRow}${publishesRow}${economics}${strikesRow}</div>`;
1802
1811
  // The same facts for the rebuilt page, row for row.
@@ -1811,9 +1820,10 @@ export function taskBodyParts(data) {
1811
1820
  facts.push({ label: "Queue", parts: [`${data.position.position} of ${data.position.total}${data.position.column === null ? " in the shared queue" : ` in ${data.position.column}'s queue`} · `, { label: "Reorder", href: "/board?view=order" }] });
1812
1821
  }
1813
1822
  facts.push(scope === null ? { label: "Scope", parts: ["none yet"] }
1814
- : approval.approved && data.release?.kind === "carried" ? { label: "Approved scope", parts: [`Carried from ${data.release.approver ?? "a person"}'s release approval · `, { label: `Release ${data.release.version}`, href: taskHref(data.release.anchorTask) }] }
1815
- : approval.approved ? { label: "Approved scope", parts: [{ seal: `signs ${scope.digest.length <= 12 ? scope.digest : `${scope.digest.slice(0, 12)}…`}` }, ` · ${qualityModeTitle(scope.qualityMode ?? "default")} · approved by ${approval.by}${approvalProofWords(data.approvalProof)}`, ...(approval.at === null ? [] : [" · ", { at: approval.at }])] }
1816
- : { label: "Scope", parts: [approval.reason === "changed" ? "rewritten since its approval — needs a new yes" : "not approved"] });
1823
+ : approval.approved && data.autoRelease != null ? { label: "Approved scope", parts: [autoReleaseWords(data.autoRelease)] }
1824
+ : approval.approved && data.release?.kind === "carried" ? { label: "Approved scope", parts: [`Carried from ${data.release.approver ?? "a person"}'s release approval · `, { label: `Release ${data.release.version}`, href: taskHref(data.release.anchorTask) }] }
1825
+ : approval.approved ? { label: "Approved scope", parts: [{ seal: `signs ${scope.digest.length <= 12 ? scope.digest : `${scope.digest.slice(0, 12)}…`}` }, ` · ${qualityModeTitle(scope.qualityMode ?? "default")} · approved by ${approval.by}${approvalProofWords(data.approvalProof)}`, ...(approval.at === null ? [] : [" · ", { at: approval.at }])] }
1826
+ : { label: "Scope", parts: [approval.reason === "changed" ? "rewritten since its approval — needs a new yes" : "not approved"] });
1817
1827
  if (data.release?.kind === "anchor")
1818
1828
  facts.push({ label: "Release", parts: [`Approved for release ${data.release.version}`, ...(data.release.landed.length === 0 ? [] : [` · later commits: ${data.release.landed.map(one => one.sha.slice(0, 7)).join(", ")}`]), ...(data.release.carrying ? [] : [` · later commits ask again`])] });
1819
1829
  facts.push({ label: "Publishes as", parts: [publishesAs] });
@@ -303,6 +303,9 @@ post("tasks", "sweep.cancel-rest", "^\\/sweeps\\/[1-9][0-9]{0,9}\\/cancel-rest$"
303
303
  // (the handler proves it as a credential act); turning it off is one click, so the row itself is not a step-up.
304
304
  get("settings", "settings.passwordless", "/settings/passwordless", { ...free, ...operator("An instance operator sets Passwordless.", "/settings") });
305
305
  post("settings", "settings.passwordless-send", "/settings/passwordless", { callers: ["cookie"], ...operator("An instance operator sets Passwordless.", "/settings") });
306
+ // Release approval (docs/plans/auto-approve-green.md): an approver of the project, from a browser. Approving releases
307
+ // automatically takes a typed password (the handler proves it as a credential act); asking again is one click.
308
+ post("settings", "settings.release-approval-send", "/settings/release-approval", { callers: ["cookie"], ...approver("An approver sets how releases are approved.", "/settings/approval") });
306
309
  post("pages", "ledger.checkpoint", "/ledger/checkpoint", operator("An instance operator makes ledger checkpoints.", "/ledger"));
307
310
  const SETTINGS_ACT_ROLES = {
308
311
  policy: operator("An instance operator sets the policy.", "/settings/policy"),
@@ -39,7 +39,8 @@ import { browserCaller } from "./session.js";
39
39
  import { DEFAULT_ACCENT, normalHex } from "../accent-colors.js";
40
40
  import { resolvePhaseAgent } from "../agentconfig.js";
41
41
  import { parseProtectedPaths } from "../approval-policy.js";
42
- import { approvalRulesHtml } from "../approval-rules-ui.js";
42
+ import { approvalRulesHtml, releaseApprovalHtml } from "../approval-rules-ui.js";
43
+ import { releaseApprovalSetting, setReleaseApproval } from "../release-auto.js";
43
44
  import { backupHtml } from "../backup-ui.js";
44
45
  import { BACKUP_EVERY_HOURS, backupFolderOf, backupNow, checkBackupFolder, defaultBackupFolder, MAX_KEEP } from "../backup.js";
45
46
  import { projectBatchChecks, setProjectBatchChecks } from '../batch-policy.js';
@@ -366,7 +367,8 @@ ${connectionHtml(asked, connection, task)}${command === null ? "" : html `<p>On
366
367
  return refuse(response, who, 403, "That project is outside your access.", "/settings/approval");
367
368
  const selector = projects.length > 1 ? html `<form class="approval-project" method="get" action="/settings/approval"><label>Project<select name="repo">${projects.map(p => html `<option value="${p}"${p === chosen ? " selected" : ""}>${projectName(p)}</option>`)}</select></label><button>Show</button></form>` : "";
368
369
  const approvers = chosen === "" ? 0 : store.listApprovers().filter(one => { const account = store.accountOf(one.name); return account !== null && account.revokedAt === null && account.role === "approver" && store.accountCanAccess(one.name, chosen); }).length;
369
- const content = chosen === "" ? html `<p>Add a project to set its approval rules.</p>` : (approvalRulesHtml({ repo: chosen, name: projectName(chosen), rules: store.approvalRules(chosen), canChange: who.via === "cookie" && store.isInstanceOperator(who.name), approvers }, { said: url.searchParams.get("said"), problem: url.searchParams.get("problem") }));
370
+ const release = chosen === "" ? null : releaseApprovalSetting(store, chosen);
371
+ const content = chosen === "" ? html `<p>Add a project to set its approval rules.</p>` : html `${approvalRulesHtml({ repo: chosen, name: projectName(chosen), rules: store.approvalRules(chosen), canChange: who.via === "cookie" && store.isInstanceOperator(who.name), approvers }, { said: url.searchParams.get("said"), problem: url.searchParams.get("problem") })}${releaseApprovalHtml({ repo: chosen, auto: release.auto, setBy: release.setBy, at: release.at, canChange: who.via === "cookie" && who.role === "approver" && !store.isDemo() && store.accountCanAccess(who.name, chosen) })}`;
370
372
  return sendScreen(response, 200, screen("Approval rules", html `<p><a href="/settings">Settings</a></p><h1>Approval rules</h1>${selector}${content}`, { chrome: chromeFor(chosen || project, "settings") }));
371
373
  }
372
374
  // Sprint 8: Settings → Policy, the organisation policy and its history. Anyone signed in reads it; an instance operator changes it.
@@ -2538,6 +2540,7 @@ ${modeWords(terms)
2538
2540
  "settings.sign-in": signInPage,
2539
2541
  "settings.passwordless": passwordlessPage,
2540
2542
  "settings.passwordless-send": passwordlessSave,
2543
+ "settings.release-approval-send": releaseApprovalSave,
2541
2544
  "settings.lead": leadPage,
2542
2545
  "settings.teams": teamsPage,
2543
2546
  "settings.discord": discordPage,
@@ -2725,5 +2728,29 @@ ${modeWords(terms)
2725
2728
  return back("problem", "An approver for this project changes its sandbox. Nothing changed.");
2726
2729
  return back("said", JSON.stringify(changed.before) === JSON.stringify(changed.after) ? "Nothing changed." : "Saved. Tasks scoped from now on use it; tasks already approved keep what they were approved with.");
2727
2730
  }
2731
+ // Release approval (docs/plans/auto-approve-green.md): an approver of the project. Approving automatically is a
2732
+ // credential act (a password typed now, never a session); asking again is one click and takes back automatic seals.
2733
+ async function releaseApprovalSave(ctx) {
2734
+ const { response, now, posted } = ctx;
2735
+ const who = browserCaller(ctx.who);
2736
+ const body = readForm(posted, CONSOLE_FORMS.releaseApproval);
2737
+ const repo = body.get("repo") ?? "";
2738
+ if (!consoleProjects().includes(repo) || !visible(repo))
2739
+ return refuse(response, who, 403, "That project is outside your access.", "/settings/approval");
2740
+ const back = sayTo(response, `/settings/approval?repo=${encodeURIComponent(repo)}`);
2741
+ if (who.role !== "approver" || store.isDemo() || !store.accountCanAccess(who.name, repo))
2742
+ return back("problem", "An approver of this project sets how its releases are approved.");
2743
+ const action = body.get("action");
2744
+ if (action === "ask") {
2745
+ const changed = setReleaseApproval(store, repo, false, who.name, now);
2746
+ return back("said", !changed.changed ? "Saved." : `Releases ask you again.${changed.withdrawn.length === 0 ? "" : ` ${changed.withdrawn.length} automatic approval${changed.withdrawn.length === 1 ? " was" : "s were"} taken back.`}`);
2747
+ }
2748
+ if (action !== "auto")
2749
+ return back("problem", "Choose Ask me or Approve automatically when checks pass.");
2750
+ if (!authenticateCredential(who, body.get("password") ?? "", repo).ok)
2751
+ return back("problem", "Enter your Toolroll password to approve releases automatically.");
2752
+ const changed = setReleaseApproval(store, repo, true, who.name, now);
2753
+ return back("said", changed.changed ? "Releases approve themselves once CI and the release check pass." : "Saved.");
2754
+ }
2728
2755
  return { registrations, modelsView };
2729
2756
  }
@@ -496,6 +496,7 @@ export function createTaskViews(runtime) {
496
496
  steeringRun: ref === null ? null : runtime.store.steeringLiveBuild(ref.id, now),
497
497
  release: ref === null ? null : releaseViewOf(runtime.store, ref.externalId, now, authority => releaseObligationsOf(runtime.store, runtime.evidenceRoot, authority)),
498
498
  canStopRelease: who.role === "approver" && who.via === "cookie",
499
+ autoRelease: ref === null ? null : autoReleaseOf(runtime.store.getScope(ref.externalId)),
499
500
  approvalProof: (() => {
500
501
  // The proof of the approval shown: its approver, its exact sealed digest (approvedDigest === digest when approved).
501
502
  const scope = ref === null ? null : runtime.store.getScope(ref.externalId);
@@ -1139,3 +1140,10 @@ export function createTaskViews(runtime) {
1139
1140
  }
1140
1141
  return { guided, projectFamilyPeek, familiesInView, familyTasksInView, revisionDestination, earlierLiveVersions, familyHistory, freshAssignment, workRowOf, taskListPane, runIsLive, runIsTaskResult, revisionViewOf, taskViewData, routeViewOf, taskChatFocus, pullRequestTargetOf, taskPullRequestOf, failureOf, explainAttempt, runChecksHere, taskScreen, armTaskResume, planViewOf, planContractViewOf, revisionDocOf, revisionLedgerOf, progressOf, resultDetailOf };
1141
1142
  }
1143
+ /** A release task the plane approved (or is checking) itself, for the task page (docs/plans/auto-approve-green.md). */
1144
+ function autoReleaseOf(scope) {
1145
+ if (scope === null || scope.approvedDigest !== scope.digest || (scope.candidate ?? null) === null)
1146
+ return null;
1147
+ return scope.approvalBasis === "auto-green" ? { kind: "approved", candidate: scope.candidate }
1148
+ : scope.approvalBasis === "auto-check" ? { kind: "checking", candidate: scope.candidate } : null;
1149
+ }
package/dist/store.d.ts CHANGED
@@ -2355,7 +2355,7 @@ export declare class Store {
2355
2355
  * exactly what the signed digest was bound to. Returns false when the
2356
2356
  * scope vanished mid-ceremony. */
2357
2357
  sealScopeApproval(taskId: string, by: string, now: Date, mutation?: Mutation, basis?: {
2358
- kind: "mode" | "chat" | "grant" | "schedule";
2358
+ kind: "mode" | "chat" | "grant" | "schedule" | "auto-check" | "auto-green";
2359
2359
  modeDigest: string;
2360
2360
  /** A chat seal of a deploy gate: the release card it came from (chat-management.md §0a.5). */
2361
2361
  card?: "release";
package/dist/store.js CHANGED
@@ -7014,7 +7014,17 @@ export class Store {
7014
7014
  const deployGate = (typeof working["candidate"] === "string" && working["candidate"] !== "") || working["release_terms_json"] != null;
7015
7015
  if (deployGate && basis?.kind === "chat" && !(basis.card === "release" && typeof basis.cardDigest === "string" && /^[0-9a-f]{64}$/.test(basis.cardDigest)))
7016
7016
  return false;
7017
- if (terms.terms !== null && basis !== undefined && basis.kind !== "chat")
7017
+ // AUTOMATIC RELEASE SEALS (auto-approve-green.md): only on a release task (a candidate and release terms signed
7018
+ // in), only naming the evidence row the plane wrote for exactly this task, kind, digest and candidate.
7019
+ const automatic = basis?.kind === "auto-check" || basis?.kind === "auto-green";
7020
+ if (automatic) {
7021
+ if (terms.terms === null || typeof working["candidate"] !== "string" || working["candidate"] === "")
7022
+ return false;
7023
+ const evidence = autoEvidenceOf(this, taskId, basis.modeDigest, basis.kind);
7024
+ if (evidence === null || evidence.digest !== String(working["digest"]) || evidence.candidate !== working["candidate"] || evidence.by !== by)
7025
+ return false;
7026
+ }
7027
+ if (terms.terms !== null && basis !== undefined && basis.kind !== "chat" && !automatic)
7018
7028
  return false;
7019
7029
  if (terms.terms !== null && releaseSealProblem(this, terms.terms) !== null)
7020
7030
  return false;
@@ -7033,7 +7043,8 @@ export class Store {
7033
7043
  approved_route_json = proposed_route_json
7034
7044
  WHERE task_id = ?`)
7035
7045
  .run(now.toISOString(), by, basis === undefined ? "password" : basis.kind, basis === undefined ? null : basis.modeDigest, taskId);
7036
- if (changed.changes > 0 && terms.ok && terms.terms !== null) {
7046
+ // `auto-check` only lets the check run: it opens no release authority (only `auto-green` and a person's yes do).
7047
+ if (changed.changes > 0 && terms.ok && terms.terms !== null && basis?.kind !== "auto-check") {
7037
7048
  if (openReleaseAuthority(this, { taskId, digest: String(working["digest"]), terms: terms.terms, by, now }) === null)
7038
7049
  throw new ReleaseSealRefused();
7039
7050
  }
@@ -7066,7 +7077,11 @@ export class Store {
7066
7077
  ? "Approved automatically: its flow runs on its own. A connected worker can take it next."
7067
7078
  : basis.kind === "schedule"
7068
7079
  ? "Approved by its standing order. A connected worker can take it next."
7069
- : "Approved automatically under the operating mode. A connected worker can take it next.",
7080
+ : basis.kind === "auto-check"
7081
+ ? "Checking automatically: it approves itself once CI and the release check pass."
7082
+ : basis.kind === "auto-green"
7083
+ ? "Approved automatically: CI and the release check passed."
7084
+ : "Approved automatically under the operating mode. A connected worker can take it next.",
7070
7085
  link: chatControlHref("task", taskId),
7071
7086
  }, now);
7072
7087
  }
@@ -20422,7 +20437,7 @@ export function serializeReleaseTerms(terms) {
20422
20437
  }
20423
20438
  // ---- one approval per release: the authority record (release-once.md, part 1) --------------------------------------
20424
20439
  export { LIVE_RELEASE_STATES, belongsToRelease, newestReleaseGeneration, outstandingReleaseOperations, releaseAuthority, releaseAuthorityOfTask, releaseCarries, releaseCarryTo, releaseOperation, releaseOperations, releaseSealProblem, } from "./release-authority.js";
20425
- import { LIVE_RELEASE_STATES, belongsToRelease, newestReleaseGeneration, outstandingReleaseOperations, releaseAuthority, releaseCarryTo, releaseOperation, releaseSealProblem, } from "./release-authority.js";
20440
+ import { LIVE_RELEASE_STATES, autoEvidenceOf, belongsToRelease, newestReleaseGeneration, outstandingReleaseOperations, releaseAuthority, releaseCarryTo, releaseOperation, releaseSealProblem, } from "./release-authority.js";
20426
20441
  /**
20427
20442
  * Open a release's authority for a person's seal of its terms (inside the seal's transaction): the next generation,
20428
20443
  * superseding the live one. False when it can't (releaseSealProblem), which refuses the seal.
package/dist/task-base.js CHANGED
@@ -7,7 +7,7 @@
7
7
  import { execFileSync } from "node:child_process";
8
8
  export const readGit = (cwd, args) => {
9
9
  try {
10
- return execFileSync("git", ["--no-optional-locks", ...args], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5_000, maxBuffer: 65_536 }).trim();
10
+ return execFileSync("git", ["--no-optional-locks", "--no-lazy-fetch", "-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", ...args], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5_000, maxBuffer: 65_536 }).trim();
11
11
  }
12
12
  catch {
13
13
  return null;
@@ -6,14 +6,24 @@ export declare const TEARDOWN_FLAGS: readonly string[];
6
6
  /** Exit code of a refused command (git's own fatal code, so no caller reads it as "not found" or "no"). */
7
7
  export declare const REFUSED_CODE = 128;
8
8
  /** The command-running setting a `git config --list --show-scope -z` listing sets, or null. Only local and worktree
9
- * scope count: teardown commands never read system or global config. */
10
- export declare function commandSetting(listing: string): string | null;
9
+ * scope count: teardown commands never read system or global config. `lfsChecked`: the caller refuses a checkout whose
10
+ * `.lfsconfig` names a git-lfs extension (lfsExtensionIn), so git-lfs's own exact filters may stand (runsCommand).
11
+ * Fails closed: a listing that can't be classified names a setting. */
12
+ export declare function commandSetting(listing: string, options?: {
13
+ lfsChecked?: boolean;
14
+ }): string | null;
11
15
  /** The `-c` settings that carry the operator's own values (CARRIED_VALUES, LFS_FILTER) from a global/system listing. */
12
16
  export declare function carriedSettings(listing: string): string[];
13
17
  /** Why a teardown command was refused, or null when `result` isn't a refusal. */
14
18
  export declare function refusal(result: ExecResult): string | null;
15
- /** The one way teardown runs git, including the target checkout immediately before removal. */
16
- export declare function teardownGit(runner?: Runner): Git;
19
+ /** The one way teardown runs git, including the target checkout immediately before removal. Every command runs
20
+ * against a write view of its repository (gitView), whose config is the one inspected and whose info is a copy, so a
21
+ * setting or attributes file an agent adds after the inspection is never read (a build's process can outlive its turn:
22
+ * on macOS containment is observational, src/containment.ts, and a sibling build shares the repository). A removal's
23
+ * target is bound to the same repository as the folder it is removed from. A removal without --force runs under the
24
+ * target's own view (its own config.worktree folded in), so git's last check of whether it is clean reads it as the
25
+ * target itself would. `binding.repo`: the project's main checkout, from the plane's records, when the caller has it. */
26
+ export declare function teardownGit(runner?: Runner, binding?: GitBinding): Git;
17
27
  /**
18
28
  * Git on a build's checkout, as the plane runs it outside every fence (staging, committing and inspecting the build's
19
29
  * work): the same fixed hardening as teardown (no hooks, fsmonitor, ext transport, credential helpers or automatic
@@ -29,14 +39,24 @@ export declare function teardownGit(runner?: Runner): Git;
29
39
  * that can't be read so counts as including.
30
40
  */
31
41
  export declare function checkoutIncludes(checkout: string): boolean;
42
+ export type GitBinding = {
43
+ repo?: string | null;
44
+ };
45
+ /** Settings fixed on every write through a view: `worktree add` records absolute paths (relative ones would need a
46
+ * config write, which a view keeps); no transport at all (a caller that fetches allows its one protocol by name, as the
47
+ * nightly check's local fetch does with protocol.file.allow); and no split index (its shared index files would be
48
+ * written in the plane's GIT_DIR). */
49
+ export declare const WRITE_VIEW_FLAGS: readonly string[];
32
50
  /** Whether the checkout's .lfsconfig (read as git parses it, that file only) sets any git-lfs key that could name a
33
51
  * program (anything but INERT_LFS). A file that
34
52
  * isn't a plain one, or doesn't parse, counts as naming one. */
35
53
  export declare function lfsExtensionIn(checkout: string): boolean;
36
- /** `mode` "read" is for reading a checkout an agent may still be writing (the code page): every command also takes
37
- * readFlags() and runs against the checkout's readView, so neither its config nor its info/attributes can change
38
- * between the inspection and the run. */
39
- export declare function checkoutGit(inner?: Runner, mode?: "build" | "read"): Runner;
54
+ /** Every command runs against a view of the checkout's repository (gitView) whose config and info are copies the
55
+ * plane made, inspected there, so neither can change between the inspection and the run. `mode` "read" is for reading
56
+ * a checkout an agent may still be writing (the code page): every command also takes readFlags(), and the view holds
57
+ * no info/attributes. "build" is the plane's staging and committing of a build's work, which writes the repository
58
+ * through the view (gitView). */
59
+ export declare function checkoutGit(inner?: Runner, mode?: "build" | "read", binding?: GitBinding): Runner;
40
60
  /**
41
61
  * Flags for reading a checkout an agent writes while a page waits (docs/plans/get-side-effects.md). Command-line
42
62
  * settings win over anything the checkout's config says, even one changed after it was inspected: no fsmonitor, hooks,
@@ -47,16 +67,24 @@ export declare const PAGE_READ_FLAGS: readonly string[];
47
67
  /** The read flags, for every read of an agent-written checkout, whatever the HTTP method (security review of
48
68
  * 01bf3114): the worktree's attributes come from the empty tree. Git still reads the repository's info/attributes and
49
69
  * its config as it starts, so the flags alone don't stop a filter planted in both after the inspection: every read
50
- * also runs against the checkout's readView (checkoutGitSync, checkoutGit "read"), whose config and info are the plane's. The cost is cosmetic: a checkout's line-ending or encoding rules are not applied, so
70
+ * also runs against a view of the checkout's repository (gitView; checkoutGitSync, checkoutGit "read"), whose config and info are the plane's. The cost is cosmetic: a checkout's line-ending or encoding rules are not applied, so
51
71
  * a file they would normalize can read as changed (fail closed: shipping then asks for a clean commit). */
52
72
  export declare function readFlags(): readonly string[];
53
73
  /**
54
74
  * checkoutGit, synchronously, for reads a page shows from a checkout an agent writes (docs/plans/get-side-effects.md):
55
75
  * the same refusals (an include, an .lfsconfig extension, a config key that can run a command), no system or global
56
- * config, the operator's own values carried, PAGE_READ_FLAGS and the checkout's readView (which hold even if its config
76
+ * config, the operator's own values carried, PAGE_READ_FLAGS and a view of the checkout's repository (gitView: they hold even if its config
57
77
  * or info/attributes change after they were read), submodules skipped, bounded. A refusal throws.
58
78
  */
59
79
  export declare function checkoutGitSync(cwd: string, args: readonly string[], options?: {
60
80
  timeoutMs?: number;
61
81
  maxBuffer?: number;
82
+ binding?: GitBinding;
62
83
  }): string;
84
+ /**
85
+ * checkoutGit ("build": the repository written through its view) for callers that name the repository with a leading
86
+ * `git -C <dir>` instead of a cwd: worktree add and remove in a project's repository, a merge in a checkout of it.
87
+ * The project's repository is shared with every build, which can write its config, info and hooks; nothing here runs
88
+ * what they name. Anything but git goes to `inner` as given.
89
+ */
90
+ export declare function hardenedGit(inner?: Runner, mode?: "build" | "read", binding?: GitBinding): Runner;