toolroll 0.9.8 → 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 +22 -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 +9 -0
- package/dist/assignment.js +39 -7
- 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/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-progress.js +4 -3
- package/dist/checkout-cleanup.d.ts +40 -2
- package/dist/checkout-cleanup.js +129 -9
- package/dist/desktop-update.d.ts +52 -0
- package/dist/desktop-update.js +147 -5
- package/dist/evidence.d.ts +6 -0
- package/dist/evidence.js +8 -0
- package/dist/first-run.d.ts +1 -1
- package/dist/first-run.js +7 -5
- 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/needs-you.d.ts +55 -0
- package/dist/needs-you.js +83 -0
- package/dist/operate.d.ts +2 -0
- package/dist/operate.js +112 -14
- package/dist/plane-review.js +4 -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/result-completion.d.ts +4 -0
- package/dist/result-completion.js +11 -0
- package/dist/review-context.d.ts +3 -0
- package/dist/review-context.js +45 -5
- 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 +129 -14
- 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 +36 -0
- package/dist/store.js +125 -29
- package/dist/task-status.d.ts +18 -4
- package/dist/task-status.js +35 -18
- 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/workspace-ui.js +1 -1
- package/dist/worktree.d.ts +16 -0
- package/dist/worktree.js +47 -0
- package/package.json +1 -1
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/** Every reason a task, result or flow card can wait on a person, in one
|
|
2
|
+
* place: the plain sentence it shows and the one action that resolves it
|
|
3
|
+
* (task-status.ts reads it for every surface). A reason no person can act
|
|
4
|
+
* on is not Needs you: it is a Waiting reason, worded as what it waits for.
|
|
5
|
+
*
|
|
6
|
+
* The words never name the machine's internals; `plainSentence` keeps a
|
|
7
|
+
* recorded reason only when it is free of them. */
|
|
8
|
+
const always = () => true;
|
|
9
|
+
const build = (context) => context.build == null ? "the last build" : `build #${context.build}`;
|
|
10
|
+
const Build = (context) => context.build == null ? "The last build" : `Build #${context.build}`;
|
|
11
|
+
export const NEEDS = {
|
|
12
|
+
approval: { sentence: () => "Review the plan and approve it to start.", action: { code: "approve-scope", label: "Approve plan" } },
|
|
13
|
+
card: { sentence: () => "This card is waiting for your approval.", action: { code: "approve-card", label: "Approve card" } },
|
|
14
|
+
answer: { sentence: () => "Answer the question so the work can continue.", action: { code: "answer-decision", label: "Answer the question" }, useReason: always },
|
|
15
|
+
questions: { sentence: () => "Too many questions are open. Answer one so this task can start.", action: { code: "inspect-decisions", label: "Answer questions" } },
|
|
16
|
+
"sign-in": { sentence: context => `${context.provider ?? "Your agent"} needs you to sign in again. The task starts on its own after.`, action: { code: "sign-in", label: "Sign in again" }, useReason: reason => /sign in|API key/i.test(reason) },
|
|
17
|
+
"confirm-stopped": { sentence: context => `Toolroll can't confirm ${build(context)} stopped. Nothing from it is running.`, action: { code: "confirm-stopped", label: "Confirm it stopped" } },
|
|
18
|
+
// Toolroll can't look at all: never "nothing is running"; the person checks, says so, then confirms.
|
|
19
|
+
"check-stopped": { sentence: context => `Toolroll can't check whether ${build(context)} stopped. Make sure nothing from it is running, then confirm.`, action: { code: "confirm-stopped", label: "Confirm it stopped" } },
|
|
20
|
+
hold: { sentence: () => "A hold is keeping this task from starting. Review it to release it.", action: { code: "inspect-hold", label: "Review hold" } },
|
|
21
|
+
"choose-project": { sentence: () => "Choose a project so a builder can start.", action: { code: "place-task", label: "Choose a project" } },
|
|
22
|
+
"define-task": { sentence: () => "Say what the task should do so it can be planned.", action: { code: "write-scope", label: "Define the task" } },
|
|
23
|
+
"fix-request": { sentence: () => "The request can't be planned as written. Edit it so the planner can start.", action: { code: "write-scope", label: "Edit the request" } },
|
|
24
|
+
"choose-agent": { sentence: () => "Choose an agent to build this.", action: { code: "select-agent", label: "Choose an agent" } },
|
|
25
|
+
"fix-dependency": { sentence: () => "A task this one waits on didn't finish. Fix or remove it so this one can start.", action: { code: "repair-dependency", label: "Fix the required task" },
|
|
26
|
+
useReason: reason => /didn't finish|did not finish|before it finished/i.test(reason), withReason: reason => `${reason.replace(/[.\s]*$/, ".")} Fix or remove it so this one can start.` },
|
|
27
|
+
"add-requirement": { sentence: () => "This project is missing something the task needs. Add it so a builder can start.", action: { code: "repair-capability", label: "Add the requirement" } },
|
|
28
|
+
"connect-builder": { sentence: () => "No builder works on this project yet. Connect one so the task can start.", action: { code: "start-worker", label: "Connect a builder" } },
|
|
29
|
+
"start-builder": { sentence: () => "The builder for this project is offline. Start it and the task begins on its own.", action: { code: "start-worker", label: "Start the builder" } },
|
|
30
|
+
vanished: { sentence: context => `${Build(context)} stopped without finishing. Check it, then retry.`, action: { code: "reconcile-run", label: "Check the build" } },
|
|
31
|
+
"review-result": { sentence: () => "Check the result, then accept it or ask for changes.", action: { code: "open-result", label: "Review result" } },
|
|
32
|
+
// Accepting it would not help: it was built to terms that are no longer the plan.
|
|
33
|
+
rebuild: { sentence: context => `${Build(context)} was made to an earlier plan. Build it again to the current plan.`, action: { code: "retry-task", label: "Build again" } },
|
|
34
|
+
"earlier-version": { sentence: () => "An earlier version of this task still needs you. Finish it first.", action: { code: "inspect-task", label: "Open earlier version" } },
|
|
35
|
+
other: { sentence: () => "Open the task to see what it needs from you.", action: { code: "inspect-task", label: "Open the task" }, useReason: always },
|
|
36
|
+
};
|
|
37
|
+
export const WAITS = {
|
|
38
|
+
"build-stopping": context => `Waiting for ${build(context)} to stop. Nothing is needed from you.`,
|
|
39
|
+
"other-computer": context => `Waiting for the computer that ran ${build(context)} to confirm it stopped.`,
|
|
40
|
+
"card-reply": () => "Waiting for a reply.",
|
|
41
|
+
"card-time": () => "Waiting before moving on.",
|
|
42
|
+
"card-ci": () => "Waiting for the pull request's checks.",
|
|
43
|
+
};
|
|
44
|
+
/** Words that name the machine's internals; no Needs you or Waiting sentence carries them. */
|
|
45
|
+
export const BANNED_WORDS = ["witness", "unproven", "quiescence", "quiescent", "digest", "scope digest"];
|
|
46
|
+
const BANNED = new RegExp(`\\b(?:${BANNED_WORDS.join("|")})\\b`, "i");
|
|
47
|
+
export const hasInternalWords = (text) => BANNED.test(text);
|
|
48
|
+
/** The sentence for a need: its recorded reason when the need keeps one and
|
|
49
|
+
* it reads plainly, otherwise the need's own words. */
|
|
50
|
+
export function needSentence(key, reason, context = {}) {
|
|
51
|
+
const need = NEEDS[key];
|
|
52
|
+
const plain = reason?.trim() || null;
|
|
53
|
+
if (need.useReason === undefined || plain === null || !need.useReason(plain) || hasInternalWords(plain))
|
|
54
|
+
return need.sentence(context);
|
|
55
|
+
return need.withReason === undefined ? plain : need.withReason(plain);
|
|
56
|
+
}
|
|
57
|
+
export function waitSentence(key, reason, context = {}) {
|
|
58
|
+
if (key !== undefined)
|
|
59
|
+
return WAITS[key](context);
|
|
60
|
+
const plain = reason?.trim() || null;
|
|
61
|
+
return plain !== null && !hasInternalWords(plain) ? plain : "Waiting for the work to continue on its own.";
|
|
62
|
+
}
|
|
63
|
+
/** A finished run Toolroll can't prove stopped (store.stopQuiescenceFact):
|
|
64
|
+
* a person can confirm it only when nothing of it may still be running.
|
|
65
|
+
* `unknown`: Toolroll can't check, so the person confirms they checked. */
|
|
66
|
+
export function processNeedOf(fact) {
|
|
67
|
+
if (fact === null)
|
|
68
|
+
return null;
|
|
69
|
+
if (fact.kind === "unprovable")
|
|
70
|
+
return { need: "confirm-stopped", build: fact.run };
|
|
71
|
+
if (fact.kind === "unknown")
|
|
72
|
+
return { need: "check-stopped", build: fact.run };
|
|
73
|
+
return { wait: fact.kind === "elsewhere" ? "other-computer" : "build-stopping", build: fact.run };
|
|
74
|
+
}
|
|
75
|
+
/** Dispatch and work-index codes that wait on a person, as their need. */
|
|
76
|
+
export const CODE_NEED = {
|
|
77
|
+
"signed-out": "sign-in", "needs-approval": "approval", "waiting-decision": "answer", "decision-queue": "answer",
|
|
78
|
+
"needs-project": "choose-project", "needs-scope": "define-task", "planner-source": "fix-request", "needs-agent-profile": "choose-agent",
|
|
79
|
+
"terminal-dependency": "fix-dependency", "missing-requirement": "add-requirement", "no-worker-registered": "connect-builder",
|
|
80
|
+
"no-worker-online": "start-builder", "vanished-run": "vanished", "needs-verification": "review-result", "proof-refuted": "review-result",
|
|
81
|
+
"no-build-record": "review-result", "verification-needed": "review-result", "evidence-mismatch": "review-result",
|
|
82
|
+
"record-incomplete": "review-result", "evidence-damaged": "review-result",
|
|
83
|
+
};
|
package/dist/operate.d.ts
CHANGED
|
@@ -37,6 +37,8 @@ export type OperateOptions = {
|
|
|
37
37
|
releaseIo?: ReleaseIo;
|
|
38
38
|
/** Injected by tests: the bin whose real path says how Toolroll was installed. */
|
|
39
39
|
installBin?: string;
|
|
40
|
+
/** Injected by tests: where `storage` looks for leftover test temp folders (default: the temp folder and /tmp). */
|
|
41
|
+
tempRoots?: readonly string[];
|
|
40
42
|
openDatabase?: (file: string) => Store;
|
|
41
43
|
now?: Date;
|
|
42
44
|
/**
|
package/dist/operate.js
CHANGED
|
@@ -7,8 +7,11 @@ import { parseProtectedPaths } from "./approval-policy.js";
|
|
|
7
7
|
import { rulesSummary } from "./approval-rules-ui.js";
|
|
8
8
|
import { evidencePack, exportDay, ledgerExportChunks, standaloneEvidenceHtml } from "./evidence-pack.js";
|
|
9
9
|
import { matchesOutsideCheckpoint } from "./ledger-chain.js";
|
|
10
|
-
import { CLEANUP_CHOICES, bytesWords, cleanupWords, parseCleanup, storageReport } from "./storage.js";
|
|
11
|
-
import {
|
|
10
|
+
import { CLEANUP_CHOICES, bytesWords, cleanupWords, diskBytes, parseCleanup, storageReport } from "./storage.js";
|
|
11
|
+
import { removeStaleTestTemp, tempRoots, testTempFolders } from "./test-temp.js";
|
|
12
|
+
import { removeShared, sharedDepsRoot, sharedUse, unusedShared } from "./shared-deps.js";
|
|
13
|
+
import { checkoutPlan, cleanCheckouts, discardCheckout, finishedCheckouts, slimKeptCheckouts, whyWords } from "./checkout-cleanup.js";
|
|
14
|
+
import { closeOutCheckouts, closeOutRuns, stoppedWords } from "./run-closeout.js";
|
|
12
15
|
import { MIN_DAYS, RETENTION_KINDS, countWords, dailyRetention, isRetentionKind, lastSweepAt, parsePeriod, periodLabel, periodWords, retentionPlan, sweepWords } from "./retention.js";
|
|
13
16
|
import { checkPolicy, parseList, policyParts } from "./policy.js";
|
|
14
17
|
import { billingOf, budgetHoldWords, budgetLabel, budgetStates, monthNamed, monthOf, spendItems, teammateNames, usd as spendUsd } from "./spend.js";
|
|
@@ -73,6 +76,7 @@ import { pullRequestLines, runTaskMergeCommand, runTaskOutcomeCommand } from "./
|
|
|
73
76
|
*/
|
|
74
77
|
import { homedir, hostname, tmpdir } from "node:os";
|
|
75
78
|
import { basename, dirname, join, resolve } from "node:path";
|
|
79
|
+
import { releaseStalledUpdate, waitingUpdate } from "./toolroll-update.js";
|
|
76
80
|
import { openStore, openStoreNoMigrate, CONCURRENT_WRITER_WAIT_MS, isDatabaseBusy, databasePath, BUILT_IN, DEFAULT_ACTOR, parseCapabilityKey, verifiedAuthor, contestantProfileOf, isDigestTime, } from "./store.js";
|
|
77
81
|
import { randomBytes, randomInt, randomUUID } from "node:crypto";
|
|
78
82
|
import { authorizePlanUnderMode } from "./plan-auto.js";
|
|
@@ -193,7 +197,7 @@ import { runModelsCommand } from "./models-cli.js";
|
|
|
193
197
|
import { updateAdmissionPaused, UPDATE_PAUSED } from "./desktop-update-gate.js";
|
|
194
198
|
import { beads } from "./beads.js";
|
|
195
199
|
import { githubIssues } from "./issues.js";
|
|
196
|
-
import { installationStatus, renderInstallationStatus, renderTaskWait, taskWaitSnapshot } from "./lead-status.js";
|
|
200
|
+
import { installationStatus, renderInstallationStatus, renderTaskWait, repairStaleStatuses, taskWaitSnapshot } from "./lead-status.js";
|
|
197
201
|
import { latestRelease, newerRelease, notifySecurityRelease, recordRunnerVersion, startUpdateChecks, updateLine } from "./releases.js";
|
|
198
202
|
import { installMethod } from "./install-method.js";
|
|
199
203
|
import { PACKAGE_VERSION } from "./version.js";
|
|
@@ -227,8 +231,8 @@ export const OPERATE_HELP = `toolroll — operating the queue
|
|
|
227
231
|
how many of a project's tasks build at once (default 2, never past the
|
|
228
232
|
worker's capacity); an approver changes it with <n>
|
|
229
233
|
toolroll task evidence <id> the task's evidence pack as JSON (--html for a printable page; --out <file>)
|
|
230
|
-
toolroll storage where the disk goes: database, build checkouts, releases, evidence
|
|
231
|
-
toolroll storage clean [--yes] preview removing finished tasks' clean checkouts (their branches stay); --yes removes them
|
|
234
|
+
toolroll storage where the disk goes: database, build checkouts, releases, evidence, leftover test temp folders
|
|
235
|
+
toolroll storage clean [--yes] preview removing finished tasks' clean checkouts (their branches stay) and test temp folders older than a day; --yes removes them
|
|
232
236
|
toolroll storage cleanup <when> remove a finished task's clean checkout: finished|2d|7d|never (instance operator)
|
|
233
237
|
toolroll storage discard <path> --yes throw away a checkout kept for its changes; its branch stays
|
|
234
238
|
toolroll integrations [--json] which integrations work: Connected, Not set up or Broken, and what to do (--saved: no new checks)
|
|
@@ -807,6 +811,7 @@ export async function runOperate(command, argv, write, options = {}) {
|
|
|
807
811
|
// it matters.
|
|
808
812
|
evidenceRoot: join(dirname(file), "evidence"),
|
|
809
813
|
databaseFile: file,
|
|
814
|
+
...(options.tempRoots === undefined ? {} : { tempRoots: options.tempRoots }),
|
|
810
815
|
// The bot token's file home. Never a column; see telegram.ts.
|
|
811
816
|
telegramTokenFile: join(dirname(file), "telegram-token"),
|
|
812
817
|
...(options.desktopIdentity === undefined ? {} : { desktopIdentity: options.desktopIdentity }),
|
|
@@ -4397,10 +4402,21 @@ async function reconcileCommand(flags, context) {
|
|
|
4397
4402
|
}
|
|
4398
4403
|
}
|
|
4399
4404
|
const recovered = recoverDead(store, clock());
|
|
4405
|
+
// A run that ended, whatever its outcome, leaves nothing running: what is still in its process groups is stopped
|
|
4406
|
+
// (SIGTERM, then SIGKILL after 10 s) and the ledger names it. Never the live service.
|
|
4407
|
+
const closedRuns = await closeOutRuns(store, clock, { repo });
|
|
4408
|
+
// A finished run's processes that have since gone get their exits recorded,
|
|
4409
|
+
// so its task stops waiting on a person by itself.
|
|
4410
|
+
store.recordFinishedRunExits(clock());
|
|
4400
4411
|
// A witness reserved for a spawn that never made a process (a crash or a
|
|
4401
4412
|
// thrown spawn) settles once its run's process groups are proven gone.
|
|
4402
4413
|
store.settleUnspawnedWitnesses(clock());
|
|
4403
4414
|
store.settleQuiescentStops(clock());
|
|
4415
|
+
// An update whose updater ended while letting work finish must not leave new work paused.
|
|
4416
|
+
try {
|
|
4417
|
+
releaseStalledUpdate(dirname(context.databaseFile), clock());
|
|
4418
|
+
}
|
|
4419
|
+
catch { /* status and the console say it */ }
|
|
4404
4420
|
for (const one of recovered) {
|
|
4405
4421
|
for (const leaseId of one.claims) {
|
|
4406
4422
|
// Lease ids are unique forever, so each recovery is its own episode.
|
|
@@ -4452,7 +4468,11 @@ async function reconcileCommand(flags, context) {
|
|
|
4452
4468
|
}
|
|
4453
4469
|
// Checkout cleanup (Settings → Storage): finished work's clean checkouts go as the setting says (by default when
|
|
4454
4470
|
// the task is complete or cancelled); their branches stay. Each removal is in the ledger.
|
|
4471
|
+
// Completing or cancelling a task stops anything still running from its checkout first.
|
|
4472
|
+
const closedCheckouts = await closeOutCheckouts(store, clock, finishedCheckouts(store, clock(), repo));
|
|
4455
4473
|
const pruned = await cleanCheckouts(store, worktrees, clock, { manual: false, actor: "worker", repo });
|
|
4474
|
+
// What stays for review or for its changes drops its dependencies and build output; the next run's setup restores them.
|
|
4475
|
+
const slimmed = await slimKeptCheckouts(store, worktrees, clock, { actor: "worker", repo });
|
|
4456
4476
|
// Retention (Settings → Retention): once a day, what's older than its setting goes; one ledger entry says what.
|
|
4457
4477
|
let retention = null;
|
|
4458
4478
|
try {
|
|
@@ -4468,6 +4488,9 @@ async function reconcileCommand(flags, context) {
|
|
|
4468
4488
|
adoption.adopted.length === 0 &&
|
|
4469
4489
|
adoption.forgotten.length === 0 &&
|
|
4470
4490
|
pruned.removed.length === 0 &&
|
|
4491
|
+
closedRuns.length === 0 &&
|
|
4492
|
+
closedCheckouts.length === 0 &&
|
|
4493
|
+
slimmed.slimmed.length === 0 &&
|
|
4471
4494
|
liveSwept.removed.length === 0;
|
|
4472
4495
|
return succeed(write, json, "reconcile", {
|
|
4473
4496
|
recovered,
|
|
@@ -4476,6 +4499,8 @@ async function reconcileCommand(flags, context) {
|
|
|
4476
4499
|
forgotten: adoption.forgotten,
|
|
4477
4500
|
checkoutsRemoved: pruned.removed.map(one => one.path),
|
|
4478
4501
|
checkoutsKept: pruned.kept,
|
|
4502
|
+
processesStopped: [...closedRuns.map(one => ({ run: one.runId, pids: one.done.stopped.map(row => row.pid) })), ...closedCheckouts.map(one => ({ checkout: one.path, pids: one.done.stopped.map(row => row.pid) }))],
|
|
4503
|
+
checkoutsSlimmed: slimmed.slimmed.map(one => one.path),
|
|
4479
4504
|
liveViewsSwept: liveSwept.removed.length,
|
|
4480
4505
|
retention: retention === null ? null : { counts: retention.counts, freedBytes: retention.freed },
|
|
4481
4506
|
}, () => nothing
|
|
@@ -4486,6 +4511,9 @@ async function reconcileCommand(flags, context) {
|
|
|
4486
4511
|
...adoption.adopted.map(path => `Adopted ${path} — released, unverified, somebody should look.`),
|
|
4487
4512
|
...adoption.forgotten.map(path => `Forgot ${path} — its directory is gone.`),
|
|
4488
4513
|
...(pruned.removed.length === 0 ? [] : [`Removed ${pruned.removed.length} finished task checkout(s), about ${bytesWords(pruned.freed)}; their branches stay.`]),
|
|
4514
|
+
...closedRuns.map(one => `Run #${one.runId} ended; stopped ${stoppedWords(one.done)}.`),
|
|
4515
|
+
...closedCheckouts.map(one => `${one.path}: its task finished; stopped ${stoppedWords(one.done)}.`),
|
|
4516
|
+
...(slimmed.slimmed.length === 0 ? [] : [`Dropped dependencies and build output from ${slimmed.slimmed.length} kept checkout(s), about ${bytesWords(slimmed.slimmed.reduce((sum, one) => sum + one.bytes, 0))}.`]),
|
|
4489
4517
|
...(liveSwept.removed.length === 0 ? [] : [`Cleared ${liveSwept.removed.length} finished live view(s).`]),
|
|
4490
4518
|
...(retention === null || retained.length === 0 ? [] : [`Retention removed ${sweepWords(retention.counts, retention.freed)}.`]),
|
|
4491
4519
|
]);
|
|
@@ -7259,6 +7287,8 @@ async function daemonCommand(positional, flags, context) {
|
|
|
7259
7287
|
// ---- the watch loop --------------------------------------------------------
|
|
7260
7288
|
const WATCH_LEASE_MS = 90_000;
|
|
7261
7289
|
const WATCH_HEARTBEAT_MS = 30_000;
|
|
7290
|
+
/** The start repair runs once per process, not once per project loop. */
|
|
7291
|
+
let staleStatusesRepaired = false;
|
|
7262
7292
|
async function runWatchLoop(args) {
|
|
7263
7293
|
const { context, flags, runner, token, repo, progress } = args;
|
|
7264
7294
|
const { store } = context;
|
|
@@ -7296,6 +7326,14 @@ async function runWatchLoop(args) {
|
|
|
7296
7326
|
store.startWatchEpisode({ repo, runner, incarnation }, new Date());
|
|
7297
7327
|
recordRunnerVersion(dirname(context.databaseFile), runner, PACKAGE_VERSION, new Date());
|
|
7298
7328
|
args.onReady?.();
|
|
7329
|
+
// Once per worker start, before any pass: statuses an older build left
|
|
7330
|
+
// stale (unrecorded exits of finished runs) are re-derived and clear.
|
|
7331
|
+
if (!staleStatusesRepaired) {
|
|
7332
|
+
staleStatusesRepaired = true;
|
|
7333
|
+
const repaired = repairStaleStatuses(store, new Date());
|
|
7334
|
+
if (repaired.exitsRecorded > 0)
|
|
7335
|
+
progress(`watch: recorded ${repaired.exitsRecorded} finished process exit(s) left open by an earlier version`);
|
|
7336
|
+
}
|
|
7299
7337
|
// A false renewal is FATAL (arc 2 finding 15): the lease or the
|
|
7300
7338
|
// credential is gone, and admitting one more pass would be work done for
|
|
7301
7339
|
// an authority this process no longer holds.
|
|
@@ -7792,7 +7830,8 @@ async function watchCommand(flags, context) {
|
|
|
7792
7830
|
token,
|
|
7793
7831
|
repo,
|
|
7794
7832
|
progress,
|
|
7795
|
-
|
|
7833
|
+
// An injected fence (tests) stops admission exactly as a signal does.
|
|
7834
|
+
isStopping: () => stopping || context.shouldStop?.() === true,
|
|
7796
7835
|
onFollowController: controller => {
|
|
7797
7836
|
followAbort = controller;
|
|
7798
7837
|
},
|
|
@@ -8836,6 +8875,10 @@ async function bridgeCommand(positional, flags, context) {
|
|
|
8836
8875
|
const runFor = text(flags, "for");
|
|
8837
8876
|
const timer = runFor === undefined ? null : setTimeout(() => controller.abort(), Number(runFor));
|
|
8838
8877
|
timer?.unref?.();
|
|
8878
|
+
// An injected fence (tests) ends the follow exactly as a signal does.
|
|
8879
|
+
const fence = context.shouldStop === undefined ? null : setInterval(() => { if (context.shouldStop?.() === true)
|
|
8880
|
+
controller.abort(); }, 50);
|
|
8881
|
+
fence?.unref?.();
|
|
8839
8882
|
if (!json)
|
|
8840
8883
|
write(`Following bot ${source.botId} — taps apply as they arrive. Ctrl-C stops it.`);
|
|
8841
8884
|
try {
|
|
@@ -8865,6 +8908,8 @@ async function bridgeCommand(positional, flags, context) {
|
|
|
8865
8908
|
finally {
|
|
8866
8909
|
if (timer !== null)
|
|
8867
8910
|
clearTimeout(timer);
|
|
8911
|
+
if (fence !== null)
|
|
8912
|
+
clearInterval(fence);
|
|
8868
8913
|
process.removeListener("SIGINT", stop);
|
|
8869
8914
|
process.removeListener("SIGTERM", stop);
|
|
8870
8915
|
}
|
|
@@ -9528,10 +9573,13 @@ async function statusCommand(positional, flags, context) {
|
|
|
9528
9573
|
const brokenIntegrations = await savedIntegrations(context).then(list => list.filter(one => one.state === "broken"), () => []);
|
|
9529
9574
|
const brokenLine = integrationsBrokenLine(brokenIntegrations);
|
|
9530
9575
|
const integrations = brokenIntegrations.length === 0 ? {} : { integrations: { broken: brokenIntegrations.map(one => ({ key: one.key, name: one.name, fix: one.action.kind === "fix" ? one.action.words : null })) } };
|
|
9576
|
+
// An update waiting, on what, and the action: before anything else, as it holds up new work.
|
|
9577
|
+
const waiting = waitingUpdate(context.databaseFile, run => context.store.stopQuiescenceProblem(run) !== null, context.clock());
|
|
9578
|
+
const updateWaiting = waiting === null ? {} : { updateWaiting: { app: waiting.app, version: waiting.version, stopped: waiting.stopped, run: waiting.run, on: waiting.on, action: waiting.action } };
|
|
9531
9579
|
const projects = projectBuildsStatus(context);
|
|
9532
9580
|
const projectsLine = projects.length === 0 ? null
|
|
9533
9581
|
: `Builds by project: ${projects.slice(0, 8).map(one => `${one.name} ${one.running} of ${one.limit}`).join(", ")}${projects.length > 8 ? ", …" : ""}`;
|
|
9534
|
-
return succeed(context.write, context.json, command, { ...status, projects, ...update, ...integrations }, () => [...renderInstallationStatus(status), ...(projectsLine === null ? [] : [projectsLine]), ...(line === null ? [] : [line]), ...(brokenLine === null ? [] : [brokenLine])]);
|
|
9582
|
+
return succeed(context.write, context.json, command, { ...status, projects, ...update, ...integrations, ...updateWaiting }, () => [...(waiting === null ? [] : [waiting.words]), ...renderInstallationStatus(status), ...(projectsLine === null ? [] : [projectsLine]), ...(line === null ? [] : [line]), ...(brokenLine === null ? [] : [brokenLine])]);
|
|
9535
9583
|
}
|
|
9536
9584
|
/** Each known project's running builds against its limit: its own number, never past its workers' capacity. */
|
|
9537
9585
|
function projectBuildsStatus(context) {
|
|
@@ -12145,13 +12193,35 @@ async function storageCommand(positional, flags, context) {
|
|
|
12145
12193
|
`A finished task's clean checkout is removed ${cleanupWords(plan.cleanup)}; its branch stays.`,
|
|
12146
12194
|
];
|
|
12147
12195
|
const itemLine = (one) => ` ${bytesWords(one.bytes).padStart(8)} ${one.path}${one.why === null ? "" : ` (${whyWords(one)})`}`;
|
|
12196
|
+
// Test temp folders the tests, journeys and scripts left (a killed run, an older version): those nothing touched for a day go.
|
|
12197
|
+
const roots = context.tempRoots ?? tempRoots();
|
|
12198
|
+
// Counting is quick; sizing thousands of folders isn't, so only a clean-up measures them.
|
|
12199
|
+
const leftovers = (measure) => {
|
|
12200
|
+
const found = testTempFolders(roots, context.clock());
|
|
12201
|
+
const sizes = measure ? diskBytes(found.stale.map(one => one.path)) : new Map();
|
|
12202
|
+
return { count: found.all.length, stale: found.stale.length, staleBytes: [...sizes.values()].reduce((sum, one) => sum + one, 0) };
|
|
12203
|
+
};
|
|
12204
|
+
const leftoverLine = (temp) => temp.count === 0 ? "Test temp folders: none." :
|
|
12205
|
+
`Test temp folders: ${temp.count}; ${temp.stale === 0 ? "none older than a day." : `${temp.stale} older than a day: toolroll storage clean removes them.`}`;
|
|
12206
|
+
// Shared dependencies (shared-deps.ts): one install per lockfile, linked into checkouts.
|
|
12207
|
+
const depsRoot = sharedDepsRoot(context.databaseFile);
|
|
12208
|
+
const checkoutRows = () => store.listWorktrees().map(row => ({ path: row.path, repo: row.repo }));
|
|
12209
|
+
const sharedFacts = (copies) => {
|
|
12210
|
+
const sizes = diskBytes(copies.map(one => one.dir));
|
|
12211
|
+
return copies.map(one => ({ key: one.key, repo: one.repo, path: one.dir, bytes: sizes.get(one.dir) ?? 0, checkouts: one.checkouts.length, usedBy: one.checkouts, retired: one.retired, node: one.node, createdAt: one.createdAt, usedAt: one.usedAt }));
|
|
12212
|
+
};
|
|
12213
|
+
const sharedLine = (one) => ` ${bytesWords(one.bytes).padStart(8)} ${projectName(one.repo)} ${one.key.slice(0, 8)}: ${one.checkouts === 1 ? "1 checkout uses it" : `${one.checkouts} checkouts use it`}${one.retired ? " (changed after install; never linked again)" : ""}`;
|
|
12148
12214
|
if (action === undefined) {
|
|
12149
12215
|
const plan = await checkoutPlan(store, pool, context.clock(), { manual: true });
|
|
12150
12216
|
const report = storageReport(store, context.databaseFile, plan.go.length === 0 ? "nothing to clean up" : `about ${bytesWords(plan.freeBytes)} to clean up`);
|
|
12151
|
-
|
|
12217
|
+
const { staleBytes: _, ...temp } = leftovers(false);
|
|
12218
|
+
const shared = sharedFacts(sharedUse(depsRoot, checkoutRows()));
|
|
12219
|
+
return succeed(context.write, context.json, "storage", { ...report, checkouts: checkoutSummary(plan), testTemp: temp, shared }, () => [
|
|
12152
12220
|
`${report.folder}: ${bytesWords(report.total)}`,
|
|
12153
|
-
...report.lines.map(one => ` ${one.what.padEnd(
|
|
12221
|
+
...report.lines.map(one => ` ${one.what.padEnd(19)} ${bytesWords(one.bytes).padStart(8)}${one.count === undefined ? "" : ` (${one.count})`}${one.note === undefined ? "" : ` ${one.note}`}`),
|
|
12222
|
+
...(shared.length === 0 ? [] : ["Shared dependencies:", ...shared.map(sharedLine)]),
|
|
12154
12223
|
...summary(plan),
|
|
12224
|
+
leftoverLine({ ...temp, staleBytes: 0 }),
|
|
12155
12225
|
]);
|
|
12156
12226
|
}
|
|
12157
12227
|
if (action === "cleanup") {
|
|
@@ -12173,20 +12243,48 @@ async function storageCommand(positional, flags, context) {
|
|
|
12173
12243
|
return fail(context.write, context.json, command, "usage", "Use storage clean to preview, then storage clean --yes.", EXIT.usage);
|
|
12174
12244
|
if (!flags.has("yes")) {
|
|
12175
12245
|
const plan = await checkoutPlan(store, pool, context.clock(), { manual: true });
|
|
12176
|
-
|
|
12177
|
-
|
|
12246
|
+
const temp = leftovers(true);
|
|
12247
|
+
// A shared copy goes when no checkout uses it, counting the checkouts this clean-up would remove.
|
|
12248
|
+
const leaving = new Set(plan.go.map(one => one.path));
|
|
12249
|
+
const unused = unusedShared(depsRoot, checkoutRows().filter(row => !leaving.has(row.path)), context.clock());
|
|
12250
|
+
const sharedGo = sharedFacts(unused.copies);
|
|
12251
|
+
const sharedBytes = sharedGo.reduce((sum, one) => sum + one.bytes, 0);
|
|
12252
|
+
const anything = plan.go.length > 0 || temp.stale > 0 || sharedGo.length > 0;
|
|
12253
|
+
return succeed(context.write, context.json, command, { preview: true, go: plan.go, stay: plan.stay, freeBytes: plan.freeBytes, testTemp: temp, sharedGo, sharedFreeBytes: sharedBytes }, () => [
|
|
12254
|
+
...(plan.go.length === 0 ? [anything ? "No checkouts to clean up." : "Nothing to clean up."] : [`Would remove ${plan.go.length} checkout${plan.go.length === 1 ? "" : "s"}, freeing about ${bytesWords(plan.freeBytes)} (their branches stay):`, ...plan.go.map(itemLine)]),
|
|
12255
|
+
...(sharedGo.length === 0 ? [] : [`Would remove ${sharedGo.length} shared dependency install${sharedGo.length === 1 ? "" : "s"} no checkout uses, about ${bytesWords(sharedBytes)}:`, ...sharedGo.map(sharedLine)]),
|
|
12178
12256
|
...(plan.stay.length === 0 ? [] : [`Stays (${plan.stay.length}):`, ...plan.stay.map(itemLine)]),
|
|
12179
|
-
|
|
12257
|
+
...(temp.stale === 0 ? [] : [`Would remove ${temp.stale} test temp folder${temp.stale === 1 ? "" : "s"} older than a day, about ${bytesWords(temp.staleBytes)}.`]),
|
|
12258
|
+
anything ? "Nothing was removed. Run toolroll storage clean --yes to remove them." : "Nothing was removed.",
|
|
12180
12259
|
]);
|
|
12181
12260
|
}
|
|
12182
12261
|
const actor = await operator();
|
|
12183
12262
|
if (actor === null)
|
|
12184
12263
|
return refused("cleans up checkouts");
|
|
12185
12264
|
const done = await cleanCheckouts(store, pool, context.clock, { manual: true, actor });
|
|
12186
|
-
|
|
12187
|
-
|
|
12265
|
+
const temp = leftovers(true);
|
|
12266
|
+
const swept = removeStaleTestTemp(roots, context.clock());
|
|
12267
|
+
if (swept.removed.length > 0) {
|
|
12268
|
+
store.recordAction({ at: context.clock().toISOString(), actor, repo: null, taskId: null, runId: null, action: "test temp folders removed", outcome: "removed", source: "request",
|
|
12269
|
+
detail: `${swept.removed.length} older than a day, about ${bytesWords(temp.staleBytes)}${swept.failed.length === 0 ? "" : `; ${swept.failed.length} couldn't be removed`}` });
|
|
12270
|
+
}
|
|
12271
|
+
// After the checkouts go: a shared copy none of the remaining ones uses goes too.
|
|
12272
|
+
const unused = unusedShared(depsRoot, checkoutRows(), context.clock());
|
|
12273
|
+
const sharedRemoved = sharedFacts(unused.copies).filter(one => removeShared(one.path));
|
|
12274
|
+
for (const one of sharedRemoved) {
|
|
12275
|
+
store.recordAction({ at: context.clock().toISOString(), actor, repo: one.repo, taskId: null, runId: null, action: "shared dependencies removed", outcome: "removed", source: "request",
|
|
12276
|
+
detail: `${one.key} (about ${bytesWords(one.bytes)}); no checkout used it` });
|
|
12277
|
+
}
|
|
12278
|
+
for (const debris of unused.debris)
|
|
12279
|
+
removeShared(debris);
|
|
12280
|
+
const sharedFreed = sharedRemoved.reduce((sum, one) => sum + one.bytes, 0);
|
|
12281
|
+
return succeed(context.write, context.json, command, { removed: done.removed, kept: done.kept, freedBytes: done.freed, testTempRemoved: swept.removed.length, testTempFailed: swept.failed, sharedRemoved, sharedFreedBytes: sharedFreed }, () => [
|
|
12282
|
+
done.removed.length === 0 ? (swept.removed.length === 0 && sharedRemoved.length === 0 ? "Nothing to clean up." : "No checkouts to clean up.") : `Removed ${done.removed.length} checkout${done.removed.length === 1 ? "" : "s"}, about ${bytesWords(done.freed)}; their branches stay.`,
|
|
12188
12283
|
...done.removed.map(itemLine),
|
|
12284
|
+
...(sharedRemoved.length === 0 ? [] : [`Removed ${sharedRemoved.length} shared dependency install${sharedRemoved.length === 1 ? "" : "s"} no checkout used, about ${bytesWords(sharedFreed)}.`, ...sharedRemoved.map(sharedLine)]),
|
|
12189
12285
|
...done.kept.map(one => ` kept ${one.path} (${whyWords({ why: one.why })})`),
|
|
12286
|
+
...(swept.removed.length === 0 ? [] : [`Removed ${swept.removed.length} test temp folder${swept.removed.length === 1 ? "" : "s"} older than a day, about ${bytesWords(temp.staleBytes)}.`]),
|
|
12287
|
+
...(swept.failed.length === 0 ? [] : [`Couldn't remove ${swept.failed.length} test temp folder${swept.failed.length === 1 ? "" : "s"}: ${swept.failed.slice(0, 3).join(", ")}${swept.failed.length > 3 ? " …" : ""}`]),
|
|
12190
12288
|
]);
|
|
12191
12289
|
}
|
|
12192
12290
|
if (action === "discard") {
|
package/dist/plane-review.js
CHANGED
|
@@ -148,6 +148,10 @@ export function reviewPlane(store, now, canSee = () => true) {
|
|
|
148
148
|
for (const row of tasks) {
|
|
149
149
|
if (!canSee(str(row["repo"])))
|
|
150
150
|
continue;
|
|
151
|
+
// A version a later revision replaced never waits on its own.
|
|
152
|
+
const family = store.taskFamilyOf(String(row["id"]), null, true);
|
|
153
|
+
if (family !== null && family.current.id !== String(row["id"]))
|
|
154
|
+
continue;
|
|
151
155
|
const snapshot = taskWaitSnapshot(store, String(row["id"]), now);
|
|
152
156
|
if (snapshot === null || (snapshot.reason !== "needs-person" && snapshot.reason !== "ready"))
|
|
153
157
|
continue;
|
|
@@ -95,6 +95,7 @@ export declare function preparedCandidateObserverGapEligibility(store: Store, in
|
|
|
95
95
|
readonly items: readonly string[];
|
|
96
96
|
readonly gaps: readonly string[];
|
|
97
97
|
readonly priorSupport: "eligible" | "invalid" | "none";
|
|
98
|
+
readonly assets?: string;
|
|
98
99
|
};
|
|
99
100
|
}[];
|
|
100
101
|
readonly machineVerdict: import("./store.js").ProofVerdictRow["verdict"] | null;
|
|
@@ -48,9 +48,9 @@ export type LearningEvent = {
|
|
|
48
48
|
};
|
|
49
49
|
export declare const learningSha: (text: string) => string;
|
|
50
50
|
/** Path plus the physical shared Git database: replacing a repository does not inherit its lessons. */
|
|
51
|
-
export declare function learningIdentity(repo: string): string;
|
|
51
|
+
export declare function learningIdentity(repo: string, seen?: Map<string, string>): string;
|
|
52
52
|
/** Compare the applicable code and configuration, not model confidence or a claimed passing command. */
|
|
53
|
-
export declare function learningFingerprint(repo: string, head: string, paths: readonly string[]): string;
|
|
53
|
+
export declare function learningFingerprint(repo: string, head: string, paths: readonly string[], seen?: Map<string, string>): string;
|
|
54
54
|
export declare function parseLearning(value: unknown): LearningCandidate[];
|
|
55
55
|
/** A write problem is optional: preserve core completion, leave a visible diagnostic, retry on the next read/admission. */
|
|
56
56
|
export declare function learningFailure(store: Store, repo: string, run: number | null, now: Date): void;
|
|
@@ -60,7 +60,7 @@ export declare function queueLearning(store: Store, source: number, reviewer: nu
|
|
|
60
60
|
sha256: string;
|
|
61
61
|
}[], now: Date, assessment?: unknown): void;
|
|
62
62
|
/** Validate after core commit; both suggestions commit or neither. */
|
|
63
|
-
export declare function recoverLearning(store: Store, root: string, repo: string, now?: Date): void;
|
|
63
|
+
export declare function recoverLearning(store: Store, root: string, repo: string, now?: Date, seen?: Map<string, string>): void;
|
|
64
64
|
export declare function learningView(store: Store, root: string, repo: string, actor: string, before?: number): {
|
|
65
65
|
repo: string;
|
|
66
66
|
identity: string;
|
package/dist/project-learning.js
CHANGED
|
@@ -44,12 +44,21 @@ CREATE TRIGGER IF NOT EXISTS learning_capture_no_delete BEFORE DELETE ON learnin
|
|
|
44
44
|
export const learningSha = (text) => createHash('sha256').update(text).digest('hex');
|
|
45
45
|
const text = (x, max) => typeof x === 'string' && x.trim().length > 0 && Buffer.byteLength(x) <= max && !/[\x00-\x1f\x7f]/.test(x);
|
|
46
46
|
const pathOk = (x) => text(x, 300) && !x.startsWith('/') && !x.includes('\\') && !/^[A-Za-z]:/.test(x) && x.split('/').every(p => p !== '.' && p !== '..' && p !== '') && !/[\*?\[\]:]/.test(x);
|
|
47
|
-
|
|
47
|
+
/** One read per distinct command within a single selection pass (`seen`), so a pass over many lessons reads the repository once. */
|
|
48
|
+
const git = (repo, args, seen) => {
|
|
49
|
+
const key = JSON.stringify([repo, args]);
|
|
50
|
+
const known = seen?.get(key);
|
|
51
|
+
if (known !== undefined)
|
|
52
|
+
return known;
|
|
53
|
+
const out = execFileSync('git', ['--no-optional-locks', '-C', repo, ...args], { encoding: 'utf8', maxBuffer: 2 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'] }).trimEnd();
|
|
54
|
+
seen?.set(key, out);
|
|
55
|
+
return out;
|
|
56
|
+
};
|
|
48
57
|
/** Path plus the physical shared Git database: replacing a repository does not inherit its lessons. */
|
|
49
|
-
export function learningIdentity(repo) {
|
|
58
|
+
export function learningIdentity(repo, seen) {
|
|
50
59
|
if (canonicalProject(repo) !== repo)
|
|
51
60
|
throw new Error('Project identity changed.');
|
|
52
|
-
const common = realpathSync(resolve(repo, git(repo, ['rev-parse', '--git-common-dir'])));
|
|
61
|
+
const common = realpathSync(resolve(repo, git(repo, ['rev-parse', '--git-common-dir'], seen)));
|
|
53
62
|
const st = statSync(common);
|
|
54
63
|
return learningSha(JSON.stringify([repo, common, st.dev, st.ino, st.birthtimeMs]));
|
|
55
64
|
}
|
|
@@ -59,10 +68,10 @@ function environmentFingerprint(store, repo) {
|
|
|
59
68
|
}
|
|
60
69
|
const configPath = (p) => /(^|\/)(AGENTS\.md|package(-lock)?\.json|[^/]*lock[^/]*|[^/]*config[^/]*|Cargo\.toml|go\.mod|pyproject\.toml|\.tool-versions|\.nvmrc)$/.test(p);
|
|
61
70
|
/** Compare the applicable code and configuration, not model confidence or a claimed passing command. */
|
|
62
|
-
export function learningFingerprint(repo, head, paths) {
|
|
71
|
+
export function learningFingerprint(repo, head, paths, seen) {
|
|
63
72
|
if (!/^[a-f0-9]{40,64}$/.test(head))
|
|
64
73
|
throw new Error('No exact source revision.');
|
|
65
|
-
const entries = git(repo, ['ls-tree', '-r', head]).split('\n').filter(line => {
|
|
74
|
+
const entries = git(repo, ['ls-tree', '-r', head], seen).split('\n').filter(line => {
|
|
66
75
|
const p = line.slice(line.indexOf('\t') + 1);
|
|
67
76
|
return paths.includes(p) || configPath(p);
|
|
68
77
|
});
|
|
@@ -198,7 +207,7 @@ function verifyEvidence(store, root, source, evidence, catalog) {
|
|
|
198
207
|
// missing work, so newer reviews cannot starve an older recovery.
|
|
199
208
|
const recoveryCursors = new WeakMap();
|
|
200
209
|
/** Validate after core commit; both suggestions commit or neither. */
|
|
201
|
-
export function recoverLearning(store, root, repo, now = new Date()) {
|
|
210
|
+
export function recoverLearning(store, root, repo, now = new Date(), seen = new Map()) {
|
|
202
211
|
let projects = recoveryCursors.get(store);
|
|
203
212
|
if (!projects)
|
|
204
213
|
recoveryCursors.set(store, projects = new Map());
|
|
@@ -225,7 +234,7 @@ export function recoverLearning(store, root, repo, now = new Date()) {
|
|
|
225
234
|
if (!source || !reviewer || source.taskRef !== reviewer.taskRef)
|
|
226
235
|
continue;
|
|
227
236
|
const snapshot = reviewSnapshot(store, reviewer.id);
|
|
228
|
-
if (!snapshot || snapshot['identity'] !== learningIdentity(repo))
|
|
237
|
+
if (!snapshot || snapshot['identity'] !== learningIdentity(repo, seen))
|
|
229
238
|
continue;
|
|
230
239
|
const response = store.artifactsFor(reviewer.id).find(a => a.kind === 'structured-output' && a.captureStatus === 'ok');
|
|
231
240
|
const read = response && readVerifiedArtifact(root, response);
|
|
@@ -260,7 +269,7 @@ export function recoverLearning(store, root, repo, now = new Date()) {
|
|
|
260
269
|
if (!run || !reviewed || reviewed.outcome !== 'no-change' || reviewed.taskRef !== run.taskRef || reviewed.role !== 'reviewer' || reviewedSource(store, reviewer) !== source || store.refForId(run.taskRef)?.repo !== repo)
|
|
261
270
|
throw new Error('Learning review provenance is unavailable.');
|
|
262
271
|
const candidates = parseLearning(JSON.parse(String(row['payload'])));
|
|
263
|
-
const identity = learningIdentity(repo);
|
|
272
|
+
const identity = learningIdentity(repo, seen);
|
|
264
273
|
if (identity !== row['identity'])
|
|
265
274
|
throw new Error('Learning project identity changed.');
|
|
266
275
|
const head = run.headRevision ?? run.baseRevision ?? '';
|
|
@@ -273,7 +282,7 @@ export function recoverLearning(store, root, repo, now = new Date()) {
|
|
|
273
282
|
verifyEvidence(store, root, source, c.evidence, JSON.parse(String(row['catalog'])));
|
|
274
283
|
if (c.paths.some(p => !patch.split('\n').some(l => l === `+++ b/${p}` || l === `--- a/${p}`)))
|
|
275
284
|
throw new Error('Learning applicability must name reviewed files.');
|
|
276
|
-
const payload = JSON.stringify({ ...c, environment: String(row['environment']), fingerprint: learningFingerprint(repo, head, c.paths), platform: process.platform, head });
|
|
285
|
+
const payload = JSON.stringify({ ...c, environment: String(row['environment']), fingerprint: learningFingerprint(repo, head, c.paths, seen), platform: process.platform, head });
|
|
277
286
|
const finding = learningSha(JSON.stringify(c));
|
|
278
287
|
store.handle.prepare("INSERT OR IGNORE INTO project_lesson(repo,identity,source,reviewer,finding,payload,sha,status) VALUES (?,?,?,?,?,?,?,'proposed')").run(repo, identity, source, reviewer, finding, payload, learningSha(payload));
|
|
279
288
|
const id = Number(store.handle.prepare('SELECT id FROM project_lesson WHERE source=? AND finding=?').get(source, finding)['id']);
|
|
@@ -302,15 +311,15 @@ function lessonOf(row) {
|
|
|
302
311
|
parseLearning([payload]);
|
|
303
312
|
return { id: Number(row['id']), repo: String(row['repo']), source: Number(row['source']), reviewer: Number(row['reviewer']), status: String(row['status']), version: Number(row['version']), payload, sha: String(row['sha']), identity: String(row['identity']), adoptedBy: row['adopted_by'] == null ? null : String(row['adopted_by']) };
|
|
304
313
|
}
|
|
305
|
-
function supported(store, root, lesson) {
|
|
306
|
-
if (lesson.identity !== learningIdentity(lesson.repo))
|
|
314
|
+
function supported(store, root, lesson, seen) {
|
|
315
|
+
if (lesson.identity !== learningIdentity(lesson.repo, seen))
|
|
307
316
|
throw new Error('Project identity changed.');
|
|
308
317
|
const capture = store.handle.prepare('SELECT catalog FROM learning_capture WHERE source=? AND reviewer=? AND repo=?').get(lesson.source, lesson.reviewer, lesson.repo);
|
|
309
318
|
if (!capture || reviewedSource(store, lesson.reviewer) !== lesson.source)
|
|
310
319
|
throw new Error('Learning source is unavailable.');
|
|
311
320
|
verifyEvidence(store, root, lesson.source, lesson.payload.evidence, JSON.parse(String(capture['catalog'])));
|
|
312
321
|
const run = store.getRun(lesson.source);
|
|
313
|
-
if ((run?.headRevision ?? run?.baseRevision) !== lesson.payload.head || learningFingerprint(lesson.repo, lesson.payload.head, lesson.payload.paths) !== lesson.payload.fingerprint)
|
|
322
|
+
if ((run?.headRevision ?? run?.baseRevision) !== lesson.payload.head || learningFingerprint(lesson.repo, lesson.payload.head, lesson.payload.paths, seen) !== lesson.payload.fingerprint)
|
|
314
323
|
throw new Error('Learning source changed.');
|
|
315
324
|
}
|
|
316
325
|
export function learningView(store, root, repo, actor, before = 0) {
|
|
@@ -377,15 +386,17 @@ export function learningContext(store, root, runId, phase, now = new Date(), val
|
|
|
377
386
|
const run = store.getRun(runId), ref = run && store.refForId(run.taskRef), repo = ref?.repo;
|
|
378
387
|
if (!run || !repo)
|
|
379
388
|
return '';
|
|
389
|
+
// One read of the repository per command for this whole pass, recovery included.
|
|
390
|
+
const seen = new Map();
|
|
380
391
|
try {
|
|
381
|
-
recoverLearning(store, root, repo, now);
|
|
392
|
+
recoverLearning(store, root, repo, now, seen);
|
|
382
393
|
return store.transact(() => {
|
|
383
394
|
if (!store.schemaCurrent() || run.outcome !== null || !['planner', 'builder', 'repair', 'reviewer'].includes(run.role))
|
|
384
395
|
return '';
|
|
385
396
|
const runner = store.getRunner(run.runner)?.runner;
|
|
386
397
|
if (!runner || runner.retiredAt !== null || !runner.repos.includes(repo) || (phase === 'review') !== (run.role === 'reviewer') || (phase === 'plan') !== (run.role === 'planner'))
|
|
387
398
|
return '';
|
|
388
|
-
const identity = learningIdentity(repo);
|
|
399
|
+
const identity = learningIdentity(repo, seen);
|
|
389
400
|
const existing = store.handle.prepare('SELECT * FROM learning_snapshot WHERE run=?').get(runId);
|
|
390
401
|
if (existing) {
|
|
391
402
|
if (existing['repo'] !== repo || existing['identity'] !== identity || learningSha(String(existing['payload'])) !== existing['sha'])
|
|
@@ -409,11 +420,11 @@ export function learningContext(store, root, runId, phase, now = new Date(), val
|
|
|
409
420
|
const l = lessonOf(row);
|
|
410
421
|
if (!store.handle.prepare("SELECT 1 FROM learning_event WHERE lesson=? AND action='adopt' AND actor=?").get(l.id, l.adoptedBy ?? '') || l.source === source?.id || l.payload.kind !== 'project' || !l.adoptedBy || !store.accountCanAccess(l.adoptedBy, repo) || store.accountOf(l.adoptedBy)?.role !== 'approver' || !l.payload.phases.includes(phase) || l.payload.platform !== process.platform || (!unscopedPlan && !l.payload.paths.some(p => paths.some(t => p === t || p.startsWith(t.replace(/\/$/, '') + '/')))))
|
|
411
422
|
continue;
|
|
412
|
-
supported(store, root, l);
|
|
413
|
-
if (l.payload.environment !== environmentFingerprint(store, repo) || learningFingerprint(repo, head, l.payload.paths) !== l.payload.fingerprint)
|
|
423
|
+
supported(store, root, l, seen);
|
|
424
|
+
if (l.payload.environment !== environmentFingerprint(store, repo) || learningFingerprint(repo, head, l.payload.paths, seen) !== l.payload.fingerprint)
|
|
414
425
|
continue;
|
|
415
426
|
const cwd = run.worktree ?? repo;
|
|
416
|
-
const dirty = git(cwd, ['status', '--porcelain', '--untracked-files=all']).split('\n').some(line => { const p = line.slice(3); return configPath(p) || l.payload.paths.includes(p); });
|
|
427
|
+
const dirty = git(cwd, ['status', '--porcelain', '--untracked-files=all'], seen).split('\n').some(line => { const p = line.slice(3); return configPath(p) || l.payload.paths.includes(p); });
|
|
417
428
|
if (!dirty)
|
|
418
429
|
eligible.push(l);
|
|
419
430
|
}
|
package/dist/proof.d.ts
CHANGED
|
@@ -266,7 +266,7 @@ export type VerifyCommandFacts = {
|
|
|
266
266
|
configured: true;
|
|
267
267
|
ran: false;
|
|
268
268
|
attemptFailed: true;
|
|
269
|
-
failure?: "spawn-failed" | "timed-out" | "dependency-missing" | "setup-stale" | "setup-failed" | "tracked-files-changed" | "setup-changed-files" | "checkout-moved" | "cleanliness-unavailable" | "dependency-still-missing" | "retry-spawn-failed" | "retry-timed-out" | "custody-lost";
|
|
269
|
+
failure?: "spawn-failed" | "timed-out" | "dependency-missing" | "setup-stale" | "setup-failed" | "own-install-failed" | "tracked-files-changed" | "setup-changed-files" | "checkout-moved" | "cleanliness-unavailable" | "dependency-still-missing" | "retry-spawn-failed" | "retry-timed-out" | "custody-lost";
|
|
270
270
|
} | {
|
|
271
271
|
configured: true;
|
|
272
272
|
ran: true;
|
|
@@ -390,6 +390,8 @@ export type CriterionCoverage = {
|
|
|
390
390
|
* context: eligible only with the same text, verified ancestry, and
|
|
391
391
|
* byte-identical relevant code. Never a judgement on this run. */
|
|
392
392
|
priorSupport: "eligible" | "invalid" | "none";
|
|
393
|
+
/** The change's images and other binary files in one line, when it has any. */
|
|
394
|
+
assets?: string;
|
|
393
395
|
};
|
|
394
396
|
/** v40: the three words an evidence reviewer can say about one signed
|
|
395
397
|
* criterion, and nothing else — `cannot-tell` is a CORRECT answer whenever
|
package/dist/proof.js
CHANGED
|
@@ -722,7 +722,7 @@ function assessCapturedEvidence(input) {
|
|
|
722
722
|
id: criterion.id, statement: criterion.statement, requiredEvidence: criterion.evidence,
|
|
723
723
|
assessment: { evidenceState, detail }, state: evidenceState === "pass" ? "missing" : evidenceState,
|
|
724
724
|
detail: [...detail, `criterion "${criterion.id}" awaits independent goal assessment`], answered, review: null,
|
|
725
|
-
...(coverage === undefined ? {} : { coverage: { state: coverage.state, inherited: coverage.inherited, items: [...coverage.items], gaps: [...coverage.gaps], priorSupport: coverage.priorSupport } }),
|
|
725
|
+
...(coverage === undefined ? {} : { coverage: { state: coverage.state, inherited: coverage.inherited, items: [...coverage.items], gaps: [...coverage.gaps], priorSupport: coverage.priorSupport, ...(coverage.assets === undefined ? {} : { assets: coverage.assets }) } }),
|
|
726
726
|
};
|
|
727
727
|
});
|
|
728
728
|
return { machineVerdict, verdict: machineVerdict === "refuted" ? "refuted" : "short", matrix,
|
|
@@ -761,6 +761,8 @@ function verificationFailureWords(failure) {
|
|
|
761
761
|
return "automatic recovery stopped because the project setup or check changed";
|
|
762
762
|
case "setup-failed":
|
|
763
763
|
return "the approved setup command failed during automatic recovery";
|
|
764
|
+
case "own-install-failed":
|
|
765
|
+
return "the checkout's own dependency install failed after its package.json or lockfile changed";
|
|
764
766
|
case "tracked-files-changed":
|
|
765
767
|
return "automatic recovery stopped because tracked files no longer matched the built result";
|
|
766
768
|
case "setup-changed-files":
|
|
@@ -828,7 +830,7 @@ export function adjudicate(input) {
|
|
|
828
830
|
// must show, never a silent "judged from the patch".
|
|
829
831
|
const coverage = found === undefined
|
|
830
832
|
? { state: "gap", inherited: false, items: [], gaps: ["the sealed review context carries no coverage entry for this criterion"], priorSupport: "none" }
|
|
831
|
-
: { state: found.state, inherited: found.inherited, items: [...found.items], gaps: [...found.gaps], priorSupport: found.priorSupport };
|
|
833
|
+
: { state: found.state, inherited: found.inherited, items: [...found.items], gaps: [...found.gaps], priorSupport: found.priorSupport, ...(found.assets === undefined ? {} : { assets: found.assets }) };
|
|
832
834
|
return { ...row, coverage };
|
|
833
835
|
});
|
|
834
836
|
if (!input.proofArtifactPresent) {
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
import type { Store } from "./store.js";
|
|
7
7
|
/** The ledger action a person's (or lead's) completion of a result writes. */
|
|
8
8
|
export declare const COMPLETION_ACTION = "assignment handoff checked";
|
|
9
|
+
/** Whether a task's family is marked complete. Completion is recorded on the
|
|
10
|
+
* family's root against its current version's latest result, so the root, a
|
|
11
|
+
* superseded revision and the current version all read Complete together. */
|
|
12
|
+
export declare function familyCompleted(store: Store, taskId: string): boolean;
|
|
9
13
|
/** The files a build changed, renames on both sides; null when the inventory can't be read whole. */
|
|
10
14
|
export declare function changedFilesOf(store: Store, runId: number, root: string): string[] | null;
|
|
11
15
|
/** Everything a result's branch carries: this build's changes and every build of every task it revises (a
|