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/store.js
CHANGED
|
@@ -67,8 +67,9 @@ import { PLAN_AUTO_SCHEMA } from "./plan-auto.js";
|
|
|
67
67
|
import { RECIPE_SCHEMA } from "./recipes.js";
|
|
68
68
|
import { POLICY_SCHEMA, agentRefusal, approvalRefusal, attendedRefusal, policyParts, readPolicy, underCeiling, sessionCeilingRefusal } from "./policy.js";
|
|
69
69
|
import { PROVIDER_AUTH_SCHEMA } from "./provider-auth.js";
|
|
70
|
+
import { REVIEW_SCHEMA, reviewSwitchWords } from "./review-switch.js";
|
|
70
71
|
import { SPEND_SCHEMA, billingOf, budgetStates, canPrice, claudeMachineBilling, countsToward, filersOf, monthOf, priceWork, seenBilling as seenBillingOf, spendItems, teammateFilers, usd } from "./spend.js";
|
|
71
|
-
import {
|
|
72
|
+
import { DEFAULT_PERIODS, RETENTION_SCHEMA, periodWords, widenRetentionSchema } from "./retention.js";
|
|
72
73
|
import { CHECKOUT_CLEANUP_SCHEMA, DEFAULT_CLEANUP, cleanupWords } from "./storage.js";
|
|
73
74
|
import { IN_RANGE, LEDGER_CHAIN_SCHEMA, safeWhole, sealLedger, verifyLedgerChain } from "./ledger-chain.js";
|
|
74
75
|
import { NO_RULES, filerFor, isProtectedWork, protectedChanges, rulesWords } from "./approval-policy.js";
|
|
@@ -214,6 +215,20 @@ CREATE TABLE IF NOT EXISTS project_mute (
|
|
|
214
215
|
const SECURITY_KINDS = new Set(["secret-detected", "security-release"]);
|
|
215
216
|
/** A failure that leaves nothing for the lead to try: the attempts are spent. */
|
|
216
217
|
const SPENT_KINDS = new Set(["attempts-exhausted", "plan-attempts-exhausted"]);
|
|
218
|
+
/** Store.stopQuiescenceFact, read safely: a deploy proves completion with this code over the INSTALLED runtime's store,
|
|
219
|
+
* which may predate the method. There the older stopQuiescenceProblem decides, and its words give the kind. */
|
|
220
|
+
export function stopFactOf(store, runId) {
|
|
221
|
+
if (typeof store.stopQuiescenceFact === "function")
|
|
222
|
+
return store.stopQuiescenceFact(runId);
|
|
223
|
+
const problem = store.stopQuiescenceProblem(runId);
|
|
224
|
+
if (problem === null)
|
|
225
|
+
return null;
|
|
226
|
+
const kind = /still open|does not exist/.test(problem) ? "open"
|
|
227
|
+
: /another host/.test(problem) ? "elsewhere"
|
|
228
|
+
: /may still be running|still has|held by|has not finished|still has members/.test(problem) ? "alive"
|
|
229
|
+
: /cannot be proven|could not be established|incomplete native/.test(problem) ? "unknown" : "unprovable";
|
|
230
|
+
return { run: Number(/run #(\d+)/.exec(problem)?.[1] ?? runId), kind, problem };
|
|
231
|
+
}
|
|
217
232
|
/** Settings → Integrations (no version bump): each integration's last check. `outcome` is ok, failed, or absent (the check found
|
|
218
233
|
* nothing set up, such as no `gh` sign-in). `ok_at` and `error`/`error_at` keep the last success and the last
|
|
219
234
|
* failure across checks, so a row can say both. `account` is a display name only, never a credential. */
|
|
@@ -3801,7 +3816,10 @@ function initializeStore(db, file) {
|
|
|
3801
3816
|
db.exec(PROVIDER_AUTH_SCHEMA);
|
|
3802
3817
|
// The sign-in pause the dispatch gate last left this task waiting on (null: none), for the work index.
|
|
3803
3818
|
addColumn(db, "task_ref", "auth_wait_pause", "INTEGER REFERENCES provider_auth_pause(id)");
|
|
3819
|
+
db.exec(REVIEW_SCHEMA);
|
|
3804
3820
|
db.exec(RETENTION_SCHEMA);
|
|
3821
|
+
// 1-day evidence: an older file's retention_setting only allowed 7 days or more (no version bump: its rows carry over).
|
|
3822
|
+
widenRetentionSchema(db);
|
|
3805
3823
|
db.exec(CHECKOUT_CLEANUP_SCHEMA);
|
|
3806
3824
|
db.exec(BACKUP_SCHEMA);
|
|
3807
3825
|
// Sprint 8: the organisation policy (one row, or none: nothing restricted).
|
|
@@ -9333,6 +9351,27 @@ export class Store {
|
|
|
9333
9351
|
return;
|
|
9334
9352
|
this.recordAction({ at: now.toISOString(), actor: by, repo, taskId: null, runId: null, action, outcome, source: "policy", detail: `${before} → ${after}` });
|
|
9335
9353
|
}
|
|
9354
|
+
/** The project's automatic review switch: its explicit setting, else on
|
|
9355
|
+
* while a hands-off mode is active on it, else off. */
|
|
9356
|
+
reviewSwitch(repo, now) {
|
|
9357
|
+
const row = this.db.prepare("SELECT enabled, changed_by, changed_at FROM review_switch WHERE repo = ?").get(repo);
|
|
9358
|
+
if (row !== undefined)
|
|
9359
|
+
return { on: Number(row["enabled"]) === 1, source: "project", changedBy: String(row["changed_by"]), changedAt: String(row["changed_at"]) };
|
|
9360
|
+
const handsOff = this.activeMode(repo, now)?.name === "hands-off";
|
|
9361
|
+
return { on: handsOff, source: handsOff ? "hands-off" : "default", changedBy: null, changedAt: null };
|
|
9362
|
+
}
|
|
9363
|
+
/** Turn a project's automatic review on or off; the policy log keeps before → after. */
|
|
9364
|
+
setReviewSwitch(repo, on, by, now) {
|
|
9365
|
+
return this.transact(() => {
|
|
9366
|
+
const before = this.reviewSwitch(repo, now);
|
|
9367
|
+
this.db.prepare(`INSERT INTO review_switch (repo, enabled, changed_by, changed_at) VALUES (?, ?, ?, ?)
|
|
9368
|
+
ON CONFLICT(repo) DO UPDATE SET enabled = excluded.enabled, changed_by = excluded.changed_by, changed_at = excluded.changed_at`)
|
|
9369
|
+
.run(repo, on ? 1 : 0, by, now.toISOString());
|
|
9370
|
+
const after = this.reviewSwitch(repo, now);
|
|
9371
|
+
this.recordPolicy(by, repo, "automatic review", reviewSwitchWords(before), reviewSwitchWords(after), now);
|
|
9372
|
+
return { before, after };
|
|
9373
|
+
});
|
|
9374
|
+
}
|
|
9336
9375
|
/** A project deleted (project-delete.ts): who, and what it held → how it went. Returns the entry's id. */
|
|
9337
9376
|
recordProjectDeleted(by, repo, held, how, now) {
|
|
9338
9377
|
this.recordPolicy(by, repo, "project deleted", held, how, now, "deleted");
|
|
@@ -9982,6 +10021,10 @@ export class Store {
|
|
|
9982
10021
|
stampWorktreeSetup(path, digest) {
|
|
9983
10022
|
this.db.prepare("UPDATE worktree SET setup_digest = ? WHERE path = ?").run(digest, path);
|
|
9984
10023
|
}
|
|
10024
|
+
/** The checkout no longer holds what its setup made (its dependencies were dropped): the next lease runs setup again. */
|
|
10025
|
+
forgetWorktreeSetup(path) {
|
|
10026
|
+
this.db.prepare("UPDATE worktree SET setup_digest = NULL WHERE path = ?").run(path);
|
|
10027
|
+
}
|
|
9985
10028
|
// ---- verify command (Priority 2) ---------------------------------------
|
|
9986
10029
|
/**
|
|
9987
10030
|
* Approve one repo's verification command, revoking any predecessor in
|
|
@@ -12696,6 +12739,22 @@ export class Store {
|
|
|
12696
12739
|
.get(taskRef, head);
|
|
12697
12740
|
return Number(row?.["n"] ?? 0);
|
|
12698
12741
|
}
|
|
12742
|
+
/** Why a finished result can't be built again on the same filing: it was
|
|
12743
|
+
* verified, attested or accepted, or its branch was published (revised
|
|
12744
|
+
* through its pull request). Null when `requeueTask` may run it again. */
|
|
12745
|
+
finalResultReason(runId) {
|
|
12746
|
+
const verdict = this.proofVerdictFor(runId);
|
|
12747
|
+
if (verdict !== null && (verdict.verdict === "verified" || verdict.verdict === "attested"))
|
|
12748
|
+
return "accepted-result";
|
|
12749
|
+
if (this.proofAcceptance(runId) !== null)
|
|
12750
|
+
return "accepted-result";
|
|
12751
|
+
// An intent that never pushed is not a publication; a pushed or
|
|
12752
|
+
// opened branch is, and is revised through its pull request.
|
|
12753
|
+
const publication = this.publicationForRun(runId);
|
|
12754
|
+
if (publication !== null && (publication.state === "pushed" || publication.state === "opened"))
|
|
12755
|
+
return "published";
|
|
12756
|
+
return null;
|
|
12757
|
+
}
|
|
12699
12758
|
requeueTask(taskId, by, now) {
|
|
12700
12759
|
return this.transact(() => {
|
|
12701
12760
|
const ref = this.db
|
|
@@ -12734,16 +12793,9 @@ export class Store {
|
|
|
12734
12793
|
}
|
|
12735
12794
|
else {
|
|
12736
12795
|
rejectedRun = Number(last["id"]);
|
|
12737
|
-
const
|
|
12738
|
-
if (
|
|
12739
|
-
return { ok: false, reason:
|
|
12740
|
-
if (this.proofAcceptance(rejectedRun) !== null)
|
|
12741
|
-
return { ok: false, reason: "accepted-result" };
|
|
12742
|
-
// An intent that never pushed is not a publication; a pushed or
|
|
12743
|
-
// opened branch is, and is revised through its pull request.
|
|
12744
|
-
const publication = this.publicationForRun(rejectedRun);
|
|
12745
|
-
if (publication !== null && (publication.state === "pushed" || publication.state === "opened"))
|
|
12746
|
-
return { ok: false, reason: "published" };
|
|
12796
|
+
const final = this.finalResultReason(rejectedRun);
|
|
12797
|
+
if (final !== null)
|
|
12798
|
+
return { ok: false, reason: final };
|
|
12747
12799
|
}
|
|
12748
12800
|
}
|
|
12749
12801
|
else if (incidents.length === 0 && state !== "failed") {
|
|
@@ -16353,13 +16405,17 @@ export class Store {
|
|
|
16353
16405
|
});
|
|
16354
16406
|
}
|
|
16355
16407
|
// ---- Retention (v105) -----------------------------------------------------
|
|
16356
|
-
/** How long each kind of data is kept; null is forever (
|
|
16408
|
+
/** How long each kind of data is kept; null is forever. A kind nobody chose keeps its default (evidence 28 days, the rest forever). */
|
|
16357
16409
|
retentionPeriods() {
|
|
16358
|
-
|
|
16410
|
+
return { ...DEFAULT_PERIODS, ...this.retentionChosen() };
|
|
16411
|
+
}
|
|
16412
|
+
/** The periods someone chose: only kinds with a saved row. */
|
|
16413
|
+
retentionChosen() {
|
|
16414
|
+
const chosen = {};
|
|
16359
16415
|
for (const row of this.db.prepare("SELECT kind, days FROM retention_setting").all()) {
|
|
16360
|
-
|
|
16416
|
+
chosen[String(row["kind"])] = row["days"] === null ? null : Number(row["days"]);
|
|
16361
16417
|
}
|
|
16362
|
-
return
|
|
16418
|
+
return chosen;
|
|
16363
16419
|
}
|
|
16364
16420
|
/** Set how long one kind is kept (null: forever); the ledger keeps before → after. */
|
|
16365
16421
|
setRetentionPeriod(kind, days, by, now) {
|
|
@@ -20952,21 +21008,32 @@ export class Store {
|
|
|
20952
21008
|
/** Every owned run must have ended, and every retained local process
|
|
20953
21009
|
* witness must be absent. Read-only probes never authorize PID kills. */
|
|
20954
21010
|
stopQuiescenceProblem(runId) {
|
|
21011
|
+
return this.stopQuiescenceFact(runId)?.problem ?? null;
|
|
21012
|
+
}
|
|
21013
|
+
/** The same answer as stopQuiescenceProblem, with which run it names and
|
|
21014
|
+
* what kind of answer it is (needs-you.ts reads it): `alive` while
|
|
21015
|
+
* something of the run may still be running, `elsewhere` when only
|
|
21016
|
+
* another computer can tell, `unprovable` when nothing is known to run
|
|
21017
|
+
* but its exit was never recorded, `unknown` when this computer can't
|
|
21018
|
+
* check at all (both are what `run settle` resolves; only the first may
|
|
21019
|
+
* be called not running). */
|
|
21020
|
+
stopQuiescenceFact(runId) {
|
|
21021
|
+
const fact = (run, kind, problem) => ({ run, kind, problem });
|
|
20955
21022
|
const ids = this.ownedRunsOf(runId);
|
|
20956
21023
|
if (ids.length === 0)
|
|
20957
|
-
return `run #${runId} does not exist
|
|
21024
|
+
return fact(runId, "open", `run #${runId} does not exist`);
|
|
20958
21025
|
for (const id of ids) {
|
|
20959
21026
|
const run = this.getRun(id);
|
|
20960
21027
|
if (run.outcome === null)
|
|
20961
|
-
return `run #${id} is still open
|
|
21028
|
+
return fact(id, "open", `run #${id} is still open`);
|
|
20962
21029
|
if (ownedProcessCount(runOwnerTag(this, id)) > 0)
|
|
20963
|
-
return `run #${id} still has an owned subprocess
|
|
21030
|
+
return fact(id, "alive", `run #${id} still has an owned subprocess`);
|
|
20964
21031
|
const held = this.heldSessionOf(id);
|
|
20965
21032
|
if (held !== null && held.endedAt === null)
|
|
20966
|
-
return `run #${id}'s held supervisor has not finished shutdown
|
|
21033
|
+
return fact(id, "alive", `run #${id}'s held supervisor has not finished shutdown`);
|
|
20967
21034
|
const witnesses = this.db.prepare("SELECT * FROM run_process WHERE run = ? ORDER BY id").all(id);
|
|
20968
21035
|
if (run.providerStartedAt !== null && witnesses.length === 0) {
|
|
20969
|
-
return `run #${id} may have spawned before its process witness was recorded; exit is unproven
|
|
21036
|
+
return fact(id, "unprovable", `run #${id} may have spawned before its process witness was recorded; exit is unproven`);
|
|
20970
21037
|
}
|
|
20971
21038
|
for (const witness of witnesses) {
|
|
20972
21039
|
if (witness["exited_at"] !== null)
|
|
@@ -20980,12 +21047,12 @@ export class Store {
|
|
|
20980
21047
|
if (provenDeadByBootChange({ host, bootId }))
|
|
20981
21048
|
continue;
|
|
20982
21049
|
if (host !== hostname())
|
|
20983
|
-
return `run #${id}'s process belongs to another host; exit is unproven here
|
|
21050
|
+
return fact(id, "elsewhere", `run #${id}'s process belongs to another host; exit is unproven here`);
|
|
20984
21051
|
const backend = witness["containment"] === null || witness["containment"] === undefined ? null : String(witness["containment"]);
|
|
20985
21052
|
const container = witness["container"] === null || witness["container"] === undefined ? null : String(witness["container"]);
|
|
20986
21053
|
if (backend !== null || container !== null) {
|
|
20987
21054
|
if (backend === null || container === null)
|
|
20988
|
-
return `run #${id} has incomplete native containment custody
|
|
21055
|
+
return fact(id, "unknown", `run #${id} has incomplete native containment custody`);
|
|
20989
21056
|
if (witness["container_empty_at"] !== null)
|
|
20990
21057
|
continue;
|
|
20991
21058
|
// A native witness settles ONLY on the OS's word: the object is
|
|
@@ -20993,30 +21060,37 @@ export class Store {
|
|
|
20993
21060
|
// when empty). A transport exit never proved it.
|
|
20994
21061
|
const state = containerEmptiness(backend, container, process.platform, witness["container_identity"] == null ? null : String(witness["container_identity"]));
|
|
20995
21062
|
if (state === "populated")
|
|
20996
|
-
return `run #${id}'s ${backend} object ${container} still has members
|
|
21063
|
+
return fact(id, "alive", `run #${id}'s ${backend} object ${container} still has members`);
|
|
20997
21064
|
if (state === "unknown")
|
|
20998
|
-
return `run #${id}'s ${backend} object ${container} cannot be proven empty from here
|
|
21065
|
+
return fact(id, "unknown", `run #${id}'s ${backend} object ${container} cannot be proven empty from here`);
|
|
20999
21066
|
continue;
|
|
21000
21067
|
}
|
|
21001
21068
|
if (witness["pid"] === null)
|
|
21002
|
-
return `run #${id} has an incomplete spawn witness; exit is unproven
|
|
21069
|
+
return fact(id, "unprovable", `run #${id} has an incomplete spawn witness; exit is unproven`);
|
|
21003
21070
|
const pid = Number(witness["pid"]);
|
|
21004
21071
|
if (processMayBeAlive(pid, witness["process_group"] === 1, { observedAt: witness["observed_at"], finishedAt: run.finishedAt }))
|
|
21005
|
-
return `run #${id}'s process ${pid} may still be running
|
|
21072
|
+
return fact(id, "alive", `run #${id}'s process ${pid} may still be running`);
|
|
21006
21073
|
}
|
|
21007
21074
|
if (run.worktree !== null) {
|
|
21008
21075
|
try {
|
|
21009
21076
|
const occupied = worktreeProcessOccupancy(run.worktree);
|
|
21010
21077
|
if (occupied.held)
|
|
21011
|
-
return `run #${id}'s workspace is still held by process ${occupied.by}
|
|
21078
|
+
return fact(id, "alive", `run #${id}'s workspace is still held by process ${occupied.by}`);
|
|
21012
21079
|
}
|
|
21013
21080
|
catch {
|
|
21014
|
-
|
|
21081
|
+
// An unreadable workspace note is no proof either way; an approver's recorded word settles it.
|
|
21082
|
+
if (!this.workspaceConfirmedByApprover(id))
|
|
21083
|
+
return fact(id, "unknown", `run #${id}'s workspace occupancy could not be established`);
|
|
21015
21084
|
}
|
|
21016
21085
|
}
|
|
21017
21086
|
}
|
|
21018
21087
|
return null;
|
|
21019
21088
|
}
|
|
21089
|
+
/** `run settle` recorded an approver's word that a run whose workspace
|
|
21090
|
+
* note couldn't be read has stopped. */
|
|
21091
|
+
workspaceConfirmedByApprover(runId) {
|
|
21092
|
+
return this.db.prepare("SELECT 1 FROM action_ledger WHERE run_id = ? AND action = 'workspace settled by approver' LIMIT 1").get(runId) !== undefined;
|
|
21093
|
+
}
|
|
21020
21094
|
reserveRunProcess(runId, now, group = true) {
|
|
21021
21095
|
return Number(this.db.prepare("INSERT INTO run_process (run,host,process_group,observed_at,boot_id) VALUES (?,?,?,?,?)")
|
|
21022
21096
|
.run(runId, hostname(), group ? 1 : 0, now.toISOString(), currentBootId()).lastInsertRowid);
|
|
@@ -21030,6 +21104,10 @@ export class Store {
|
|
|
21030
21104
|
const rows = only === undefined
|
|
21031
21105
|
? this.db.prepare("SELECT * FROM run_process WHERE run = ? AND exited_at IS NULL").all(runId)
|
|
21032
21106
|
: this.db.prepare("SELECT * FROM run_process WHERE run = ? AND pid = ? AND process_group = ? AND exited_at IS NULL").all(runId, only.pid, only.group ? 1 : 0);
|
|
21107
|
+
// A finished run's witness gets the same proof the deploy's quiescence
|
|
21108
|
+
// check uses: ESRCH, or a process now holding the pid that was born after
|
|
21109
|
+
// the run ended. An open run has no finish time, so only ESRCH counts.
|
|
21110
|
+
const finishedAt = this.getRun(runId)?.finishedAt ?? null;
|
|
21033
21111
|
let recorded = 0;
|
|
21034
21112
|
for (const row of rows) {
|
|
21035
21113
|
const host = String(row["host"]), bootId = row["boot_id"] == null ? null : String(row["boot_id"]);
|
|
@@ -21043,9 +21121,7 @@ export class Store {
|
|
|
21043
21121
|
gone = backend !== null && container !== null && (row["container_empty_at"] != null || containerEmptiness(backend, container, process.platform, row["container_identity"] == null ? null : String(row["container_identity"])) === "empty");
|
|
21044
21122
|
}
|
|
21045
21123
|
else {
|
|
21046
|
-
|
|
21047
|
-
// With no historical witness argument only ESRCH proves absence.
|
|
21048
|
-
gone = row["pid"] != null && !processMayBeAlive(Number(row["pid"]), row["process_group"] === 1);
|
|
21124
|
+
gone = row["pid"] != null && !processMayBeAlive(Number(row["pid"]), row["process_group"] === 1, finishedAt === null ? undefined : { observedAt: row["observed_at"], finishedAt });
|
|
21049
21125
|
}
|
|
21050
21126
|
}
|
|
21051
21127
|
if (gone)
|
|
@@ -21056,10 +21132,161 @@ export class Store {
|
|
|
21056
21132
|
}
|
|
21057
21133
|
return recorded;
|
|
21058
21134
|
}
|
|
21135
|
+
/** The reconcile road for a finished run whose process outlived its
|
|
21136
|
+
* finish: once each process is provably gone, its exit is recorded, so
|
|
21137
|
+
* the task stops waiting on a person by itself. A run that can't be proven
|
|
21138
|
+
* yet stays open and is passed over, never holding newer runs behind it:
|
|
21139
|
+
* every finished run is visited, a batch at a time. Never signals. */
|
|
21140
|
+
recordFinishedRunExits(now, batch = 200) {
|
|
21141
|
+
const page = this.db.prepare(`SELECT DISTINCT p.run FROM run_process p JOIN run r ON r.id = p.run
|
|
21142
|
+
WHERE p.exited_at IS NULL AND p.host = ? AND p.run > ? AND r.outcome IS NOT NULL AND r.finished_at IS NOT NULL
|
|
21143
|
+
ORDER BY p.run LIMIT ?`);
|
|
21144
|
+
let recorded = 0;
|
|
21145
|
+
for (let after = 0;;) {
|
|
21146
|
+
const runs = page.all(hostname(), after, batch).map(row => Number(row["run"]));
|
|
21147
|
+
for (const run of runs)
|
|
21148
|
+
recorded += this.recordRunProcessExits(run, now);
|
|
21149
|
+
if (runs.length < batch)
|
|
21150
|
+
return recorded;
|
|
21151
|
+
after = runs[runs.length - 1];
|
|
21152
|
+
}
|
|
21153
|
+
}
|
|
21059
21154
|
finishUnspawnedProcess(witness, now) {
|
|
21060
21155
|
this.db.prepare("UPDATE run_process SET exited_at = ? WHERE id = ? AND pid IS NULL AND ((containment IS NULL AND container IS NULL) OR container_empty_at IS NOT NULL)")
|
|
21061
21156
|
.run(now.toISOString(), witness);
|
|
21062
21157
|
}
|
|
21158
|
+
/** The reconcile road for a pid-less witness a crash or a thrown spawn
|
|
21159
|
+
* left behind: once its run has finished, no live child is tracked for it
|
|
21160
|
+
* here, every other witness of the run has exited and none of the run's
|
|
21161
|
+
* process groups is alive, it settles as never started, with a ledger
|
|
21162
|
+
* entry. A sole pid-less witness has no group to prove gone and stays for
|
|
21163
|
+
* `run settle`. Native witnesses keep their OS-only road. */
|
|
21164
|
+
settleUnspawnedWitnesses(now) {
|
|
21165
|
+
const candidates = this.db.prepare(`SELECT DISTINCT p.run FROM run_process p JOIN run r ON r.id = p.run
|
|
21166
|
+
WHERE p.exited_at IS NULL AND p.pid IS NULL AND p.containment IS NULL AND p.container IS NULL
|
|
21167
|
+
AND p.host = ? AND r.outcome IS NOT NULL AND r.finished_at IS NOT NULL ORDER BY p.run`).all(hostname());
|
|
21168
|
+
let settled = 0;
|
|
21169
|
+
for (const candidate of candidates) {
|
|
21170
|
+
const runId = Number(candidate["run"]);
|
|
21171
|
+
this.recordRunProcessExits(runId, now);
|
|
21172
|
+
settled += this.transact(() => {
|
|
21173
|
+
const run = this.getRun(runId);
|
|
21174
|
+
if (run === null || run.outcome === null)
|
|
21175
|
+
return 0;
|
|
21176
|
+
if (ownedProcessCount(runOwnerTag(this, runId)) > 0)
|
|
21177
|
+
return 0;
|
|
21178
|
+
const held = this.heldSessionOf(runId);
|
|
21179
|
+
if (held !== null && held.endedAt === null)
|
|
21180
|
+
return 0;
|
|
21181
|
+
const rows = this.db.prepare("SELECT * FROM run_process WHERE run = ? ORDER BY id").all(runId);
|
|
21182
|
+
const pidless = rows.filter(row => row["exited_at"] === null && row["pid"] === null && row["containment"] === null && row["container"] === null && row["host"] === hostname());
|
|
21183
|
+
const others = rows.filter(row => !pidless.includes(row));
|
|
21184
|
+
if (pidless.length === 0 || others.length === 0)
|
|
21185
|
+
return 0;
|
|
21186
|
+
if (others.some(row => row["exited_at"] === null))
|
|
21187
|
+
return 0;
|
|
21188
|
+
// Exit was recorded once; the group's members are asked again now.
|
|
21189
|
+
const groups = others.filter(row => row["pid"] !== null && row["host"] === hostname());
|
|
21190
|
+
if (groups.length === 0 || groups.some(row => processMayBeAlive(Number(row["pid"]), row["process_group"] === 1, { observedAt: row["observed_at"], finishedAt: run.finishedAt })))
|
|
21191
|
+
return 0;
|
|
21192
|
+
const ref = this.refById(run.taskRef);
|
|
21193
|
+
let count = 0;
|
|
21194
|
+
for (const row of pidless) {
|
|
21195
|
+
const changed = this.db.prepare(`UPDATE run_process SET exited_at = ?
|
|
21196
|
+
WHERE id = ? AND run = ? AND pid IS NULL AND exited_at IS NULL AND containment IS NULL AND container IS NULL`)
|
|
21197
|
+
.run(now.toISOString(), row["id"], runId);
|
|
21198
|
+
if (Number(changed.changes) !== 1)
|
|
21199
|
+
continue;
|
|
21200
|
+
count++;
|
|
21201
|
+
this.recordAction({ at: now.toISOString(), actor: "system", repo: ref?.repo ?? null, taskId: ref?.externalId ?? null, runId,
|
|
21202
|
+
action: "process witness settled", outcome: "never started", source: "work",
|
|
21203
|
+
detail: `witness ${String(row["id"])}: no pid; the run finished, every other witness exited and none of its process groups is alive` });
|
|
21204
|
+
}
|
|
21205
|
+
return count;
|
|
21206
|
+
});
|
|
21207
|
+
}
|
|
21208
|
+
return settled;
|
|
21209
|
+
}
|
|
21210
|
+
/** `run settle` (an approver's last resort): every witness of a finished
|
|
21211
|
+
* run that cannot be proven either way is recorded settled under the
|
|
21212
|
+
* approver's reason, in the ledger. Refuses while anything of the run is
|
|
21213
|
+
* alive or could be checked only from another host. Never signals. */
|
|
21214
|
+
settleRunWitnessesByApprover(args, now) {
|
|
21215
|
+
return this.transact(() => {
|
|
21216
|
+
const run = this.getRun(args.runId);
|
|
21217
|
+
if (run === null)
|
|
21218
|
+
return { ok: false, reason: "unknown-run", detail: `no run #${args.runId}` };
|
|
21219
|
+
if (run.outcome === null)
|
|
21220
|
+
return { ok: false, reason: "still-running", detail: `run #${args.runId} is still open` };
|
|
21221
|
+
const alive = (detail) => ({ ok: false, reason: "alive", detail });
|
|
21222
|
+
if (ownedProcessCount(runOwnerTag(this, args.runId)) > 0)
|
|
21223
|
+
return alive(`run #${args.runId} still has an owned subprocess`);
|
|
21224
|
+
const held = this.heldSessionOf(args.runId);
|
|
21225
|
+
if (held !== null && held.endedAt === null)
|
|
21226
|
+
return alive(`run #${args.runId}'s held supervisor has not finished shutdown`);
|
|
21227
|
+
const rows = this.db.prepare("SELECT * FROM run_process WHERE run = ? ORDER BY id").all(args.runId);
|
|
21228
|
+
const open = rows.filter(row => row["exited_at"] === null);
|
|
21229
|
+
for (const row of rows) {
|
|
21230
|
+
if (row["pid"] === null)
|
|
21231
|
+
continue;
|
|
21232
|
+
if (row["host"] !== hostname()) {
|
|
21233
|
+
if (row["exited_at"] === null)
|
|
21234
|
+
return alive(`run #${args.runId}'s process ${String(row["pid"])} belongs to ${String(row["host"])}; settle it there`);
|
|
21235
|
+
continue;
|
|
21236
|
+
}
|
|
21237
|
+
if (processMayBeAlive(Number(row["pid"]), row["process_group"] === 1, { observedAt: row["observed_at"], finishedAt: run.finishedAt }))
|
|
21238
|
+
return alive(`run #${args.runId}'s process ${String(row["pid"])} is still running`);
|
|
21239
|
+
}
|
|
21240
|
+
for (const row of open) {
|
|
21241
|
+
if (row["pid"] === null && row["host"] !== hostname())
|
|
21242
|
+
return alive(`run #${args.runId} has a witness from ${String(row["host"])}; settle it there`);
|
|
21243
|
+
if (row["containment"] != null && row["container"] != null && row["container_empty_at"] == null &&
|
|
21244
|
+
containerEmptiness(String(row["containment"]), String(row["container"]), process.platform, row["container_identity"] == null ? null : String(row["container_identity"])) === "populated") {
|
|
21245
|
+
return alive(`run #${args.runId}'s ${String(row["containment"])} object ${String(row["container"])} still has members`);
|
|
21246
|
+
}
|
|
21247
|
+
}
|
|
21248
|
+
// A workspace still held by a live process refuses; one whose note can't
|
|
21249
|
+
// be read is settled by the approver's word, in the ledger.
|
|
21250
|
+
let workspace = false;
|
|
21251
|
+
if (run.worktree !== null) {
|
|
21252
|
+
try {
|
|
21253
|
+
const occupied = worktreeProcessOccupancy(run.worktree);
|
|
21254
|
+
if (occupied.held)
|
|
21255
|
+
return alive(`run #${args.runId}'s workspace is still held by process ${occupied.by}`);
|
|
21256
|
+
}
|
|
21257
|
+
catch {
|
|
21258
|
+
workspace = !this.workspaceConfirmedByApprover(args.runId);
|
|
21259
|
+
}
|
|
21260
|
+
}
|
|
21261
|
+
const ref = this.refById(run.taskRef);
|
|
21262
|
+
if (workspace) {
|
|
21263
|
+
this.recordAction({ at: now.toISOString(), actor: args.by, repo: ref?.repo ?? null, taskId: ref?.externalId ?? null, runId: args.runId,
|
|
21264
|
+
action: "workspace settled by approver", outcome: run.worktree ?? "", source: "request", detail: args.why });
|
|
21265
|
+
}
|
|
21266
|
+
// A run that may have spawned before any witness was written has none
|
|
21267
|
+
// to settle: the approver's word is recorded as one ended witness.
|
|
21268
|
+
if (rows.length === 0 && run.providerStartedAt !== null) {
|
|
21269
|
+
const id = Number(this.db.prepare("INSERT INTO run_process (run,host,process_group,observed_at,boot_id,exited_at) VALUES (?,?,0,?,?,?)")
|
|
21270
|
+
.run(args.runId, hostname(), now.toISOString(), currentBootId(), now.toISOString()).lastInsertRowid);
|
|
21271
|
+
this.recordAction({ at: now.toISOString(), actor: args.by, repo: ref?.repo ?? null, taskId: ref?.externalId ?? null, runId: args.runId,
|
|
21272
|
+
action: "process witness settled by approver", outcome: `witness ${id}`, source: "request", detail: args.why });
|
|
21273
|
+
this.settleQuiescentStops(now);
|
|
21274
|
+
return { ok: true, witnesses: [id], repeated: false };
|
|
21275
|
+
}
|
|
21276
|
+
if (open.length === 0) {
|
|
21277
|
+
if (workspace)
|
|
21278
|
+
this.settleQuiescentStops(now);
|
|
21279
|
+
return { ok: true, witnesses: [], repeated: !workspace };
|
|
21280
|
+
}
|
|
21281
|
+
const ids = open.map(row => Number(row["id"]));
|
|
21282
|
+
for (const id of ids)
|
|
21283
|
+
this.db.prepare("UPDATE run_process SET exited_at = ? WHERE id = ? AND run = ? AND exited_at IS NULL").run(now.toISOString(), id, args.runId);
|
|
21284
|
+
this.recordAction({ at: now.toISOString(), actor: args.by, repo: ref?.repo ?? null, taskId: ref?.externalId ?? null, runId: args.runId,
|
|
21285
|
+
action: "process witness settled by approver", outcome: `witness ${ids.join(", ")}`, source: "request", detail: args.why });
|
|
21286
|
+
this.settleQuiescentStops(now);
|
|
21287
|
+
return { ok: true, witnesses: ids, repeated: false };
|
|
21288
|
+
});
|
|
21289
|
+
}
|
|
21063
21290
|
recordRunProcess(runId, pid, now, group = true, witness) {
|
|
21064
21291
|
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
21065
21292
|
throw new Error("a process witness needs a valid spawned PID");
|
package/dist/surface.js
CHANGED
|
@@ -265,12 +265,13 @@ export const COMMAND_GUIDE = [
|
|
|
265
265
|
operator("task approve", "the yes — nothing builds without one; binds to the scope digest"),
|
|
266
266
|
operator("task requeue", "exit a stall: incidents resolved, strikes cleared, queued again"),
|
|
267
267
|
operator("task regate", "explicitly rerun the approved check on the last exact commit — no agent or automatic retry"),
|
|
268
|
-
operator("task review", "
|
|
268
|
+
operator("task review", "show a run's automatic review: HIGH findings, suggested follow-ups, or why it was not reviewed"),
|
|
269
269
|
operator("task accept", "record an explicit acceptance with limitations; does not change checks or prove deployment"),
|
|
270
270
|
operator("task repair", "inspect or approve an existing historical repair draft; use task revise for new feedback"),
|
|
271
271
|
operator("task route", "which configured agents plan, build and repair this task; phase and risk changes require renewed approval"),
|
|
272
272
|
operator("task reopen", "resume external work its tracker closed and has been SEEN open again"),
|
|
273
273
|
operator("task stop", "stop ONE exact live attempt (--run <id>): durable before any process is signalled, answers 'stopping' until its own processes are established gone; work, branch, and evidence preserved; no strike, no retry"),
|
|
274
|
+
operator("run settle", "last resort for a finished run whose process witness can't be proven either way: records your --why in the ledger; refuses while any of the run's processes is alive"),
|
|
274
275
|
operator("task resume", "resume the exact stopped attempt (--run <id>): refuses until it is quiescent, lifts only that stop's hold, approves nothing — the next pass re-proves the scope and inherits the draft with fresh proof"),
|
|
275
276
|
// ---- leases (runner surface) ----
|
|
276
277
|
{ invocation: "claim", synopsis: "take one ready, approved task; answers with a lease", audience: "agent", agentMayInvoke: true, mutation: "keyed",
|
|
@@ -358,6 +359,9 @@ export const COMMAND_GUIDE = [
|
|
|
358
359
|
operator("mode set", "sign a per-repository operating mode (password ceremony; standard or hands-off, always expiring)"),
|
|
359
360
|
operator("publish merge", "say yes to ONE waiting merge — it fires only when CI is seen green on the exact commit you authorized"),
|
|
360
361
|
operator("publish refire", "recover a merge whose issued call went silent — re-enters the full merge road, never auto-retried"),
|
|
362
|
+
operatorRead("review show", "whether a project's finished builds get one automatic review"),
|
|
363
|
+
operator("review on", "one automatic review of each finished build whose check passes; only HIGH findings send it back, once"),
|
|
364
|
+
operator("review off", "finished builds go straight to the person, unreviewed"),
|
|
361
365
|
operator("mode revoke", "revoke a repository's mode — one click, every act it covered falls back to its own ceremony"),
|
|
362
366
|
operator("config set", "set phase or spend configuration"),
|
|
363
367
|
operator("config clear", "clear phase or spend configuration"),
|
package/dist/task-status.d.ts
CHANGED
|
@@ -11,9 +11,12 @@
|
|
|
11
11
|
* output) is an amber note on its own row with one action; the exact
|
|
12
12
|
* technical reason stays one tap away in `why`. */
|
|
13
13
|
import type { AssignmentSnapshot } from "./assignment.js";
|
|
14
|
+
import { type NeedAction, type NeedContext, type NeedKey, type WaitKey } from "./needs-you.js";
|
|
15
|
+
export type { NeedKey, WaitKey } from "./needs-you.js";
|
|
14
16
|
/** A cancelled task with a successor reads "Replaced by <id>" everywhere, never "Cancelled". */
|
|
15
17
|
export declare const replacedWords: (successor: string) => string;
|
|
16
|
-
|
|
18
|
+
/** Waiting: a reason no person can act on, worded as what it waits for (needs-you.ts). */
|
|
19
|
+
export declare const HEADLINES: readonly ["Queued", "Planning", "Needs you", "Waiting", "Building", "Ready for review", "Complete", "Failed", "Stopped"];
|
|
17
20
|
export type Headline = (typeof HEADLINES)[number];
|
|
18
21
|
export type HeadlineTone = "neutral" | "live" | "attention" | "ready" | "success" | "danger";
|
|
19
22
|
export declare const HEADLINE_TONE: Readonly<Record<Headline, HeadlineTone>>;
|
|
@@ -34,6 +37,7 @@ export type StatusDetail = {
|
|
|
34
37
|
action: StatusAction | null;
|
|
35
38
|
why: string | null;
|
|
36
39
|
};
|
|
40
|
+
/** `need`: what a Needs you asks for and the one action that resolves it; null under every other headline. */
|
|
37
41
|
export type TaskStatus = {
|
|
38
42
|
headline: Headline;
|
|
39
43
|
tone: HeadlineTone;
|
|
@@ -41,8 +45,13 @@ export type TaskStatus = {
|
|
|
41
45
|
details: StatusDetail[];
|
|
42
46
|
primaryAction: StatusAction | null;
|
|
43
47
|
why: string[];
|
|
48
|
+
need?: {
|
|
49
|
+
key: NeedKey;
|
|
50
|
+
action: NeedAction;
|
|
51
|
+
} | null;
|
|
44
52
|
};
|
|
45
|
-
|
|
53
|
+
/** `reviewing`: the one automatic review (build-review.ts) is reading a finished build; it reads Building, not a headline of its own. */
|
|
54
|
+
export type TaskStage = "queued" | "planning" | "needs-you" | "waiting" | "building" | "checking" | "reviewing" | "finished" | "complete" | "failed" | "stopped";
|
|
46
55
|
export type ChecksFact = {
|
|
47
56
|
status: "passed" | "failed" | "running" | "not-run" | "unavailable";
|
|
48
57
|
exitCode: number | null;
|
|
@@ -65,8 +74,12 @@ export type PullRequestFact = {
|
|
|
65
74
|
};
|
|
66
75
|
export type TaskStatusFacts = {
|
|
67
76
|
stage: TaskStage;
|
|
68
|
-
/** Needs you: what a person is asked for. The sentence names it. */
|
|
69
|
-
need?:
|
|
77
|
+
/** Needs you: what a person is asked for (needs-you.ts). The sentence names it. */
|
|
78
|
+
need?: NeedKey;
|
|
79
|
+
/** Waiting: what the work waits for when no person can act. */
|
|
80
|
+
wait?: WaitKey;
|
|
81
|
+
/** The build or agent a need's sentence names. */
|
|
82
|
+
needContext?: NeedContext;
|
|
70
83
|
/** The recorded reason in plain words, when the stage has one. */
|
|
71
84
|
reason?: string | null;
|
|
72
85
|
/** A research report rather than a code change. */
|
|
@@ -121,12 +134,16 @@ export declare function pullRequestFactOf(publication: {
|
|
|
121
134
|
} | null): PullRequestFact | null;
|
|
122
135
|
/** The assignment's stage. The work status, when the caller read one, names
|
|
123
136
|
* the exact reason (a running planner, a pause, a failed attempt, a sign-in). */
|
|
124
|
-
export declare function assignmentStageOf(assignment: Pick<AssignmentSnapshot, "state" | "primaryAction"
|
|
137
|
+
export declare function assignmentStageOf(assignment: Pick<AssignmentSnapshot, "state" | "primaryAction"> & {
|
|
138
|
+
review?: AssignmentSnapshot["review"];
|
|
139
|
+
need?: AssignmentSnapshot["need"];
|
|
140
|
+
}, work?: {
|
|
125
141
|
token: string;
|
|
126
142
|
views?: readonly string[];
|
|
127
143
|
} | null, planning?: boolean): {
|
|
128
144
|
stage: TaskStage;
|
|
129
145
|
need?: TaskStatusFacts["need"];
|
|
146
|
+
wait?: WaitKey;
|
|
130
147
|
};
|
|
131
148
|
/** Requirements from the stored matrix: met, the ones only a person confirms, and the total. */
|
|
132
149
|
export declare function requirementsOf(matrix: readonly {
|