toolroll 0.9.7 → 0.9.9
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 +48 -1
- package/dist/assignment-adapters.d.ts +1 -0
- package/dist/assignment-adapters.js +2 -0
- package/dist/assignment-brief.d.ts +2 -0
- package/dist/assignment-brief.js +2 -1
- package/dist/assignment-presentation.js +3 -1
- package/dist/assignment-status.d.ts +1 -1
- package/dist/assignment-ui.js +5 -0
- package/dist/assignment.d.ts +15 -0
- package/dist/assignment.js +56 -9
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +18 -18
- package/dist/browser-crew.js +4 -0
- package/dist/browser-workspace.d.ts +40 -0
- package/dist/build-review.d.ts +38 -0
- package/dist/build-review.js +341 -0
- package/dist/builder.d.ts +2 -0
- package/dist/builder.js +114 -8
- package/dist/chat-controls.d.ts +1 -1
- package/dist/chat-controls.js +1 -1
- package/dist/check-levels-ui.d.ts +5 -0
- package/dist/check-levels-ui.js +16 -2
- package/dist/check-progress.js +4 -3
- package/dist/checkout-cleanup.d.ts +40 -2
- package/dist/checkout-cleanup.js +129 -9
- package/dist/cli.js +2 -0
- package/dist/desktop-update.d.ts +52 -0
- package/dist/desktop-update.js +147 -5
- package/dist/dispatch.js +2 -1
- package/dist/dispose.d.ts +4 -3
- package/dist/dispose.js +12 -4
- package/dist/evidence.d.ts +6 -0
- package/dist/evidence.js +8 -0
- package/dist/exec.d.ts +5 -0
- package/dist/exec.js +30 -4
- package/dist/first-run.d.ts +1 -1
- package/dist/first-run.js +8 -6
- package/dist/invoke.js +5 -0
- package/dist/lead-context.d.ts +1 -1
- package/dist/lead-context.js +4 -2
- package/dist/lead-status.d.ts +10 -1
- package/dist/lead-status.js +21 -24
- package/dist/modes.d.ts +3 -1
- package/dist/modes.js +3 -1
- package/dist/needs-you.d.ts +55 -0
- package/dist/needs-you.js +83 -0
- package/dist/operate.d.ts +2 -0
- package/dist/operate.js +231 -36
- package/dist/plane-review.js +4 -0
- package/dist/process-custody.js +8 -0
- package/dist/process-recovery-eligibility.d.ts +1 -0
- package/dist/project-learning.d.ts +3 -3
- package/dist/project-learning.js +28 -17
- package/dist/proof.d.ts +3 -1
- package/dist/proof.js +4 -2
- package/dist/provider.js +16 -0
- package/dist/result-completion.d.ts +4 -0
- package/dist/result-completion.js +11 -0
- package/dist/retention-ui.d.ts +3 -1
- package/dist/retention-ui.js +4 -4
- package/dist/retention.d.ts +13 -6
- package/dist/retention.js +50 -12
- package/dist/review-context.d.ts +3 -0
- package/dist/review-context.js +45 -5
- package/dist/review-switch.d.ts +41 -0
- package/dist/review-switch.js +88 -0
- package/dist/reviewer.d.ts +24 -2
- package/dist/reviewer.js +49 -1
- package/dist/run-closeout.d.ts +89 -0
- package/dist/run-closeout.js +302 -0
- package/dist/serve.d.ts +15 -1
- package/dist/serve.js +158 -18
- package/dist/setup-guide.d.ts +2 -2
- package/dist/shared-deps.d.ts +94 -0
- package/dist/shared-deps.js +365 -0
- package/dist/storage.js +2 -0
- package/dist/store.d.ts +72 -1
- package/dist/store.js +258 -31
- package/dist/surface.js +5 -1
- package/dist/task-status.d.ts +22 -5
- package/dist/task-status.js +45 -24
- package/dist/telegram-progress.js +2 -1
- package/dist/telegram-status.js +3 -3
- package/dist/test-temp.d.ts +19 -0
- package/dist/test-temp.js +98 -0
- package/dist/toolroll-update-ui.js +7 -2
- package/dist/toolroll-update.d.ts +21 -1
- package/dist/toolroll-update.js +135 -5
- package/dist/work-index.d.ts +11 -0
- package/dist/work-index.js +103 -16
- package/dist/work-summary.d.ts +3 -1
- package/dist/work-summary.js +2 -1
- package/dist/workspace-ui.d.ts +3 -1
- package/dist/workspace-ui.js +10 -9
- package/dist/worktree.d.ts +16 -0
- package/dist/worktree.js +47 -0
- package/package.json +1 -1
package/dist/builder.js
CHANGED
|
@@ -59,11 +59,23 @@ import { TELEGRAM_TOKEN_ENVS } from "./names.js";
|
|
|
59
59
|
import { OPENROUTER_ENV_KEY, auditOf, ALL_CREDENTIAL_ENV } from "./provider.js";
|
|
60
60
|
import { openLiveLog } from "./live.js";
|
|
61
61
|
import { CheckProgressTracker } from "./check-progress.js";
|
|
62
|
-
import { captureParkEvidence, captureTerminalDiff, captureBaseTree, evidenceRoot, handoffName, storeHandoffArtifact, looksLikeProtocolFile, mailboxName, progressFileName, proofFileName, rubricFileName, proposalFileName, quarantineMailboxes, readMailbox, readVerifiedArtifact, scanForSecrets, redactSecretLines, storeEvidence, validateScreenshotBytes, imageDimensions, SCREENSHOT_BYTE_CAP, boundStreamHeadTail } from "./evidence.js";
|
|
62
|
+
import { captureParkEvidence, captureTerminalDiff, captureBaseTree, evidenceRoot, handoffName, isImagePath, WORKTREE_EVIDENCE_DIR, storeHandoffArtifact, looksLikeProtocolFile, mailboxName, progressFileName, proofFileName, rubricFileName, proposalFileName, quarantineMailboxes, readMailbox, readVerifiedArtifact, scanForSecrets, redactSecretLines, storeEvidence, validateScreenshotBytes, imageDimensions, SCREENSHOT_BYTE_CAP, boundStreamHeadTail } from "./evidence.js";
|
|
63
63
|
import { PROOF_LIMITS, parseProof, serializeProof, adjudicate, artifactManifestOnly, sameDiffStatFacts } from "./proof.js";
|
|
64
64
|
import { captureReviewContext } from "./review-context.js";
|
|
65
65
|
import { authoritySnapshotDigest, classifyRevisionAuthority, isMilestoneRegression, milestonesOf, parseExecutionPlanDocument, parsePlanRevisionProposal, parseProgressSnapshot, renderExecutionPlanDocument, PROGRESS_LIMITS, REVISION_LIMITS, } from "./plan.js";
|
|
66
66
|
import { maybeSettleRepairChain } from "./dispose.js";
|
|
67
|
+
import { detachShared, linkInto, linkedKey, promoteInstall, readyCopy, sharedDepsRoot, sharingFor } from "./shared-deps.js";
|
|
68
|
+
/** Where this store keeps shared dependencies; null for an in-memory store, which shares nothing. */
|
|
69
|
+
function sharedDepsRootOf(store) {
|
|
70
|
+
const file = store.databaseFile();
|
|
71
|
+
return file === null ? null : sharedDepsRoot(file);
|
|
72
|
+
}
|
|
73
|
+
/** The one line an agent needs when its node_modules is a shared copy: it is read-only, and how to get its own. */
|
|
74
|
+
function sharedDepsBrief(store, worktree) {
|
|
75
|
+
const root = sharedDepsRootOf(store);
|
|
76
|
+
return root === null || linkedKey(worktree, root) === null ? "" :
|
|
77
|
+
"node_modules here links a shared, read-only install. If you change package.json or the lockfile, replace it with your own install first: `rm -rf node_modules && npm install`.\n";
|
|
78
|
+
}
|
|
67
79
|
/**
|
|
68
80
|
* The native shell for an operator-approved repository command. There are
|
|
69
81
|
* exactly two consumers: dependency setup before an agent starts, and the
|
|
@@ -668,6 +680,20 @@ export async function build(store, request) {
|
|
|
668
680
|
message: "runner custody lapsed before the setup spawn — the lease, the runner, or its repo binding no longer stands",
|
|
669
681
|
};
|
|
670
682
|
}
|
|
683
|
+
// Shared dependencies: a checkout whose lockfile already has an installed
|
|
684
|
+
// copy links it instead of installing its own; a setup that is about to
|
|
685
|
+
// run never reaches through an old link into a shared copy.
|
|
686
|
+
const depsRoot = sharedDepsRootOf(store);
|
|
687
|
+
const sharing = depsRoot === null ? null : sharingFor({ repo: leased.repo, worktree, setup: setupWanted });
|
|
688
|
+
if (depsRoot !== null)
|
|
689
|
+
detachShared(worktree, depsRoot);
|
|
690
|
+
if (depsRoot !== null && sharing !== null) {
|
|
691
|
+
const ready = readyCopy(depsRoot, sharing.key);
|
|
692
|
+
if (ready !== null && linkInto(worktree, ready) !== null) {
|
|
693
|
+
store.stampWorktreeSetup(worktree, setupWanted.digest);
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
671
697
|
const runSetup = request.setup ?? run;
|
|
672
698
|
const shell = approvedCommandShell(setupWanted.command);
|
|
673
699
|
// Setup runs under the stop watch (v52), owned by this run: an
|
|
@@ -700,6 +726,9 @@ export async function build(store, request) {
|
|
|
700
726
|
message: `the approved setup for ${leased.repo} ${made.timedOut ? `ran past ${Math.round(setupWanted.timeoutMs / 60_000)}m` : `exited ${made.code}`} — ${redactSecretText(firstLine(made.stderr)) || "no stderr"}; no agent spawns in a checkout whose setup failed`,
|
|
701
727
|
};
|
|
702
728
|
}
|
|
729
|
+
if (depsRoot !== null && sharing !== null) {
|
|
730
|
+
promoteInstall({ root: depsRoot, repo: leased.repo, worktree, ...sharing, setupDigest: setupWanted.digest, now: (request.clock ?? (() => now))() });
|
|
731
|
+
}
|
|
703
732
|
store.stampWorktreeSetup(worktree, setupWanted.digest);
|
|
704
733
|
}
|
|
705
734
|
return null;
|
|
@@ -1128,7 +1157,7 @@ export async function build(store, request) {
|
|
|
1128
1157
|
// replacement for.
|
|
1129
1158
|
milestones.length === 0 || planRevisionHash === null
|
|
1130
1159
|
? null
|
|
1131
|
-
: { revision: planRevisionNumber, hash: planRevisionHash, milestones, progress, proposal }, scope === null ? [] : flowGoalCuts(store, scope.taskId, scope.goal)) + `\nCanonical signed rubric: ${rubric}. Its statement fields are exact; evidence requirements are separate fields. Do not edit this input. The lead or user reads these criteria directly; no restatement is needed.\n
|
|
1160
|
+
: { revision: planRevisionNumber, hash: planRevisionHash, milestones, progress, proposal }, scope === null ? [] : flowGoalCuts(store, scope.taskId, scope.goal)) + `\nCanonical signed rubric: ${rubric}. Its statement fields are exact; evidence requirements are separate fields. Do not edit this input. The lead or user reads these criteria directly; no restatement is needed.\n` + sharedDepsBrief(store, worktree);
|
|
1132
1161
|
// THE HELD BRANCH (Phase 2, v2 S0d + v6 W8): ownership transfers to the
|
|
1133
1162
|
// coordinator at the spawn point. Everything build() armed that its
|
|
1134
1163
|
// finally would have cleared is torn down or handed over HERE — the pulse
|
|
@@ -1786,7 +1815,7 @@ export async function settleProviderOutcome(captured, result) {
|
|
|
1786
1815
|
// result is that commit, sealed below against the task's original base.
|
|
1787
1816
|
const made = finishesKeptWork
|
|
1788
1817
|
? await keptWorkResult(captured, baseRevision, handoff.conclusion)
|
|
1789
|
-
: await commit(git, worktree, branch, taskId, scope, handoff.conclusion, request.attended === undefined ? scope?.candidate ?? null : null);
|
|
1818
|
+
: await commit(git, worktree, branch, taskId, scope, handoff.conclusion, request.attended === undefined ? scope?.candidate ?? null : null, words => store.addRunNote(request.runId, "Toolroll", words, clock()));
|
|
1790
1819
|
if (made.ok && made.parked === undefined && made.committed) {
|
|
1791
1820
|
const newHead = await git(GIT, ["--no-optional-locks", "rev-parse", "HEAD"], { cwd: worktree });
|
|
1792
1821
|
if (newHead.code === 0) {
|
|
@@ -2221,8 +2250,48 @@ async function settleProof(captured, statArtifactId, sealedHead) {
|
|
|
2221
2250
|
checkLog.push(attemptLog(label, configured.command, result));
|
|
2222
2251
|
return result;
|
|
2223
2252
|
};
|
|
2224
|
-
const
|
|
2225
|
-
|
|
2253
|
+
const ownInstallIfChanged = async () => {
|
|
2254
|
+
const depsRoot = sharedDepsRootOf(store);
|
|
2255
|
+
if (depsRoot === null || repo === null)
|
|
2256
|
+
return null;
|
|
2257
|
+
const linked = linkedKey(worktree, depsRoot);
|
|
2258
|
+
const live = linked === null ? null : store.liveWorktreeSetup(repo);
|
|
2259
|
+
if (linked === null || live === null)
|
|
2260
|
+
return null;
|
|
2261
|
+
if (sharingFor({ repo, worktree, setup: live })?.key === linked && readyCopy(depsRoot, linked) !== null)
|
|
2262
|
+
return null;
|
|
2263
|
+
if (!store.proveRunnerCustodyForSpawn(runId, now()))
|
|
2264
|
+
return "custody-lost";
|
|
2265
|
+
detachShared(worktree, depsRoot);
|
|
2266
|
+
const label = "Own install · approved project setup (package.json or lockfile changed)";
|
|
2267
|
+
const setupShell = approvedCommandShell(live.command);
|
|
2268
|
+
const installed = await underStopWatch(store, runId, () => runWithIsolatedDatabase(witnessedRunner(store, runId, now, request.setup ?? run), setupShell.file, setupShell.args, {
|
|
2269
|
+
cwd: worktree,
|
|
2270
|
+
timeoutMs: live.timeoutMs,
|
|
2271
|
+
processGroup: true,
|
|
2272
|
+
owner: runOwnerTag(store, runId),
|
|
2273
|
+
beforeSpawn: () => !stopRequestedFor(store, runId, request.shouldStop),
|
|
2274
|
+
onSpawn: pid => {
|
|
2275
|
+
request.onProviderSpawn?.(pid);
|
|
2276
|
+
if (stopRequestedFor(store, runId, request.shouldStop))
|
|
2277
|
+
throw new Error("the attempt was stopped before spawn custody completed");
|
|
2278
|
+
},
|
|
2279
|
+
envAllowlist: SETUP_ENV_ALLOWLIST,
|
|
2280
|
+
omitEnv: SETUP_ENV_DENYLIST,
|
|
2281
|
+
}));
|
|
2282
|
+
checkOutcomes.push(attemptOutcome(label, installed));
|
|
2283
|
+
noteCheckSuite(label, installed);
|
|
2284
|
+
checkLog.push(attemptLog(label, live.command, installed));
|
|
2285
|
+
if (installed.notFound || installed.timedOut || installed.code !== 0)
|
|
2286
|
+
return "own-install-failed";
|
|
2287
|
+
return await sealedTreeState(request.git ?? run) === "clean" ? null : "setup-changed-files";
|
|
2288
|
+
};
|
|
2289
|
+
const ownInstall = await ownInstallIfChanged();
|
|
2290
|
+
const first = ownInstall === null ? await runVerification("Project check · attempt 1") : null;
|
|
2291
|
+
if (first === null) {
|
|
2292
|
+
verifyCommand = { configured: true, ran: false, attemptFailed: true, failure: ownInstall ?? "own-install-failed" };
|
|
2293
|
+
}
|
|
2294
|
+
else if (first.notFound || first.timedOut) {
|
|
2226
2295
|
verifyCommand = { configured: true, ran: false, attemptFailed: true, failure: first.timedOut ? "timed-out" : "spawn-failed" };
|
|
2227
2296
|
}
|
|
2228
2297
|
else if (!verificationExecutableMissing(first)) {
|
|
@@ -2419,7 +2488,7 @@ async function settleProof(captured, statArtifactId, sealedHead) {
|
|
|
2419
2488
|
now,
|
|
2420
2489
|
});
|
|
2421
2490
|
if (captureResult !== null) {
|
|
2422
|
-
reviewContext = captureResult.inventory.coverage.map(one => ({ id: one.id, state: one.state, inherited: one.inherited, items: one.items, gaps: one.gaps, priorSupport: one.priorSupport }));
|
|
2491
|
+
reviewContext = captureResult.inventory.coverage.map(one => ({ id: one.id, state: one.state, inherited: one.inherited, items: one.items, gaps: one.gaps, priorSupport: one.priorSupport, ...(one.assets === undefined ? {} : { assets: one.assets }) }));
|
|
2423
2492
|
}
|
|
2424
2493
|
}
|
|
2425
2494
|
catch {
|
|
@@ -3230,9 +3299,12 @@ flowCuts = []) {
|
|
|
3230
3299
|
" changed-file inventory or final check results. This applies to no-change",
|
|
3231
3300
|
" results too. Put useful caveats in the short handoff; do not invent evidence.",
|
|
3232
3301
|
" Run focused checks for your edits. The machine runs the approved full check.",
|
|
3302
|
+
" Save screenshots and journey output only under evidence/ (the run's",
|
|
3303
|
+
" evidence folder). They are never committed: images added there are left",
|
|
3304
|
+
" out of the commit, so do not save them anywhere else in the repository.",
|
|
3233
3305
|
" If screenshots are required, capture the actual candidate and list them in",
|
|
3234
3306
|
` ${proof}: { "version": 1, "screenshots": [`,
|
|
3235
|
-
' { "path": "<
|
|
3307
|
+
' { "path": "<evidence/… PNG or JPEG>", "caption": "<what it shows>" } ] }.',
|
|
3236
3308
|
` List at most ${PROOF_LIMITS.screenshots} screenshots, with paths/captions under ${PROOF_LIMITS.evidenceRef} UTF-8 bytes,`,
|
|
3237
3309
|
" on one line each; no absolute paths or dot segments. Images must be real,",
|
|
3238
3310
|
" at least 320 by 200 pixels. A list is optional; missing required images",
|
|
@@ -3355,7 +3427,7 @@ function fence(text) {
|
|
|
3355
3427
|
* code and concluded the task needed no change has done its job, and turning
|
|
3356
3428
|
* that into a failure would teach the loop to prefer writing something.
|
|
3357
3429
|
*/
|
|
3358
|
-
async function commit(git, worktree, branch, taskId, scope, summary, preparedCandidate = null) {
|
|
3430
|
+
async function commit(git, worktree, branch, taskId, scope, summary, preparedCandidate = null, notice) {
|
|
3359
3431
|
const status = await git(GIT, ["--no-optional-locks", "status", "--porcelain"], { cwd: worktree });
|
|
3360
3432
|
if (status.code !== 0) {
|
|
3361
3433
|
return { ok: false, reason: "commit-failure", message: firstLine(status.stderr) };
|
|
@@ -3375,6 +3447,20 @@ async function commit(git, worktree, branch, taskId, scope, summary, preparedCan
|
|
|
3375
3447
|
const add = await git(GIT, ["add", "-A", "--", ".", `:!${LEASE_MARKER}`, ":!STANDING-ORDERS-*", ":!NIGHTORDERS-*"], { cwd: worktree });
|
|
3376
3448
|
if (add.code !== 0)
|
|
3377
3449
|
return { ok: false, reason: "commit-failure", message: firstLine(add.stderr) };
|
|
3450
|
+
// Screenshots belong to the run's evidence, not the branch. Images added
|
|
3451
|
+
// under evidence/ are unstaged and left on disk, where the proof can still
|
|
3452
|
+
// name them; an exact prepared candidate is compared whole instead.
|
|
3453
|
+
if (preparedCandidate === null) {
|
|
3454
|
+
const stripped = await stripEvidenceImages(git, worktree);
|
|
3455
|
+
if (!stripped.ok)
|
|
3456
|
+
return { ok: false, reason: "commit-failure", message: stripped.message };
|
|
3457
|
+
if (stripped.notice !== null) {
|
|
3458
|
+
notice?.(stripped.notice);
|
|
3459
|
+
summary = `${summary}\n\n${stripped.notice}`;
|
|
3460
|
+
if (!stripped.staged)
|
|
3461
|
+
return { ok: true, committed: false, branch, summary };
|
|
3462
|
+
}
|
|
3463
|
+
}
|
|
3378
3464
|
// Setup can create untracked files as well as edit tracked manifests.
|
|
3379
3465
|
// Re-prove the whole staged tree after the normal path exclusions, before
|
|
3380
3466
|
// committing, so neither can widen an exact prepared candidate.
|
|
@@ -3409,6 +3495,26 @@ async function commit(git, worktree, branch, taskId, scope, summary, preparedCan
|
|
|
3409
3495
|
}
|
|
3410
3496
|
return { ok: true, committed: true, branch, summary };
|
|
3411
3497
|
}
|
|
3498
|
+
/** The plain words for images left out of a commit. */
|
|
3499
|
+
export function evidenceImagesNotice(paths) {
|
|
3500
|
+
return `Left ${paths.length} image${paths.length === 1 ? "" : "s"} under ${WORKTREE_EVIDENCE_DIR} out of the commit: screenshots belong in the run's evidence, not the repository.`;
|
|
3501
|
+
}
|
|
3502
|
+
/** Unstage images newly added under evidence/; the files stay on disk. */
|
|
3503
|
+
async function stripEvidenceImages(git, worktree) {
|
|
3504
|
+
const added = await git(GIT, ["--no-optional-locks", "diff", "--cached", "--name-only", "-z", "--no-renames", "--diff-filter=A", "--", WORKTREE_EVIDENCE_DIR], { cwd: worktree });
|
|
3505
|
+
if (added.code !== 0)
|
|
3506
|
+
return { ok: false, message: firstLine(added.stderr) };
|
|
3507
|
+
const images = added.stdout.split("\0").filter(path => path !== "" && isImagePath(path));
|
|
3508
|
+
if (images.length === 0)
|
|
3509
|
+
return { ok: true, notice: null, staged: true };
|
|
3510
|
+
for (let at = 0; at < images.length; at += 200) {
|
|
3511
|
+
const removed = await git(GIT, ["rm", "--cached", "--quiet", "--", ...images.slice(at, at + 200).map(path => `:(literal)${path}`)], { cwd: worktree });
|
|
3512
|
+
if (removed.code !== 0)
|
|
3513
|
+
return { ok: false, message: firstLine(removed.stderr) };
|
|
3514
|
+
}
|
|
3515
|
+
const rest = await git(GIT, ["--no-optional-locks", "diff", "--cached", "--quiet"], { cwd: worktree });
|
|
3516
|
+
return { ok: true, notice: evidenceImagesNotice(images), staged: rest.code !== 0 };
|
|
3517
|
+
}
|
|
3412
3518
|
/**
|
|
3413
3519
|
* `claude --output-format json` returns an envelope: the result is the
|
|
3414
3520
|
* summary, and the session id is what lets a malformed park be repaired by
|
package/dist/chat-controls.d.ts
CHANGED
package/dist/chat-controls.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
export const CHAT_CONTROLS = {
|
|
5
5
|
task: { label: "Open task", target: "task" },
|
|
6
6
|
approval: { label: "Review & start", target: "task" },
|
|
7
|
-
planning: { label: "
|
|
7
|
+
planning: { label: "Approve plan", target: "task" },
|
|
8
8
|
recovery: { label: "Review recovery options", target: "task" },
|
|
9
9
|
cancel: { label: "Cancel task", target: "task" },
|
|
10
10
|
result: { label: "Open result", target: "task" },
|
|
@@ -14,6 +14,11 @@ export type CheckSettingsView = {
|
|
|
14
14
|
quick: VerifyCommand | null;
|
|
15
15
|
/** A starting point for the quick command, from the project's scripts. */
|
|
16
16
|
suggestion: string | null;
|
|
17
|
+
/** The project's automatic review switch (review-switch.ts), beside its checks. */
|
|
18
|
+
review?: {
|
|
19
|
+
on: boolean;
|
|
20
|
+
source: "project" | "hands-off" | "default";
|
|
21
|
+
};
|
|
17
22
|
said: string | null;
|
|
18
23
|
problem: string | null;
|
|
19
24
|
};
|
package/dist/check-levels-ui.js
CHANGED
|
@@ -19,7 +19,7 @@ export function checkSettingsHtml(view) {
|
|
|
19
19
|
(fallsBack ? `<p class="meta">No quick command yet, so builds run the full check.</p>` : "") +
|
|
20
20
|
(view.level !== "full" && view.full !== null ? `<p class="meta">The full check still runs when a pull request opens, and Merge waits for it.</p>` : "");
|
|
21
21
|
if (!view.canChange) {
|
|
22
|
-
return head + `<section class="card check-settings">${current}</section>` +
|
|
22
|
+
return head + `<section class="card check-settings">${current}</section>` + reviewHtml(view, "") +
|
|
23
23
|
`<section class="card check-settings"><h2>Quick check</h2>${view.quick === null ? `<p class="meta">None yet.</p>` : command(view.quick)}</section>` +
|
|
24
24
|
`<section class="card check-settings"><h2>Full check</h2>${view.full === null ? `<p class="meta">None yet.</p>` : command(view.full)}</section>`;
|
|
25
25
|
}
|
|
@@ -45,7 +45,21 @@ export function checkSettingsHtml(view) {
|
|
|
45
45
|
`<p class="meta">Quick builds run the full check instead.</p>${password}<button type="submit" class="danger">Remove</button></form></details></section>`;
|
|
46
46
|
const full = `<details class="settings-more"><summary>Full check</summary><section class="card check-settings">` +
|
|
47
47
|
(view.full === null ? `<p class="meta">None yet. Approve one with <span class="mono">toolroll verify set --repo … --command "…"</span>.</p>` : command(view.full)) + `</section></details>`;
|
|
48
|
-
return head + levelForm + quick + full;
|
|
48
|
+
return head + levelForm + reviewHtml(view, password) + quick + full;
|
|
49
|
+
}
|
|
50
|
+
/** Automatic review: one state, one sentence, and its one change behind a disclosure (the password). */
|
|
51
|
+
function reviewHtml(view, password) {
|
|
52
|
+
const review = view.review;
|
|
53
|
+
if (review === undefined)
|
|
54
|
+
return "";
|
|
55
|
+
const state = review.on ? (review.source === "hands-off" ? "On while hands-off lasts" : "On") : "Off";
|
|
56
|
+
const act = review.on ? "Turn off" : "Turn on";
|
|
57
|
+
const change = password === "" ? ""
|
|
58
|
+
: `<details class="settings-more"><summary>${act}</summary><form method="post" action="/settings/checks" class="check-settings">` +
|
|
59
|
+
`<input type="hidden" name="csrf" value="${escape(view.csrf)}"><input type="hidden" name="repo" value="${escape(view.repo)}"><input type="hidden" name="act" value="review">` +
|
|
60
|
+
`<input type="hidden" name="on" value="${review.on ? "0" : "1"}">${password}<button type="submit">${act}</button></form></details>`;
|
|
61
|
+
return `<section class="card check-settings" data-review-switch="${review.on ? "on" : "off"}"><h2>Automatic review</h2>` +
|
|
62
|
+
`<p><strong>${state}</strong> · One read-only review after each build passes its checks. Only high findings send it back, once.</p>${change}</section>`;
|
|
49
63
|
}
|
|
50
64
|
export const CHECK_SETTINGS_CSS = `
|
|
51
65
|
.check-settings { display: grid; gap: .6rem; max-width: 40rem; }
|
package/dist/check-progress.js
CHANGED
|
@@ -153,9 +153,10 @@ export class CheckProgressTracker {
|
|
|
153
153
|
}
|
|
154
154
|
return;
|
|
155
155
|
}
|
|
156
|
-
// scripts/e2e-parallel.mjs: "Running 7 groups at once
|
|
157
|
-
//
|
|
158
|
-
|
|
156
|
+
// scripts/e2e-parallel.mjs: "Running 7 groups, at most 4 at once (12.0 GB available, about 400 MB each): console,
|
|
157
|
+
// task, …" (before: "Running 7 groups at once: …"), then every group's lines as "[group] [app] …", then
|
|
158
|
+
// "✅ group 3.1 min" per group and "N of M groups passed …".
|
|
159
|
+
const starting = /^Running ([0-9]+) groups(?:, at most [0-9]+)? at once(?: \([^)]*\))?: (.+)$/.exec(line);
|
|
159
160
|
if (starting !== null) {
|
|
160
161
|
const total = safeCount(starting[1]);
|
|
161
162
|
const names = starting[2].split(",").map(one => one.trim()).filter(one => one !== "");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type CheckoutCleanup } from "./storage.js";
|
|
2
|
-
import type { Store } from "./store.js";
|
|
2
|
+
import type { Store, WorktreeRow } from "./store.js";
|
|
3
3
|
import { type WorktreePool } from "./worktree.js";
|
|
4
4
|
/** A release candidate's checkout holds the build a deploy installs: kept a week after its task was done. */
|
|
5
5
|
export declare const CANDIDATE_KEEP_MS: number;
|
|
@@ -36,12 +36,33 @@ type Status = {
|
|
|
36
36
|
taskId: string;
|
|
37
37
|
finishedAt: string | null;
|
|
38
38
|
dueAt?: string;
|
|
39
|
+
atOnce?: boolean;
|
|
39
40
|
};
|
|
41
|
+
/** The release a deploy installed last (staged-upgrades/<stage>/deployment.json at phase "deployed"): its commit and
|
|
42
|
+
* builder run. A deploy runs from the gate's checkout, so that checkout stays while it is what runs. */
|
|
43
|
+
export type Deployed = {
|
|
44
|
+
head: string | null;
|
|
45
|
+
run: number | null;
|
|
46
|
+
};
|
|
47
|
+
export declare function deployedRelease(stateDir: string | null): Deployed | null;
|
|
48
|
+
/**
|
|
49
|
+
* Which release a gate belongs to: a failed gate is re-filed as the same name plus a letter (`release-x`, then
|
|
50
|
+
* `release-xb`, `release-xc` …), so a gate whose name is another gate's in the same project plus one letter is that
|
|
51
|
+
* release's.
|
|
52
|
+
*/
|
|
53
|
+
export declare function releaseOf(id: string, gates: ReadonlySet<string>): string;
|
|
40
54
|
/** Where every task stands for cleanup, by task_ref id and by each branch it or its runs used. */
|
|
41
|
-
export declare function taskStatuses(store: Store, now: Date): {
|
|
55
|
+
export declare function taskStatuses(store: Store, now: Date, deployed?: Deployed | null): {
|
|
42
56
|
byRef: Map<number, Status>;
|
|
43
57
|
byBranch: Map<string, Status>;
|
|
44
58
|
};
|
|
59
|
+
/** The let-go checkouts whose task is complete or cancelled, and when it finished. Not a release candidate's that
|
|
60
|
+
* stays: a deploy runs from it. */
|
|
61
|
+
export declare function finishedCheckouts(store: Store, now: Date, repo?: string): {
|
|
62
|
+
row: WorktreeRow;
|
|
63
|
+
taskId: string | null;
|
|
64
|
+
finishedAt: string;
|
|
65
|
+
}[];
|
|
45
66
|
/**
|
|
46
67
|
* What a clean-up would remove now, how much space it would free, and what stays and why. `manual` is a clean-up by
|
|
47
68
|
* hand (it doesn't wait for the setting); otherwise it is the worker's pass under the setting. Reads only.
|
|
@@ -71,6 +92,23 @@ export declare function cleanCheckouts(store: Store, pool: WorktreePool, now: ()
|
|
|
71
92
|
repo?: string;
|
|
72
93
|
only?: ReadonlySet<string>;
|
|
73
94
|
}): Promise<CleanResult>;
|
|
95
|
+
export type SlimResult = {
|
|
96
|
+
slimmed: {
|
|
97
|
+
path: string;
|
|
98
|
+
taskId: string | null;
|
|
99
|
+
dropped: string[];
|
|
100
|
+
bytes: number;
|
|
101
|
+
}[];
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* A kept checkout (its result waiting for review, or with changes) drops its dependencies and build output once its
|
|
105
|
+
* run has ended; the next run's setup restores them (node_modules only goes where the project has a setup to restore
|
|
106
|
+
* it). Never one in use or a release candidate (a deploy installs from it). Each in the ledger.
|
|
107
|
+
*/
|
|
108
|
+
export declare function slimKeptCheckouts(store: Store, pool: WorktreePool, now: () => Date, options: {
|
|
109
|
+
actor: string;
|
|
110
|
+
repo?: string;
|
|
111
|
+
}): Promise<SlimResult>;
|
|
74
112
|
/** Throw away a checkout kept for its changes, on purpose; its branch stays. In the ledger. */
|
|
75
113
|
export declare function discardCheckout(store: Store, pool: WorktreePool, path: string, now: Date, actor: string): Promise<{
|
|
76
114
|
ok: true;
|
package/dist/checkout-cleanup.js
CHANGED
|
@@ -6,13 +6,18 @@
|
|
|
6
6
|
* Only the working copy goes: its branch, and so every commit, stays, and a later lease of the branch makes a new one.
|
|
7
7
|
*
|
|
8
8
|
* Always kept: a checkout in use, one whose task isn't finished (on hold, failed, a revision under way), a result
|
|
9
|
-
* still waiting for review, a
|
|
10
|
-
*
|
|
11
|
-
* (
|
|
9
|
+
* still waiting for review, a checkout with changes (files Toolroll wrote itself aside), and one with commits on no
|
|
10
|
+
* branch. A release candidate's checkout (a deploy installs from it) is kept only while it is the newest gate for its
|
|
11
|
+
* release (for a week after it was done) or the one deployed; a superseded or cancelled gate goes at once, whatever
|
|
12
|
+
* its task's state (staged releases keep the rollback copy). A checkout no task names (adopted after a crash) goes only
|
|
13
|
+
* in a clean-up by hand. Every removal is in the ledger.
|
|
14
|
+
*
|
|
15
|
+
* A kept checkout waiting for review or with changes drops its dependencies and build output once its run ends
|
|
16
|
+
* (`slimKeptCheckouts`); the next run's setup restores them.
|
|
12
17
|
*/
|
|
13
18
|
import { createHash } from "node:crypto";
|
|
14
|
-
import { existsSync } from "node:fs";
|
|
15
|
-
import { basename } from "node:path";
|
|
19
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
20
|
+
import { basename, dirname, join } from "node:path";
|
|
16
21
|
import { taskBranches } from "./names.js";
|
|
17
22
|
import { COMPLETION_ACTION } from "./result-completion.js";
|
|
18
23
|
import { CLEANUP_CHOICES, bytesWords, diskBytes } from "./storage.js";
|
|
@@ -24,19 +29,80 @@ export const CANDIDATE_KEEP_MS = 7 * DAY_MS;
|
|
|
24
29
|
export const AUTO_MAX = 20;
|
|
25
30
|
/** Git inspections at once. */
|
|
26
31
|
const INSPECT_AT_ONCE = 8;
|
|
32
|
+
export function deployedRelease(stateDir) {
|
|
33
|
+
if (stateDir === null)
|
|
34
|
+
return null;
|
|
35
|
+
const root = join(stateDir, "staged-upgrades");
|
|
36
|
+
let newest = null;
|
|
37
|
+
let names = [];
|
|
38
|
+
try {
|
|
39
|
+
names = readdirSync(root);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
for (const name of names) {
|
|
45
|
+
let journal;
|
|
46
|
+
try {
|
|
47
|
+
journal = JSON.parse(readFileSync(join(root, name, "deployment.json"), "utf8"));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (journal["phase"] !== "deployed")
|
|
53
|
+
continue;
|
|
54
|
+
const at = typeof journal["deployedAt"] === "string" ? journal["deployedAt"] : typeof journal["updatedAt"] === "string" ? journal["updatedAt"] : "";
|
|
55
|
+
if (newest !== null && newest.at >= at)
|
|
56
|
+
continue;
|
|
57
|
+
newest = { at, head: typeof journal["candidate"] === "string" ? journal["candidate"] : null, run: Number.isSafeInteger(journal["builder"]) ? Number(journal["builder"]) : null };
|
|
58
|
+
}
|
|
59
|
+
return newest === null ? null : { head: newest.head, run: newest.run };
|
|
60
|
+
}
|
|
61
|
+
/** The folder beside the database (where staged releases live), or null for a database in memory. */
|
|
62
|
+
function stateDirOf(store) {
|
|
63
|
+
const main = store.handle.prepare("PRAGMA database_list").all().find(row => row["name"] === "main");
|
|
64
|
+
return typeof main?.["file"] === "string" && main["file"] !== "" ? dirname(main["file"]) : null;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Which release a gate belongs to: a failed gate is re-filed as the same name plus a letter (`release-x`, then
|
|
68
|
+
* `release-xb`, `release-xc` …), so a gate whose name is another gate's in the same project plus one letter is that
|
|
69
|
+
* release's.
|
|
70
|
+
*/
|
|
71
|
+
export function releaseOf(id, gates) {
|
|
72
|
+
return /[b-z]$/.test(id) && gates.has(id.slice(0, -1)) ? id.slice(0, -1) : id;
|
|
73
|
+
}
|
|
27
74
|
const STRENGTH = { "in use": 5, "task not finished": 4, "waiting for review": 3, "release candidate": 2 };
|
|
28
75
|
const strength = (one) => one === undefined ? -1 : one.keep === null ? 0 : STRENGTH[one.keep] ?? 1;
|
|
29
76
|
const stronger = (a, b) => strength(b) > strength(a) || (strength(b) === 0 && strength(a) === 0 && (b.finishedAt ?? "") > (a.finishedAt ?? "")) ? b : a;
|
|
30
77
|
/** Where every task stands for cleanup, by task_ref id and by each branch it or its runs used. */
|
|
31
|
-
export function taskStatuses(store, now) {
|
|
78
|
+
export function taskStatuses(store, now, deployed) {
|
|
32
79
|
const db = store.handle;
|
|
33
80
|
const refs = db.prepare(`SELECT r.id AS ref, r.external_id AS id, r.revision_of AS revision_of, t.state AS state, t.updated_at AS updated_at,
|
|
34
81
|
EXISTS (SELECT 1 FROM hold h WHERE h.task_ref = r.id) AS held,
|
|
35
82
|
EXISTS (SELECT 1 FROM run live WHERE live.task_ref = r.id AND live.finished_at IS NULL) AS live,
|
|
36
|
-
|
|
83
|
+
(SELECT MAX(s.candidate) FROM task_scope s WHERE s.task_id = r.external_id) AS candidate, r.repo AS repo
|
|
37
84
|
FROM task_ref r LEFT JOIN task t ON t.id = r.external_id AND r.backend = 'built-in'`).all();
|
|
38
85
|
const runs = db.prepare("SELECT id, task_ref, branch FROM run").all();
|
|
39
86
|
const refOfRun = new Map(runs.map(row => [Number(row["id"]), Number(row["task_ref"])]));
|
|
87
|
+
// Release candidates: per project and release, the newest gate not cancelled; and the one deployed.
|
|
88
|
+
const gates = refs.filter(row => row["candidate"] !== null);
|
|
89
|
+
const gateIds = new Map();
|
|
90
|
+
for (const row of gates) {
|
|
91
|
+
const repo = String(row["repo"]);
|
|
92
|
+
gateIds.set(repo, (gateIds.get(repo) ?? new Set()).add(String(row["id"])));
|
|
93
|
+
}
|
|
94
|
+
const releaseKey = (row) => `${String(row["repo"])}\u0000${releaseOf(String(row["id"]), gateIds.get(String(row["repo"])))}`;
|
|
95
|
+
const newestGate = new Map();
|
|
96
|
+
for (const row of gates) {
|
|
97
|
+
if (row["state"] === "cancelled")
|
|
98
|
+
continue;
|
|
99
|
+
const key = releaseKey(row);
|
|
100
|
+
newestGate.set(key, Math.max(newestGate.get(key) ?? -1, Number(row["ref"])));
|
|
101
|
+
}
|
|
102
|
+
const live = deployed === undefined ? deployedRelease(stateDirOf(store)) : deployed;
|
|
103
|
+
const deployedRef = live === null ? undefined : live.run !== null ? refOfRun.get(live.run) : undefined;
|
|
104
|
+
const isDeployed = (row) => live !== null &&
|
|
105
|
+
(Number(row["ref"]) === deployedRef || (live.head !== null && String(row["candidate"]) === live.head));
|
|
40
106
|
// The completion names its family's root; its run names the result it completes.
|
|
41
107
|
const completed = new Map();
|
|
42
108
|
const completedRef = new Map();
|
|
@@ -59,16 +125,22 @@ export function taskStatuses(store, now) {
|
|
|
59
125
|
const id = String(row["id"]);
|
|
60
126
|
const state = row["state"] === null ? null : String(row["state"]);
|
|
61
127
|
const updated = row["updated_at"] === null ? null : String(row["updated_at"]);
|
|
128
|
+
const gate = row["candidate"] !== null;
|
|
62
129
|
let status;
|
|
63
130
|
if (Number(row["live"]) === 1)
|
|
64
131
|
status = { keep: "in use", taskId: id, finishedAt: null };
|
|
132
|
+
else if (gate && isDeployed(row))
|
|
133
|
+
status = { keep: "release candidate", taskId: id, finishedAt: updated };
|
|
134
|
+
// A superseded or cancelled gate goes at once, whatever its task's state: a newer gate (or none) is the release.
|
|
135
|
+
else if (gate && (state === "cancelled" || newestGate.get(releaseKey(row)) !== ref))
|
|
136
|
+
status = { keep: null, taskId: id, finishedAt: updated, atOnce: true };
|
|
65
137
|
else if (state === null || Number(row["held"]) === 1 || (state !== "done" && state !== "cancelled"))
|
|
66
138
|
status = { keep: "task not finished", taskId: id, finishedAt: null };
|
|
67
139
|
else if (state === "cancelled")
|
|
68
140
|
status = { keep: null, taskId: id, finishedAt: updated };
|
|
69
141
|
else {
|
|
70
142
|
const completedAt = [completed.get(id), completedRef.get(ref)].filter((one) => one !== undefined).sort().at(-1) ?? null;
|
|
71
|
-
if (
|
|
143
|
+
if (gate) {
|
|
72
144
|
const since = completedAt ?? updated ?? now.toISOString();
|
|
73
145
|
const until = new Date(Date.parse(since) + CANDIDATE_KEEP_MS).toISOString();
|
|
74
146
|
status = until > now.toISOString() ? { keep: "release candidate", taskId: id, finishedAt: since, dueAt: until } : { keep: null, taskId: id, finishedAt: since };
|
|
@@ -119,6 +191,17 @@ function statusOf(row, statuses) {
|
|
|
119
191
|
}
|
|
120
192
|
return found;
|
|
121
193
|
}
|
|
194
|
+
/** The let-go checkouts whose task is complete or cancelled, and when it finished. Not a release candidate's that
|
|
195
|
+
* stays: a deploy runs from it. */
|
|
196
|
+
export function finishedCheckouts(store, now, repo) {
|
|
197
|
+
const statuses = taskStatuses(store, now);
|
|
198
|
+
return store.listWorktrees().filter(row => row.releasedAt !== null && row.runner === null && (repo === undefined || row.repo === repo)).flatMap(row => {
|
|
199
|
+
const status = statusOf(row, statuses);
|
|
200
|
+
if (status === undefined || status.finishedAt === null || status.keep !== null)
|
|
201
|
+
return [];
|
|
202
|
+
return [{ row, taskId: status.taskId, finishedAt: status.finishedAt }];
|
|
203
|
+
});
|
|
204
|
+
}
|
|
122
205
|
/** Why a checkout stays before anyone looks at its files, or null: it may go (as far as its task and the setting say). */
|
|
123
206
|
function keptFor(pool, row, statuses, now, cleanup) {
|
|
124
207
|
const status = statusOf(row, statuses);
|
|
@@ -136,6 +219,8 @@ function keptFor(pool, row, statuses, now, cleanup) {
|
|
|
136
219
|
return { why: null, taskId };
|
|
137
220
|
if (cleanup === "never")
|
|
138
221
|
return { why: "cleanup is off", taskId };
|
|
222
|
+
if (status.atOnce === true)
|
|
223
|
+
return { why: null, taskId };
|
|
139
224
|
const days = CLEANUP_CHOICES.find(one => one.value === cleanup).days;
|
|
140
225
|
const since = [status.finishedAt, row.releasedAt].filter((one) => one !== null).sort().at(-1);
|
|
141
226
|
const dueAt = new Date(Date.parse(since) + days * DAY_MS).toISOString();
|
|
@@ -215,6 +300,41 @@ export async function cleanCheckouts(store, pool, now, options) {
|
|
|
215
300
|
}
|
|
216
301
|
return { removed, kept: pruned.kept, freed: removed.reduce((sum, one) => sum + one.bytes, 0) };
|
|
217
302
|
}
|
|
303
|
+
/** Checkouts this process already looked at since they were let go (path and release time): looked at once. */
|
|
304
|
+
const looked = new Set();
|
|
305
|
+
/**
|
|
306
|
+
* A kept checkout (its result waiting for review, or with changes) drops its dependencies and build output once its
|
|
307
|
+
* run has ended; the next run's setup restores them (node_modules only goes where the project has a setup to restore
|
|
308
|
+
* it). Never one in use or a release candidate (a deploy installs from it). Each in the ledger.
|
|
309
|
+
*/
|
|
310
|
+
export async function slimKeptCheckouts(store, pool, now, options) {
|
|
311
|
+
const rows = store.listWorktrees().filter(row => row.releasedAt !== null && row.runner === null && (options.repo === undefined || row.repo === options.repo)
|
|
312
|
+
&& !looked.has(`${row.path}\u0000${row.releasedAt}`) && existsSync(row.path));
|
|
313
|
+
const slimmed = [];
|
|
314
|
+
if (rows.length === 0)
|
|
315
|
+
return { slimmed };
|
|
316
|
+
const keptAs = (row) => keptFor(pool, row, taskStatuses(store, now()), now(), "manual");
|
|
317
|
+
const statuses = taskStatuses(store, now());
|
|
318
|
+
for (const row of rows) {
|
|
319
|
+
const kept = keptFor(pool, row, statuses, now(), "manual");
|
|
320
|
+
if (kept.why === "in use")
|
|
321
|
+
continue;
|
|
322
|
+
looked.add(`${row.path}\u0000${row.releasedAt}`);
|
|
323
|
+
if (kept.why === "release candidate")
|
|
324
|
+
continue;
|
|
325
|
+
if (kept.why !== "waiting for review" && await pool.inspect(row.path) !== "has changes")
|
|
326
|
+
continue;
|
|
327
|
+
const wanted = (fresh) => { const why = keptAs(fresh).why; return why !== "in use" && why !== "release candidate"; };
|
|
328
|
+
const done = await pool.slim(row, wanted, store.liveWorktreeSetup(row.repo) !== null);
|
|
329
|
+
if (!done.ok || done.dropped.length === 0)
|
|
330
|
+
continue;
|
|
331
|
+
const bytes = done.bytes;
|
|
332
|
+
slimmed.push({ path: row.path, taskId: kept.taskId, dropped: done.dropped, bytes });
|
|
333
|
+
store.recordAction({ at: now().toISOString(), actor: options.actor, repo: row.repo, taskId: kept.taskId, runId: null, action: "checkout slimmed", outcome: "removed", source: "work",
|
|
334
|
+
detail: `${basename(row.path)}: dropped ${done.dropped.join(", ")}${bytes > 0 ? ` (about ${bytesWords(bytes)})` : ""}; the next run's setup restores them` });
|
|
335
|
+
}
|
|
336
|
+
return { slimmed };
|
|
337
|
+
}
|
|
218
338
|
/** Throw away a checkout kept for its changes, on purpose; its branch stays. In the ledger. */
|
|
219
339
|
export async function discardCheckout(store, pool, path, now, actor) {
|
|
220
340
|
const bytes = existsSync(path) ? diskBytes([path]).get(path) ?? 0 : 0;
|
|
@@ -233,7 +353,7 @@ export function whyWords(item) {
|
|
|
233
353
|
case "in use": return "in use";
|
|
234
354
|
case "task not finished": return "its task isn't finished";
|
|
235
355
|
case "waiting for review": return "its result is waiting for review";
|
|
236
|
-
case "release candidate": return `release candidate, kept until ${item.dueAt
|
|
356
|
+
case "release candidate": return item.dueAt === undefined ? "the deployed release candidate" : `newest release candidate, kept until ${item.dueAt.slice(0, 10)}`;
|
|
237
357
|
case "has changes": return "has changes";
|
|
238
358
|
case "has commits": return "has commits on no branch";
|
|
239
359
|
case "unreadable": return "git couldn't read it";
|
package/dist/cli.js
CHANGED
|
@@ -213,6 +213,7 @@ export const OPERATE_COMMANDS = new Set([
|
|
|
213
213
|
"restore",
|
|
214
214
|
"export",
|
|
215
215
|
"policy",
|
|
216
|
+
"review",
|
|
216
217
|
"assignment",
|
|
217
218
|
"knowledge",
|
|
218
219
|
"memory",
|
|
@@ -243,6 +244,7 @@ export const OPERATE_COMMANDS = new Set([
|
|
|
243
244
|
"bridge",
|
|
244
245
|
"publish",
|
|
245
246
|
"reconcile",
|
|
247
|
+
"run",
|
|
246
248
|
"routine",
|
|
247
249
|
"flows",
|
|
248
250
|
"config", "chat", "proposals", "mode", "people", "keys",
|