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
package/dist/store.js
CHANGED
|
@@ -215,6 +215,20 @@ CREATE TABLE IF NOT EXISTS project_mute (
|
|
|
215
215
|
const SECURITY_KINDS = new Set(["secret-detected", "security-release"]);
|
|
216
216
|
/** A failure that leaves nothing for the lead to try: the attempts are spent. */
|
|
217
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
|
+
}
|
|
218
232
|
/** Settings → Integrations (no version bump): each integration's last check. `outcome` is ok, failed, or absent (the check found
|
|
219
233
|
* nothing set up, such as no `gh` sign-in). `ok_at` and `error`/`error_at` keep the last success and the last
|
|
220
234
|
* failure across checks, so a row can say both. `account` is a display name only, never a credential. */
|
|
@@ -10007,6 +10021,10 @@ export class Store {
|
|
|
10007
10021
|
stampWorktreeSetup(path, digest) {
|
|
10008
10022
|
this.db.prepare("UPDATE worktree SET setup_digest = ? WHERE path = ?").run(digest, path);
|
|
10009
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
|
+
}
|
|
10010
10028
|
// ---- verify command (Priority 2) ---------------------------------------
|
|
10011
10029
|
/**
|
|
10012
10030
|
* Approve one repo's verification command, revoking any predecessor in
|
|
@@ -12721,6 +12739,22 @@ export class Store {
|
|
|
12721
12739
|
.get(taskRef, head);
|
|
12722
12740
|
return Number(row?.["n"] ?? 0);
|
|
12723
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
|
+
}
|
|
12724
12758
|
requeueTask(taskId, by, now) {
|
|
12725
12759
|
return this.transact(() => {
|
|
12726
12760
|
const ref = this.db
|
|
@@ -12759,16 +12793,9 @@ export class Store {
|
|
|
12759
12793
|
}
|
|
12760
12794
|
else {
|
|
12761
12795
|
rejectedRun = Number(last["id"]);
|
|
12762
|
-
const
|
|
12763
|
-
if (
|
|
12764
|
-
return { ok: false, reason:
|
|
12765
|
-
if (this.proofAcceptance(rejectedRun) !== null)
|
|
12766
|
-
return { ok: false, reason: "accepted-result" };
|
|
12767
|
-
// An intent that never pushed is not a publication; a pushed or
|
|
12768
|
-
// opened branch is, and is revised through its pull request.
|
|
12769
|
-
const publication = this.publicationForRun(rejectedRun);
|
|
12770
|
-
if (publication !== null && (publication.state === "pushed" || publication.state === "opened"))
|
|
12771
|
-
return { ok: false, reason: "published" };
|
|
12796
|
+
const final = this.finalResultReason(rejectedRun);
|
|
12797
|
+
if (final !== null)
|
|
12798
|
+
return { ok: false, reason: final };
|
|
12772
12799
|
}
|
|
12773
12800
|
}
|
|
12774
12801
|
else if (incidents.length === 0 && state !== "failed") {
|
|
@@ -20981,21 +21008,32 @@ export class Store {
|
|
|
20981
21008
|
/** Every owned run must have ended, and every retained local process
|
|
20982
21009
|
* witness must be absent. Read-only probes never authorize PID kills. */
|
|
20983
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 });
|
|
20984
21022
|
const ids = this.ownedRunsOf(runId);
|
|
20985
21023
|
if (ids.length === 0)
|
|
20986
|
-
return `run #${runId} does not exist
|
|
21024
|
+
return fact(runId, "open", `run #${runId} does not exist`);
|
|
20987
21025
|
for (const id of ids) {
|
|
20988
21026
|
const run = this.getRun(id);
|
|
20989
21027
|
if (run.outcome === null)
|
|
20990
|
-
return `run #${id} is still open
|
|
21028
|
+
return fact(id, "open", `run #${id} is still open`);
|
|
20991
21029
|
if (ownedProcessCount(runOwnerTag(this, id)) > 0)
|
|
20992
|
-
return `run #${id} still has an owned subprocess
|
|
21030
|
+
return fact(id, "alive", `run #${id} still has an owned subprocess`);
|
|
20993
21031
|
const held = this.heldSessionOf(id);
|
|
20994
21032
|
if (held !== null && held.endedAt === null)
|
|
20995
|
-
return `run #${id}'s held supervisor has not finished shutdown
|
|
21033
|
+
return fact(id, "alive", `run #${id}'s held supervisor has not finished shutdown`);
|
|
20996
21034
|
const witnesses = this.db.prepare("SELECT * FROM run_process WHERE run = ? ORDER BY id").all(id);
|
|
20997
21035
|
if (run.providerStartedAt !== null && witnesses.length === 0) {
|
|
20998
|
-
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`);
|
|
20999
21037
|
}
|
|
21000
21038
|
for (const witness of witnesses) {
|
|
21001
21039
|
if (witness["exited_at"] !== null)
|
|
@@ -21009,12 +21047,12 @@ export class Store {
|
|
|
21009
21047
|
if (provenDeadByBootChange({ host, bootId }))
|
|
21010
21048
|
continue;
|
|
21011
21049
|
if (host !== hostname())
|
|
21012
|
-
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`);
|
|
21013
21051
|
const backend = witness["containment"] === null || witness["containment"] === undefined ? null : String(witness["containment"]);
|
|
21014
21052
|
const container = witness["container"] === null || witness["container"] === undefined ? null : String(witness["container"]);
|
|
21015
21053
|
if (backend !== null || container !== null) {
|
|
21016
21054
|
if (backend === null || container === null)
|
|
21017
|
-
return `run #${id} has incomplete native containment custody
|
|
21055
|
+
return fact(id, "unknown", `run #${id} has incomplete native containment custody`);
|
|
21018
21056
|
if (witness["container_empty_at"] !== null)
|
|
21019
21057
|
continue;
|
|
21020
21058
|
// A native witness settles ONLY on the OS's word: the object is
|
|
@@ -21022,30 +21060,37 @@ export class Store {
|
|
|
21022
21060
|
// when empty). A transport exit never proved it.
|
|
21023
21061
|
const state = containerEmptiness(backend, container, process.platform, witness["container_identity"] == null ? null : String(witness["container_identity"]));
|
|
21024
21062
|
if (state === "populated")
|
|
21025
|
-
return `run #${id}'s ${backend} object ${container} still has members
|
|
21063
|
+
return fact(id, "alive", `run #${id}'s ${backend} object ${container} still has members`);
|
|
21026
21064
|
if (state === "unknown")
|
|
21027
|
-
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`);
|
|
21028
21066
|
continue;
|
|
21029
21067
|
}
|
|
21030
21068
|
if (witness["pid"] === null)
|
|
21031
|
-
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`);
|
|
21032
21070
|
const pid = Number(witness["pid"]);
|
|
21033
21071
|
if (processMayBeAlive(pid, witness["process_group"] === 1, { observedAt: witness["observed_at"], finishedAt: run.finishedAt }))
|
|
21034
|
-
return `run #${id}'s process ${pid} may still be running
|
|
21072
|
+
return fact(id, "alive", `run #${id}'s process ${pid} may still be running`);
|
|
21035
21073
|
}
|
|
21036
21074
|
if (run.worktree !== null) {
|
|
21037
21075
|
try {
|
|
21038
21076
|
const occupied = worktreeProcessOccupancy(run.worktree);
|
|
21039
21077
|
if (occupied.held)
|
|
21040
|
-
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}`);
|
|
21041
21079
|
}
|
|
21042
21080
|
catch {
|
|
21043
|
-
|
|
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`);
|
|
21044
21084
|
}
|
|
21045
21085
|
}
|
|
21046
21086
|
}
|
|
21047
21087
|
return null;
|
|
21048
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
|
+
}
|
|
21049
21094
|
reserveRunProcess(runId, now, group = true) {
|
|
21050
21095
|
return Number(this.db.prepare("INSERT INTO run_process (run,host,process_group,observed_at,boot_id) VALUES (?,?,?,?,?)")
|
|
21051
21096
|
.run(runId, hostname(), group ? 1 : 0, now.toISOString(), currentBootId()).lastInsertRowid);
|
|
@@ -21059,6 +21104,10 @@ export class Store {
|
|
|
21059
21104
|
const rows = only === undefined
|
|
21060
21105
|
? this.db.prepare("SELECT * FROM run_process WHERE run = ? AND exited_at IS NULL").all(runId)
|
|
21061
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;
|
|
21062
21111
|
let recorded = 0;
|
|
21063
21112
|
for (const row of rows) {
|
|
21064
21113
|
const host = String(row["host"]), bootId = row["boot_id"] == null ? null : String(row["boot_id"]);
|
|
@@ -21072,9 +21121,7 @@ export class Store {
|
|
|
21072
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");
|
|
21073
21122
|
}
|
|
21074
21123
|
else {
|
|
21075
|
-
|
|
21076
|
-
// With no historical witness argument only ESRCH proves absence.
|
|
21077
|
-
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 });
|
|
21078
21125
|
}
|
|
21079
21126
|
}
|
|
21080
21127
|
if (gone)
|
|
@@ -21085,6 +21132,25 @@ export class Store {
|
|
|
21085
21132
|
}
|
|
21086
21133
|
return recorded;
|
|
21087
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
|
+
}
|
|
21088
21154
|
finishUnspawnedProcess(witness, now) {
|
|
21089
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)")
|
|
21090
21156
|
.run(now.toISOString(), witness);
|
|
@@ -21179,12 +21245,42 @@ export class Store {
|
|
|
21179
21245
|
return alive(`run #${args.runId}'s ${String(row["containment"])} object ${String(row["container"])} still has members`);
|
|
21180
21246
|
}
|
|
21181
21247
|
}
|
|
21182
|
-
|
|
21183
|
-
|
|
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
|
+
}
|
|
21184
21281
|
const ids = open.map(row => Number(row["id"]));
|
|
21185
21282
|
for (const id of ids)
|
|
21186
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);
|
|
21187
|
-
const ref = this.refById(run.taskRef);
|
|
21188
21284
|
this.recordAction({ at: now.toISOString(), actor: args.by, repo: ref?.repo ?? null, taskId: ref?.externalId ?? null, runId: args.runId,
|
|
21189
21285
|
action: "process witness settled by approver", outcome: `witness ${ids.join(", ")}`, source: "request", detail: args.why });
|
|
21190
21286
|
this.settleQuiescentStops(now);
|
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,9 +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. */
|
|
46
|
-
export type TaskStage = "queued" | "planning" | "needs-you" | "building" | "checking" | "reviewing" | "finished" | "complete" | "failed" | "stopped";
|
|
54
|
+
export type TaskStage = "queued" | "planning" | "needs-you" | "waiting" | "building" | "checking" | "reviewing" | "finished" | "complete" | "failed" | "stopped";
|
|
47
55
|
export type ChecksFact = {
|
|
48
56
|
status: "passed" | "failed" | "running" | "not-run" | "unavailable";
|
|
49
57
|
exitCode: number | null;
|
|
@@ -66,8 +74,12 @@ export type PullRequestFact = {
|
|
|
66
74
|
};
|
|
67
75
|
export type TaskStatusFacts = {
|
|
68
76
|
stage: TaskStage;
|
|
69
|
-
/** Needs you: what a person is asked for. The sentence names it. */
|
|
70
|
-
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;
|
|
71
83
|
/** The recorded reason in plain words, when the stage has one. */
|
|
72
84
|
reason?: string | null;
|
|
73
85
|
/** A research report rather than a code change. */
|
|
@@ -124,12 +136,14 @@ export declare function pullRequestFactOf(publication: {
|
|
|
124
136
|
* the exact reason (a running planner, a pause, a failed attempt, a sign-in). */
|
|
125
137
|
export declare function assignmentStageOf(assignment: Pick<AssignmentSnapshot, "state" | "primaryAction"> & {
|
|
126
138
|
review?: AssignmentSnapshot["review"];
|
|
139
|
+
need?: AssignmentSnapshot["need"];
|
|
127
140
|
}, work?: {
|
|
128
141
|
token: string;
|
|
129
142
|
views?: readonly string[];
|
|
130
143
|
} | null, planning?: boolean): {
|
|
131
144
|
stage: TaskStage;
|
|
132
145
|
need?: TaskStatusFacts["need"];
|
|
146
|
+
wait?: WaitKey;
|
|
133
147
|
};
|
|
134
148
|
/** Requirements from the stored matrix: met, the ones only a person confirms, and the total. */
|
|
135
149
|
export declare function requirementsOf(matrix: readonly {
|
package/dist/task-status.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { CODE_NEED, NEEDS, needSentence, waitSentence } from "./needs-you.js";
|
|
1
2
|
/** A cancelled task with a successor reads "Replaced by <id>" everywhere, never "Cancelled". */
|
|
2
3
|
export const replacedWords = (successor) => `Replaced by ${successor}`;
|
|
3
4
|
const REPLACED = /^Replaced by \S+/;
|
|
4
|
-
|
|
5
|
+
/** Waiting: a reason no person can act on, worded as what it waits for (needs-you.ts). */
|
|
6
|
+
export const HEADLINES = ["Queued", "Planning", "Needs you", "Waiting", "Building", "Ready for review", "Complete", "Failed", "Stopped"];
|
|
5
7
|
export const HEADLINE_TONE = {
|
|
6
|
-
Queued: "neutral", Planning: "live", "Needs you": "attention", Building: "live",
|
|
8
|
+
Queued: "neutral", Planning: "live", "Needs you": "attention", Waiting: "neutral", Building: "live",
|
|
7
9
|
"Ready for review": "ready", Complete: "success", Failed: "danger", Stopped: "neutral",
|
|
8
10
|
};
|
|
9
11
|
const short = (sha) => sha !== null && /^[a-f0-9]{7,40}$/.test(sha) ? sha.slice(0, 7) : null;
|
|
@@ -12,6 +14,7 @@ export function headlineOf(facts) {
|
|
|
12
14
|
case "queued": return "Queued";
|
|
13
15
|
case "planning": return "Planning";
|
|
14
16
|
case "needs-you": return "Needs you";
|
|
17
|
+
case "waiting": return "Waiting";
|
|
15
18
|
case "building":
|
|
16
19
|
case "checking":
|
|
17
20
|
case "reviewing": return "Building";
|
|
@@ -33,10 +36,9 @@ function sentenceOf(headline, facts) {
|
|
|
33
36
|
switch (headline) {
|
|
34
37
|
case "Queued": return reason ?? "Waiting for a worker.";
|
|
35
38
|
case "Planning": return reason ?? "The lead is writing the plan.";
|
|
36
|
-
//
|
|
37
|
-
case "Needs you": return facts.need
|
|
38
|
-
|
|
39
|
-
: "Something needs your decision before the work can continue.");
|
|
39
|
+
// One plain sentence per need (needs-you.ts); a recorded reason only where the need keeps one.
|
|
40
|
+
case "Needs you": return needSentence(facts.need ?? "other", reason, facts.needContext);
|
|
41
|
+
case "Waiting": return waitSentence(facts.wait, reason, facts.needContext);
|
|
40
42
|
case "Building": return facts.stage === "checking" ? "Checks are running on the change."
|
|
41
43
|
: facts.stage === "reviewing" ? "Reviewing the change before it reaches you." : reason ?? "An agent is working on it.";
|
|
42
44
|
case "Ready for review":
|
|
@@ -133,8 +135,12 @@ function detailsOf(headline, facts) {
|
|
|
133
135
|
}
|
|
134
136
|
export function taskStatusOf(facts) {
|
|
135
137
|
const headline = headlineOf(facts);
|
|
138
|
+
// Every Needs you carries the one action that resolves it, worded the same everywhere.
|
|
139
|
+
const need = headline === "Needs you" ? { key: facts.need ?? "other", action: NEEDS[facts.need ?? "other"].action } : null;
|
|
140
|
+
const ownWords = need !== null && (need.key === "other" || need.key === "review-result") && facts.action != null;
|
|
141
|
+
const primaryAction = need === null || ownWords ? facts.action ?? null : { label: need.action.label, href: facts.action?.href ?? null };
|
|
136
142
|
return { headline, tone: HEADLINE_TONE[headline], sentence: sentenceOf(headline, facts), details: detailsOf(headline, facts),
|
|
137
|
-
primaryAction
|
|
143
|
+
primaryAction, why: [...new Set(facts.why ?? [])].filter(one => one.trim() !== ""), need };
|
|
138
144
|
}
|
|
139
145
|
// ---- reading the existing projections ----------------------------------------
|
|
140
146
|
/** Every dispatch or work-index code, as the stage it means. Unknown codes that
|
|
@@ -146,6 +152,7 @@ const CODE_STAGE = {
|
|
|
146
152
|
"signed-out": ["needs-you", "sign-in"],
|
|
147
153
|
"needs-approval": ["needs-you", "approval"],
|
|
148
154
|
"waiting-decision": ["needs-you", "answer"], "decision-queue": ["needs-you", "answer"],
|
|
155
|
+
"build-stopping": ["waiting"], "other-computer": ["waiting"],
|
|
149
156
|
stopping: ["stopped"], stopped: ["stopped"], paused: ["stopped"], cancelled: ["stopped"],
|
|
150
157
|
failed: ["failed"], "waiting-incident": ["failed"], "checks-failed": ["failed"],
|
|
151
158
|
"ready-to-check": ["finished"], "ready-to-review": ["finished"], "report-ready": ["finished"], "no-change": ["finished"],
|
|
@@ -166,11 +173,11 @@ export function stageOfCode(code, options = {}) {
|
|
|
166
173
|
if (code === "running" && options.planning)
|
|
167
174
|
return { stage: "planning" };
|
|
168
175
|
if (code === "held")
|
|
169
|
-
return options.operatorHold === false ? { stage: "needs-you", need: "
|
|
176
|
+
return options.operatorHold === false ? { stage: "needs-you", need: "hold" } : { stage: "stopped" };
|
|
170
177
|
const known = CODE_STAGE[code];
|
|
171
178
|
if (known !== undefined)
|
|
172
179
|
return known[1] === undefined ? { stage: known[0] } : { stage: known[0], need: known[1] };
|
|
173
|
-
return options.needsPerson === false ? { stage: "queued" } : { stage: "needs-you", need: "other" };
|
|
180
|
+
return options.needsPerson === false ? { stage: "queued" } : { stage: "needs-you", need: CODE_NEED[code] ?? "other" };
|
|
174
181
|
}
|
|
175
182
|
/** The pull request as the task page's follower reports it, or the bare publication row. */
|
|
176
183
|
export function pullRequestFactOf(publication) {
|
|
@@ -197,6 +204,10 @@ export function pullRequestFactOf(publication) {
|
|
|
197
204
|
/** The assignment's stage. The work status, when the caller read one, names
|
|
198
205
|
* the exact reason (a running planner, a pause, a failed attempt, a sign-in). */
|
|
199
206
|
export function assignmentStageOf(assignment, work, planning = false) {
|
|
207
|
+
// The assignment's own reading (a build Toolroll can't confirm stopped) comes first.
|
|
208
|
+
const own = assignment.need ?? null;
|
|
209
|
+
if (own !== null && (assignment.state === "working" || assignment.state === "needs-decision"))
|
|
210
|
+
return "wait" in own ? { stage: "waiting", wait: own.wait } : { stage: "needs-you", need: own.key };
|
|
200
211
|
switch (assignment.state) {
|
|
201
212
|
case "cancelled": return { stage: "stopped" };
|
|
202
213
|
case "complete": return { stage: "complete" };
|
|
@@ -211,8 +222,10 @@ export function assignmentStageOf(assignment, work, planning = false) {
|
|
|
211
222
|
const action = assignment.primaryAction?.code;
|
|
212
223
|
if (action === "approve-scope")
|
|
213
224
|
return { stage: "needs-you", need: "approval" };
|
|
214
|
-
if (action === "answer-decision"
|
|
225
|
+
if (action === "answer-decision")
|
|
215
226
|
return { stage: "needs-you", need: "answer" };
|
|
227
|
+
if (action === "inspect-decisions")
|
|
228
|
+
return { stage: "needs-you", need: "questions" };
|
|
216
229
|
const token = work?.token ?? "";
|
|
217
230
|
// A failed attempt is Failed. A saved result that still needs a decision
|
|
218
231
|
// (its scope, a hold, a process exit) is Needs you, even if its checks
|
|
@@ -220,7 +233,10 @@ export function assignmentStageOf(assignment, work, planning = false) {
|
|
|
220
233
|
if (token === "failed" || token === "waiting-incident")
|
|
221
234
|
return { stage: "failed" };
|
|
222
235
|
const read = stageOfCode(token, { needsPerson: true, operatorHold: action === "unhold" });
|
|
223
|
-
|
|
236
|
+
if (read.stage === "stopped" || read.stage === "waiting")
|
|
237
|
+
return read;
|
|
238
|
+
// A saved result that isn't ready keeps its own reading; a task's dispatch reason names its need.
|
|
239
|
+
return { stage: "needs-you", need: read.need ?? "other" };
|
|
224
240
|
}
|
|
225
241
|
}
|
|
226
242
|
}
|
|
@@ -243,11 +259,12 @@ export function evidenceOf(receipt, problems = receipt?.caveats ?? []) {
|
|
|
243
259
|
}
|
|
244
260
|
/** The status facts of one assignment, as every surface reads them. */
|
|
245
261
|
export function assignmentStatusFacts(assignment, options = {}) {
|
|
246
|
-
const { stage, need } = assignmentStageOf(assignment, options.work ?? null, options.planning ?? false);
|
|
262
|
+
const { stage, need, wait } = assignmentStageOf(assignment, options.work ?? null, options.planning ?? false);
|
|
263
|
+
const build = assignment.need?.build ?? null;
|
|
247
264
|
const receipt = assignment.receipt;
|
|
248
265
|
const finished = stage === "finished" || stage === "complete";
|
|
249
266
|
// A saved result's rows show whenever it is the reason a person is needed, too.
|
|
250
|
-
const withResult = finished || stage === "failed" || (stage === "needs-you" && receipt !== null);
|
|
267
|
+
const withResult = finished || stage === "failed" || ((stage === "needs-you" || stage === "waiting") && receipt !== null);
|
|
251
268
|
const report = receipt?.completionKind === "research-report";
|
|
252
269
|
const checks = receipt === null || !withResult ? null
|
|
253
270
|
: { status: receipt.checks.status, exitCode: receipt.checks.exitCode, head: receipt.head,
|
|
@@ -257,11 +274,11 @@ export function assignmentStatusFacts(assignment, options = {}) {
|
|
|
257
274
|
: publication === null ? (withResult && !report ? { state: "none", number: null, url: null, ci: null, error: null } : null)
|
|
258
275
|
: pullRequestFactOf(publication);
|
|
259
276
|
// Ready, Complete and Failed speak for themselves; every other stage keeps its recorded reason.
|
|
260
|
-
const reason = stage === "needs-you" || stage === "stopped" || stage === "queued" || (stage === "failed" && checks?.status !== "failed")
|
|
277
|
+
const reason = stage === "needs-you" || stage === "waiting" || stage === "stopped" || stage === "queued" || (stage === "failed" && checks?.status !== "failed")
|
|
261
278
|
? stage === "stopped" && assignment.state === "cancelled" ? REPLACED.test(assignment.detail) ? assignment.detail : "Cancelled. Nothing else will run." : plainReasonOf(stage, options.work?.token ?? "", options.work?.detail ?? assignment.detail)
|
|
262
279
|
: null;
|
|
263
280
|
return {
|
|
264
|
-
stage, ...(need === undefined ? {} : { need }), reason, report,
|
|
281
|
+
stage, ...(need === undefined ? {} : { need }), ...(wait === undefined ? {} : { wait }), ...(build === null ? {} : { needContext: { build } }), reason, report,
|
|
265
282
|
checks,
|
|
266
283
|
pullRequest: withResult ? pullRequest : null,
|
|
267
284
|
requirements: withResult ? requirementsOf(receipt?.proof?.matrix) : null,
|
|
@@ -292,7 +309,7 @@ export function statusDetailLines(status) {
|
|
|
292
309
|
}
|
|
293
310
|
/** The headline's emoji for chat cards (colour is never the only signal: the words follow). */
|
|
294
311
|
export function headlineEmoji(headline) {
|
|
295
|
-
return { Queued: "🕓", Planning: "📝", "Needs you": "👋", Building: "⏳", "Ready for review": "✅", Complete: "✅", Failed: "❌", Stopped: "⏹" }[headline];
|
|
312
|
+
return { Queued: "🕓", Planning: "📝", "Needs you": "👋", Waiting: "⏸", Building: "⏳", "Ready for review": "✅", Complete: "✅", Failed: "❌", Stopped: "⏹" }[headline];
|
|
296
313
|
}
|
|
297
314
|
const escapeHtml = (value) => value.replace(/[&<>"']/g, char => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[char]);
|
|
298
315
|
const ICON_PATHS = {
|
|
@@ -332,8 +349,8 @@ export function stageOfDispatch(d, options = {}) {
|
|
|
332
349
|
if (d.code === "complete" || d.code.startsWith("review-"))
|
|
333
350
|
return { stage: options.completed ? "complete" : "finished" };
|
|
334
351
|
if (d.code === "proof-refuted")
|
|
335
|
-
return /approved check failed|checks failed/i.test(d.detail ?? "") ? { stage: "failed" } : { stage: "needs-you", need: "
|
|
352
|
+
return /approved check failed|checks failed/i.test(d.detail ?? "") ? { stage: "failed" } : { stage: "needs-you", need: "review-result" };
|
|
336
353
|
if (d.code === "needs-verification")
|
|
337
|
-
return { stage: "needs-you", need: "
|
|
354
|
+
return { stage: "needs-you", need: "review-result" };
|
|
338
355
|
return stageOfCode(d.code, { needsPerson: d.condition === "waiting" && d.action !== null, planning: d.condition === "running" && d.role === "planner", operatorHold: d.action === "unhold" });
|
|
339
356
|
}
|
|
@@ -86,6 +86,7 @@ export function telegramProgressCard(store, run, taskId, project, now = new Date
|
|
|
86
86
|
const failedChecks = status.details.some(one => one.key === "checks" && (one.mark === "failed" || one.mark === "note"));
|
|
87
87
|
const recovery = operatorHold !== undefined || accessBlocked || (!built && run.outcome !== null);
|
|
88
88
|
return { text, next, entities: [{ type: "bold", offset: 0, length: title.length }, { type: "bold", offset: title.length + 1, length: heading.length }],
|
|
89
|
-
|
|
89
|
+
// Needs you: the button names the action that resolves it.
|
|
90
|
+
link: { label: status.need != null && status.need.key !== "other" && status.need.key !== "review-result" ? status.need.action.label : built ? "Open result" : "Open task", path: recovery
|
|
90
91
|
? chatControlHref("recovery", taskId) : built ? chatResultHref(taskId, run.id, failedChecks ? "checks" : "summary") : chatControlHref("task", taskId) } };
|
|
91
92
|
}
|
package/dist/telegram-status.js
CHANGED
|
@@ -3,7 +3,7 @@ import { manualReviewOnly } from "./proof.js";
|
|
|
3
3
|
/** Read-only phone views over the same dispatch and proof records as chat.
|
|
4
4
|
* No provider calls, repository access, new workflow state, or inferred success. */
|
|
5
5
|
import { diagnoseTaskDispatch, withDispatchDiagnoses } from "./dispatch.js";
|
|
6
|
-
import {
|
|
6
|
+
import { familyCompleted } from "./result-completion.js";
|
|
7
7
|
import { plainReasonOf, stageOfDispatch, taskStatusOf } from "./task-status.js";
|
|
8
8
|
import { CHAT_CONTROLS, chatControlHref, chatResultHref } from "./chat-controls.js";
|
|
9
9
|
export function phoneCommand(text) {
|
|
@@ -54,8 +54,8 @@ export function projectLabel(repo) {
|
|
|
54
54
|
}
|
|
55
55
|
/** The shared headline (task-status.ts) for a dispatch diagnosis on the phone. */
|
|
56
56
|
function headlineFor(store, id, d) {
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
// Any version of a completed family reads Complete, as taskWaitSnapshot says.
|
|
58
|
+
const completed = d.code === "complete" && familyCompleted(store, id);
|
|
59
59
|
const read = stageOfDispatch(d, { completed });
|
|
60
60
|
const finished = read.stage === "finished" || read.stage === "complete" || read.stage === "failed";
|
|
61
61
|
return taskStatusOf({ stage: read.stage, ...(read.need === undefined ? {} : { need: read.need }), reason: finished ? null : plainReasonOf(read.stage, d.code, d.detail),
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Name prefixes of what tests, journeys and scripts leave in the temp folder. */
|
|
2
|
+
export declare const TEST_TEMP_PREFIXES: readonly string[];
|
|
3
|
+
export declare function isTestTemp(name: string): boolean;
|
|
4
|
+
/** Where they collect: the temp folder (and /tmp, where some scripts write). */
|
|
5
|
+
export declare function tempRoots(): string[];
|
|
6
|
+
export type TempLeftover = {
|
|
7
|
+
path: string;
|
|
8
|
+
touchedAt: number;
|
|
9
|
+
};
|
|
10
|
+
/** Every test temp folder in `roots`, and which of them nothing has touched for a day. */
|
|
11
|
+
export declare function testTempFolders(roots: readonly string[], now: Date): {
|
|
12
|
+
all: TempLeftover[];
|
|
13
|
+
stale: TempLeftover[];
|
|
14
|
+
};
|
|
15
|
+
/** Remove the test temp folders nothing has touched for a day; each is looked at again right before it goes. */
|
|
16
|
+
export declare function removeStaleTestTemp(roots: readonly string[], now: Date): {
|
|
17
|
+
removed: string[];
|
|
18
|
+
failed: string[];
|
|
19
|
+
};
|