toolroll 0.9.8 → 0.9.10
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 +30 -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 +42 -8
- 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/chat-delivery-state.d.ts +5 -0
- package/dist/chat-delivery.js +45 -8
- package/dist/chat-quiet.d.ts +10 -4
- package/dist/chat-quiet.js +87 -22
- package/dist/chat-voice.d.ts +68 -0
- package/dist/chat-voice.js +175 -0
- 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/discord-chat.js +1 -0
- 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 +122 -15
- package/dist/plane-review.js +4 -0
- package/dist/process-recovery-eligibility.d.ts +1 -0
- package/dist/project-delete.js +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/releases.d.ts +9 -0
- package/dist/releases.js +30 -0
- 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/slack-chat.js +2 -0
- package/dist/storage.js +2 -0
- package/dist/store.d.ts +60 -0
- package/dist/store.js +207 -30
- package/dist/task-status.d.ts +18 -4
- package/dist/task-status.js +35 -18
- package/dist/teams-chat.js +1 -1
- package/dist/telegram-progress.d.ts +6 -1
- package/dist/telegram-progress.js +13 -5
- package/dist/telegram-status.d.ts +2 -1
- package/dist/telegram-status.js +6 -5
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +93 -43
- 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
|
@@ -139,7 +139,8 @@ CREATE TABLE IF NOT EXISTS monitoring_status (
|
|
|
139
139
|
* every step, and an optional evening digest at a local HH:MM. `digest_on` is the local day the last evening
|
|
140
140
|
* digest was considered, so each day is judged once. A task's card is the one chat message a destination keeps
|
|
141
141
|
* for it (or for several tasks filed within a minute): `message` is the Telegram message id or the chat app's
|
|
142
|
-
* part id, `digest` the text last shown, so an unchanged card is never re-sent.
|
|
142
|
+
* part id, `digest` the text last shown, so an unchanged card is never re-sent. A `chat_batch` is one person's
|
|
143
|
+
* finished-work message: every result that lands within two minutes of its first joins it, edited in place. */
|
|
143
144
|
const QUIET_CHAT_SCHEMA = `
|
|
144
145
|
CREATE TABLE IF NOT EXISTS notification_preference (
|
|
145
146
|
account TEXT PRIMARY KEY,
|
|
@@ -166,6 +167,21 @@ CREATE TABLE IF NOT EXISTS chat_card_task (
|
|
|
166
167
|
PRIMARY KEY (destination, task_ref)
|
|
167
168
|
);
|
|
168
169
|
CREATE INDEX IF NOT EXISTS chat_card_task_card ON chat_card_task(card);
|
|
170
|
+
CREATE TABLE IF NOT EXISTS chat_batch (
|
|
171
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
172
|
+
destination TEXT NOT NULL,
|
|
173
|
+
opened_at TEXT NOT NULL,
|
|
174
|
+
message TEXT,
|
|
175
|
+
digest TEXT
|
|
176
|
+
);
|
|
177
|
+
CREATE INDEX IF NOT EXISTS chat_batch_destination ON chat_batch(destination, opened_at);
|
|
178
|
+
CREATE TABLE IF NOT EXISTS chat_batch_item (
|
|
179
|
+
batch INTEGER NOT NULL REFERENCES chat_batch(id),
|
|
180
|
+
task_ref INTEGER NOT NULL,
|
|
181
|
+
run INTEGER,
|
|
182
|
+
joined_at TEXT NOT NULL,
|
|
183
|
+
PRIMARY KEY (batch, task_ref)
|
|
184
|
+
);
|
|
169
185
|
`;
|
|
170
186
|
/** Pings follow responsibility (no version bump: additive only). `lead_credential` is a lead token: an agent acting
|
|
171
187
|
* for `owner`, only its hash kept. `task_act` is who filed, approved, cancelled, completed or handed on a task
|
|
@@ -215,6 +231,20 @@ CREATE TABLE IF NOT EXISTS project_mute (
|
|
|
215
231
|
const SECURITY_KINDS = new Set(["secret-detected", "security-release"]);
|
|
216
232
|
/** A failure that leaves nothing for the lead to try: the attempts are spent. */
|
|
217
233
|
const SPENT_KINDS = new Set(["attempts-exhausted", "plan-attempts-exhausted"]);
|
|
234
|
+
/** Store.stopQuiescenceFact, read safely: a deploy proves completion with this code over the INSTALLED runtime's store,
|
|
235
|
+
* which may predate the method. There the older stopQuiescenceProblem decides, and its words give the kind. */
|
|
236
|
+
export function stopFactOf(store, runId) {
|
|
237
|
+
if (typeof store.stopQuiescenceFact === "function")
|
|
238
|
+
return store.stopQuiescenceFact(runId);
|
|
239
|
+
const problem = store.stopQuiescenceProblem(runId);
|
|
240
|
+
if (problem === null)
|
|
241
|
+
return null;
|
|
242
|
+
const kind = /still open|does not exist/.test(problem) ? "open"
|
|
243
|
+
: /another host/.test(problem) ? "elsewhere"
|
|
244
|
+
: /may still be running|still has|held by|has not finished|still has members/.test(problem) ? "alive"
|
|
245
|
+
: /cannot be proven|could not be established|incomplete native/.test(problem) ? "unknown" : "unprovable";
|
|
246
|
+
return { run: Number(/run #(\d+)/.exec(problem)?.[1] ?? runId), kind, problem };
|
|
247
|
+
}
|
|
218
248
|
/** Settings → Integrations (no version bump): each integration's last check. `outcome` is ok, failed, or absent (the check found
|
|
219
249
|
* nothing set up, such as no `gh` sign-in). `ok_at` and `error`/`error_at` keep the last success and the last
|
|
220
250
|
* failure across checks, so a row can say both. `account` is a display name only, never a credential. */
|
|
@@ -3812,6 +3842,8 @@ function initializeStore(db, file) {
|
|
|
3812
3842
|
db.exec(POLICY_SCHEMA);
|
|
3813
3843
|
db.exec(RUN_CHECK_SCHEMA);
|
|
3814
3844
|
db.exec(QUIET_CHAT_SCHEMA);
|
|
3845
|
+
// The update a batch line speaks for: its newest fact about that task (null: a finished result).
|
|
3846
|
+
addColumn(db, "chat_batch_item", "notification", "INTEGER");
|
|
3815
3847
|
db.exec(LEAD_QUIET_SCHEMA);
|
|
3816
3848
|
mergeCheckTables(db);
|
|
3817
3849
|
addColumn(db, "monitoring_status", "target", "TEXT");
|
|
@@ -10007,6 +10039,10 @@ export class Store {
|
|
|
10007
10039
|
stampWorktreeSetup(path, digest) {
|
|
10008
10040
|
this.db.prepare("UPDATE worktree SET setup_digest = ? WHERE path = ?").run(digest, path);
|
|
10009
10041
|
}
|
|
10042
|
+
/** The checkout no longer holds what its setup made (its dependencies were dropped): the next lease runs setup again. */
|
|
10043
|
+
forgetWorktreeSetup(path) {
|
|
10044
|
+
this.db.prepare("UPDATE worktree SET setup_digest = NULL WHERE path = ?").run(path);
|
|
10045
|
+
}
|
|
10010
10046
|
// ---- verify command (Priority 2) ---------------------------------------
|
|
10011
10047
|
/**
|
|
10012
10048
|
* Approve one repo's verification command, revoking any predecessor in
|
|
@@ -12721,6 +12757,22 @@ export class Store {
|
|
|
12721
12757
|
.get(taskRef, head);
|
|
12722
12758
|
return Number(row?.["n"] ?? 0);
|
|
12723
12759
|
}
|
|
12760
|
+
/** Why a finished result can't be built again on the same filing: it was
|
|
12761
|
+
* verified, attested or accepted, or its branch was published (revised
|
|
12762
|
+
* through its pull request). Null when `requeueTask` may run it again. */
|
|
12763
|
+
finalResultReason(runId) {
|
|
12764
|
+
const verdict = this.proofVerdictFor(runId);
|
|
12765
|
+
if (verdict !== null && (verdict.verdict === "verified" || verdict.verdict === "attested"))
|
|
12766
|
+
return "accepted-result";
|
|
12767
|
+
if (this.proofAcceptance(runId) !== null)
|
|
12768
|
+
return "accepted-result";
|
|
12769
|
+
// An intent that never pushed is not a publication; a pushed or
|
|
12770
|
+
// opened branch is, and is revised through its pull request.
|
|
12771
|
+
const publication = this.publicationForRun(runId);
|
|
12772
|
+
if (publication !== null && (publication.state === "pushed" || publication.state === "opened"))
|
|
12773
|
+
return "published";
|
|
12774
|
+
return null;
|
|
12775
|
+
}
|
|
12724
12776
|
requeueTask(taskId, by, now) {
|
|
12725
12777
|
return this.transact(() => {
|
|
12726
12778
|
const ref = this.db
|
|
@@ -12759,16 +12811,9 @@ export class Store {
|
|
|
12759
12811
|
}
|
|
12760
12812
|
else {
|
|
12761
12813
|
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" };
|
|
12814
|
+
const final = this.finalResultReason(rejectedRun);
|
|
12815
|
+
if (final !== null)
|
|
12816
|
+
return { ok: false, reason: final };
|
|
12772
12817
|
}
|
|
12773
12818
|
}
|
|
12774
12819
|
else if (incidents.length === 0 && state !== "failed") {
|
|
@@ -15065,6 +15110,10 @@ export class Store {
|
|
|
15065
15110
|
.run(now.toISOString(), head.length, head);
|
|
15066
15111
|
return Number(changes);
|
|
15067
15112
|
}
|
|
15113
|
+
notificationById(id) {
|
|
15114
|
+
const row = this.db.prepare("SELECT * FROM notification WHERE id = ?").get(id);
|
|
15115
|
+
return row === undefined ? null : readNotification(row);
|
|
15116
|
+
}
|
|
15068
15117
|
notificationsAfter(id, limit = 100) {
|
|
15069
15118
|
return this.db.prepare("SELECT * FROM notification WHERE id>? ORDER BY id LIMIT ?").all(id, Math.max(1, Math.min(100, limit))).map(readNotification);
|
|
15070
15119
|
}
|
|
@@ -20981,21 +21030,32 @@ export class Store {
|
|
|
20981
21030
|
/** Every owned run must have ended, and every retained local process
|
|
20982
21031
|
* witness must be absent. Read-only probes never authorize PID kills. */
|
|
20983
21032
|
stopQuiescenceProblem(runId) {
|
|
21033
|
+
return this.stopQuiescenceFact(runId)?.problem ?? null;
|
|
21034
|
+
}
|
|
21035
|
+
/** The same answer as stopQuiescenceProblem, with which run it names and
|
|
21036
|
+
* what kind of answer it is (needs-you.ts reads it): `alive` while
|
|
21037
|
+
* something of the run may still be running, `elsewhere` when only
|
|
21038
|
+
* another computer can tell, `unprovable` when nothing is known to run
|
|
21039
|
+
* but its exit was never recorded, `unknown` when this computer can't
|
|
21040
|
+
* check at all (both are what `run settle` resolves; only the first may
|
|
21041
|
+
* be called not running). */
|
|
21042
|
+
stopQuiescenceFact(runId) {
|
|
21043
|
+
const fact = (run, kind, problem) => ({ run, kind, problem });
|
|
20984
21044
|
const ids = this.ownedRunsOf(runId);
|
|
20985
21045
|
if (ids.length === 0)
|
|
20986
|
-
return `run #${runId} does not exist
|
|
21046
|
+
return fact(runId, "open", `run #${runId} does not exist`);
|
|
20987
21047
|
for (const id of ids) {
|
|
20988
21048
|
const run = this.getRun(id);
|
|
20989
21049
|
if (run.outcome === null)
|
|
20990
|
-
return `run #${id} is still open
|
|
21050
|
+
return fact(id, "open", `run #${id} is still open`);
|
|
20991
21051
|
if (ownedProcessCount(runOwnerTag(this, id)) > 0)
|
|
20992
|
-
return `run #${id} still has an owned subprocess
|
|
21052
|
+
return fact(id, "alive", `run #${id} still has an owned subprocess`);
|
|
20993
21053
|
const held = this.heldSessionOf(id);
|
|
20994
21054
|
if (held !== null && held.endedAt === null)
|
|
20995
|
-
return `run #${id}'s held supervisor has not finished shutdown
|
|
21055
|
+
return fact(id, "alive", `run #${id}'s held supervisor has not finished shutdown`);
|
|
20996
21056
|
const witnesses = this.db.prepare("SELECT * FROM run_process WHERE run = ? ORDER BY id").all(id);
|
|
20997
21057
|
if (run.providerStartedAt !== null && witnesses.length === 0) {
|
|
20998
|
-
return `run #${id} may have spawned before its process witness was recorded; exit is unproven
|
|
21058
|
+
return fact(id, "unprovable", `run #${id} may have spawned before its process witness was recorded; exit is unproven`);
|
|
20999
21059
|
}
|
|
21000
21060
|
for (const witness of witnesses) {
|
|
21001
21061
|
if (witness["exited_at"] !== null)
|
|
@@ -21009,12 +21069,12 @@ export class Store {
|
|
|
21009
21069
|
if (provenDeadByBootChange({ host, bootId }))
|
|
21010
21070
|
continue;
|
|
21011
21071
|
if (host !== hostname())
|
|
21012
|
-
return `run #${id}'s process belongs to another host; exit is unproven here
|
|
21072
|
+
return fact(id, "elsewhere", `run #${id}'s process belongs to another host; exit is unproven here`);
|
|
21013
21073
|
const backend = witness["containment"] === null || witness["containment"] === undefined ? null : String(witness["containment"]);
|
|
21014
21074
|
const container = witness["container"] === null || witness["container"] === undefined ? null : String(witness["container"]);
|
|
21015
21075
|
if (backend !== null || container !== null) {
|
|
21016
21076
|
if (backend === null || container === null)
|
|
21017
|
-
return `run #${id} has incomplete native containment custody
|
|
21077
|
+
return fact(id, "unknown", `run #${id} has incomplete native containment custody`);
|
|
21018
21078
|
if (witness["container_empty_at"] !== null)
|
|
21019
21079
|
continue;
|
|
21020
21080
|
// A native witness settles ONLY on the OS's word: the object is
|
|
@@ -21022,30 +21082,37 @@ export class Store {
|
|
|
21022
21082
|
// when empty). A transport exit never proved it.
|
|
21023
21083
|
const state = containerEmptiness(backend, container, process.platform, witness["container_identity"] == null ? null : String(witness["container_identity"]));
|
|
21024
21084
|
if (state === "populated")
|
|
21025
|
-
return `run #${id}'s ${backend} object ${container} still has members
|
|
21085
|
+
return fact(id, "alive", `run #${id}'s ${backend} object ${container} still has members`);
|
|
21026
21086
|
if (state === "unknown")
|
|
21027
|
-
return `run #${id}'s ${backend} object ${container} cannot be proven empty from here
|
|
21087
|
+
return fact(id, "unknown", `run #${id}'s ${backend} object ${container} cannot be proven empty from here`);
|
|
21028
21088
|
continue;
|
|
21029
21089
|
}
|
|
21030
21090
|
if (witness["pid"] === null)
|
|
21031
|
-
return `run #${id} has an incomplete spawn witness; exit is unproven
|
|
21091
|
+
return fact(id, "unprovable", `run #${id} has an incomplete spawn witness; exit is unproven`);
|
|
21032
21092
|
const pid = Number(witness["pid"]);
|
|
21033
21093
|
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
|
|
21094
|
+
return fact(id, "alive", `run #${id}'s process ${pid} may still be running`);
|
|
21035
21095
|
}
|
|
21036
21096
|
if (run.worktree !== null) {
|
|
21037
21097
|
try {
|
|
21038
21098
|
const occupied = worktreeProcessOccupancy(run.worktree);
|
|
21039
21099
|
if (occupied.held)
|
|
21040
|
-
return `run #${id}'s workspace is still held by process ${occupied.by}
|
|
21100
|
+
return fact(id, "alive", `run #${id}'s workspace is still held by process ${occupied.by}`);
|
|
21041
21101
|
}
|
|
21042
21102
|
catch {
|
|
21043
|
-
|
|
21103
|
+
// An unreadable workspace note is no proof either way; an approver's recorded word settles it.
|
|
21104
|
+
if (!this.workspaceConfirmedByApprover(id))
|
|
21105
|
+
return fact(id, "unknown", `run #${id}'s workspace occupancy could not be established`);
|
|
21044
21106
|
}
|
|
21045
21107
|
}
|
|
21046
21108
|
}
|
|
21047
21109
|
return null;
|
|
21048
21110
|
}
|
|
21111
|
+
/** `run settle` recorded an approver's word that a run whose workspace
|
|
21112
|
+
* note couldn't be read has stopped. */
|
|
21113
|
+
workspaceConfirmedByApprover(runId) {
|
|
21114
|
+
return this.db.prepare("SELECT 1 FROM action_ledger WHERE run_id = ? AND action = 'workspace settled by approver' LIMIT 1").get(runId) !== undefined;
|
|
21115
|
+
}
|
|
21049
21116
|
reserveRunProcess(runId, now, group = true) {
|
|
21050
21117
|
return Number(this.db.prepare("INSERT INTO run_process (run,host,process_group,observed_at,boot_id) VALUES (?,?,?,?,?)")
|
|
21051
21118
|
.run(runId, hostname(), group ? 1 : 0, now.toISOString(), currentBootId()).lastInsertRowid);
|
|
@@ -21059,6 +21126,10 @@ export class Store {
|
|
|
21059
21126
|
const rows = only === undefined
|
|
21060
21127
|
? this.db.prepare("SELECT * FROM run_process WHERE run = ? AND exited_at IS NULL").all(runId)
|
|
21061
21128
|
: 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);
|
|
21129
|
+
// A finished run's witness gets the same proof the deploy's quiescence
|
|
21130
|
+
// check uses: ESRCH, or a process now holding the pid that was born after
|
|
21131
|
+
// the run ended. An open run has no finish time, so only ESRCH counts.
|
|
21132
|
+
const finishedAt = this.getRun(runId)?.finishedAt ?? null;
|
|
21062
21133
|
let recorded = 0;
|
|
21063
21134
|
for (const row of rows) {
|
|
21064
21135
|
const host = String(row["host"]), bootId = row["boot_id"] == null ? null : String(row["boot_id"]);
|
|
@@ -21072,9 +21143,7 @@ export class Store {
|
|
|
21072
21143
|
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
21144
|
}
|
|
21074
21145
|
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);
|
|
21146
|
+
gone = row["pid"] != null && !processMayBeAlive(Number(row["pid"]), row["process_group"] === 1, finishedAt === null ? undefined : { observedAt: row["observed_at"], finishedAt });
|
|
21078
21147
|
}
|
|
21079
21148
|
}
|
|
21080
21149
|
if (gone)
|
|
@@ -21085,6 +21154,25 @@ export class Store {
|
|
|
21085
21154
|
}
|
|
21086
21155
|
return recorded;
|
|
21087
21156
|
}
|
|
21157
|
+
/** The reconcile road for a finished run whose process outlived its
|
|
21158
|
+
* finish: once each process is provably gone, its exit is recorded, so
|
|
21159
|
+
* the task stops waiting on a person by itself. A run that can't be proven
|
|
21160
|
+
* yet stays open and is passed over, never holding newer runs behind it:
|
|
21161
|
+
* every finished run is visited, a batch at a time. Never signals. */
|
|
21162
|
+
recordFinishedRunExits(now, batch = 200) {
|
|
21163
|
+
const page = this.db.prepare(`SELECT DISTINCT p.run FROM run_process p JOIN run r ON r.id = p.run
|
|
21164
|
+
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
|
|
21165
|
+
ORDER BY p.run LIMIT ?`);
|
|
21166
|
+
let recorded = 0;
|
|
21167
|
+
for (let after = 0;;) {
|
|
21168
|
+
const runs = page.all(hostname(), after, batch).map(row => Number(row["run"]));
|
|
21169
|
+
for (const run of runs)
|
|
21170
|
+
recorded += this.recordRunProcessExits(run, now);
|
|
21171
|
+
if (runs.length < batch)
|
|
21172
|
+
return recorded;
|
|
21173
|
+
after = runs[runs.length - 1];
|
|
21174
|
+
}
|
|
21175
|
+
}
|
|
21088
21176
|
finishUnspawnedProcess(witness, now) {
|
|
21089
21177
|
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
21178
|
.run(now.toISOString(), witness);
|
|
@@ -21179,12 +21267,42 @@ export class Store {
|
|
|
21179
21267
|
return alive(`run #${args.runId}'s ${String(row["containment"])} object ${String(row["container"])} still has members`);
|
|
21180
21268
|
}
|
|
21181
21269
|
}
|
|
21182
|
-
|
|
21183
|
-
|
|
21270
|
+
// A workspace still held by a live process refuses; one whose note can't
|
|
21271
|
+
// be read is settled by the approver's word, in the ledger.
|
|
21272
|
+
let workspace = false;
|
|
21273
|
+
if (run.worktree !== null) {
|
|
21274
|
+
try {
|
|
21275
|
+
const occupied = worktreeProcessOccupancy(run.worktree);
|
|
21276
|
+
if (occupied.held)
|
|
21277
|
+
return alive(`run #${args.runId}'s workspace is still held by process ${occupied.by}`);
|
|
21278
|
+
}
|
|
21279
|
+
catch {
|
|
21280
|
+
workspace = !this.workspaceConfirmedByApprover(args.runId);
|
|
21281
|
+
}
|
|
21282
|
+
}
|
|
21283
|
+
const ref = this.refById(run.taskRef);
|
|
21284
|
+
if (workspace) {
|
|
21285
|
+
this.recordAction({ at: now.toISOString(), actor: args.by, repo: ref?.repo ?? null, taskId: ref?.externalId ?? null, runId: args.runId,
|
|
21286
|
+
action: "workspace settled by approver", outcome: run.worktree ?? "", source: "request", detail: args.why });
|
|
21287
|
+
}
|
|
21288
|
+
// A run that may have spawned before any witness was written has none
|
|
21289
|
+
// to settle: the approver's word is recorded as one ended witness.
|
|
21290
|
+
if (rows.length === 0 && run.providerStartedAt !== null) {
|
|
21291
|
+
const id = Number(this.db.prepare("INSERT INTO run_process (run,host,process_group,observed_at,boot_id,exited_at) VALUES (?,?,0,?,?,?)")
|
|
21292
|
+
.run(args.runId, hostname(), now.toISOString(), currentBootId(), now.toISOString()).lastInsertRowid);
|
|
21293
|
+
this.recordAction({ at: now.toISOString(), actor: args.by, repo: ref?.repo ?? null, taskId: ref?.externalId ?? null, runId: args.runId,
|
|
21294
|
+
action: "process witness settled by approver", outcome: `witness ${id}`, source: "request", detail: args.why });
|
|
21295
|
+
this.settleQuiescentStops(now);
|
|
21296
|
+
return { ok: true, witnesses: [id], repeated: false };
|
|
21297
|
+
}
|
|
21298
|
+
if (open.length === 0) {
|
|
21299
|
+
if (workspace)
|
|
21300
|
+
this.settleQuiescentStops(now);
|
|
21301
|
+
return { ok: true, witnesses: [], repeated: !workspace };
|
|
21302
|
+
}
|
|
21184
21303
|
const ids = open.map(row => Number(row["id"]));
|
|
21185
21304
|
for (const id of ids)
|
|
21186
21305
|
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
21306
|
this.recordAction({ at: now.toISOString(), actor: args.by, repo: ref?.repo ?? null, taskId: ref?.externalId ?? null, runId: args.runId,
|
|
21189
21307
|
action: "process witness settled by approver", outcome: `witness ${ids.join(", ")}`, source: "request", detail: args.why });
|
|
21190
21308
|
this.settleQuiescentStops(now);
|
|
@@ -22101,6 +22219,7 @@ export class Store {
|
|
|
22101
22219
|
* and the evening digest keep every fact either way.
|
|
22102
22220
|
*/
|
|
22103
22221
|
pingAllowed(row, account) {
|
|
22222
|
+
// A security alert always pings, ahead of every rule below: own acts, settled, replaced or release-check tasks.
|
|
22104
22223
|
if (SECURITY_KINDS.has(row.kind) || row.kind === "evening-digest")
|
|
22105
22224
|
return true;
|
|
22106
22225
|
if (row.recipient !== null && row.kind === "task-ask")
|
|
@@ -22110,10 +22229,42 @@ export class Store {
|
|
|
22110
22229
|
return false;
|
|
22111
22230
|
if (row.recipient !== null)
|
|
22112
22231
|
return true;
|
|
22232
|
+
// A task this person (or their lead) completed or cancelled is settled for them: nothing more about it.
|
|
22233
|
+
// Release checks and replaced or superseded tasks never ping anyone; a deploy says "is live" instead.
|
|
22234
|
+
if (row.taskRef !== null && (this.settledBy(row.taskRef) === account || this.neverPings(row.taskRef)))
|
|
22235
|
+
return false;
|
|
22113
22236
|
if (this.leadQuiet(row, actor))
|
|
22114
22237
|
return false;
|
|
22115
22238
|
return row.project === null || this.db.prepare("SELECT 1 AS hit FROM project_mute WHERE account = ? AND repo = ?").get(account, row.project) === undefined;
|
|
22116
22239
|
}
|
|
22240
|
+
/** Who last completed or cancelled this task, when that is still its latest act: null otherwise. A revision's
|
|
22241
|
+
* completion is recorded on its root, so an act on a task it revises counts too, but only one made after this
|
|
22242
|
+
* revision was filed: settling older work never silences a newer revision, whose failures still reach people. */
|
|
22243
|
+
settledBy(taskRef) {
|
|
22244
|
+
const ancestors = [];
|
|
22245
|
+
for (let at = 0, from = taskRef; at < 20; at++) {
|
|
22246
|
+
const parent = this.db.prepare(`SELECT p.id FROM task_ref c JOIN task_ref p ON p.backend = c.backend AND p.external_id = c.revision_of AND p.repo IS c.repo
|
|
22247
|
+
WHERE c.id = ?`).get(from);
|
|
22248
|
+
if (parent === undefined || from === Number(parent["id"]) || ancestors.includes(Number(parent["id"])))
|
|
22249
|
+
break;
|
|
22250
|
+
from = Number(parent["id"]);
|
|
22251
|
+
ancestors.push(from);
|
|
22252
|
+
}
|
|
22253
|
+
const filed = this.db.prepare("SELECT t.created_at FROM task_ref r JOIN task t ON t.id = r.external_id AND r.backend = ? WHERE r.id = ?").get(BUILT_IN, taskRef);
|
|
22254
|
+
const since = filed === undefined || ancestors.length === 0 ? null : String(filed["created_at"]);
|
|
22255
|
+
const last = since === null
|
|
22256
|
+
? this.db.prepare("SELECT act, account FROM task_act WHERE task_ref = ? ORDER BY id DESC LIMIT 1").get(taskRef)
|
|
22257
|
+
: this.db.prepare(`SELECT act, account FROM task_act WHERE task_ref = ? OR (task_ref IN (${ancestors.map(() => "?").join(", ")}) AND at >= ?)
|
|
22258
|
+
ORDER BY id DESC LIMIT 1`).get(taskRef, ...ancestors, since);
|
|
22259
|
+
return last !== undefined && (last["act"] === "completed" || last["act"] === "cancelled") ? String(last["account"]) : null;
|
|
22260
|
+
}
|
|
22261
|
+
/** A release check, a replaced task, or one a revision superseded: its facts stay in the console. */
|
|
22262
|
+
neverPings(taskRef) {
|
|
22263
|
+
return this.db.prepare(`SELECT 1 AS hit FROM task_ref r WHERE r.id = ? AND (
|
|
22264
|
+
EXISTS (SELECT 1 FROM run JOIN run_check rc ON rc.run = run.id WHERE run.task_ref = r.id AND rc.release = 1)
|
|
22265
|
+
OR EXISTS (SELECT 1 FROM task_replacement x WHERE x.task_ref = r.id)
|
|
22266
|
+
OR EXISTS (SELECT 1 FROM task_ref c WHERE c.backend = r.backend AND c.revision_of = r.external_id AND c.repo IS r.repo))`).get(taskRef) !== undefined;
|
|
22267
|
+
}
|
|
22117
22268
|
/** The lead's work, which messages nobody: unless it is a security alert or a failure with nothing left for the lead. */
|
|
22118
22269
|
leadQuiet(row, actor = this.notificationActor(row.id)) {
|
|
22119
22270
|
if (SECURITY_KINDS.has(row.kind))
|
|
@@ -22166,6 +22317,32 @@ export class Store {
|
|
|
22166
22317
|
return this.db.prepare("SELECT * FROM notification WHERE created_at >= ? AND task_ref IS NOT NULL AND project IS NOT NULL ORDER BY id DESC LIMIT ?")
|
|
22167
22318
|
.all(since, limit).map(readNotification).reverse();
|
|
22168
22319
|
}
|
|
22320
|
+
/** The one message this update joins: the destination's batch opened within `windowMs`, or a new one. A task
|
|
22321
|
+
* has one line in it, saying its newest update (`notification`; null for a finished result). */
|
|
22322
|
+
chatBatchFor(destination, taskRef, run, now, windowMs, notification = null) {
|
|
22323
|
+
return this.transact(() => {
|
|
22324
|
+
const open = this.db.prepare("SELECT id FROM chat_batch WHERE destination = ? AND opened_at > ? AND opened_at <= ? ORDER BY id DESC LIMIT 1")
|
|
22325
|
+
.get(destination, new Date(now.getTime() - windowMs).toISOString(), now.toISOString());
|
|
22326
|
+
const batch = open !== undefined ? Number(open["id"])
|
|
22327
|
+
: Number(this.db.prepare("INSERT INTO chat_batch (destination, opened_at) VALUES (?, ?)").run(destination, now.toISOString()).lastInsertRowid);
|
|
22328
|
+
this.db.prepare(`INSERT INTO chat_batch_item (batch, task_ref, run, joined_at, notification) VALUES (?, ?, ?, ?, ?)
|
|
22329
|
+
ON CONFLICT (batch, task_ref) DO UPDATE SET run = excluded.run, notification = excluded.notification`).run(batch, taskRef, run, now.toISOString(), notification);
|
|
22330
|
+
return this.chatBatch(batch);
|
|
22331
|
+
});
|
|
22332
|
+
}
|
|
22333
|
+
chatBatch(id) {
|
|
22334
|
+
const row = this.db.prepare("SELECT * FROM chat_batch WHERE id = ?").get(id);
|
|
22335
|
+
if (row === undefined)
|
|
22336
|
+
return null;
|
|
22337
|
+
const items = this.db.prepare("SELECT task_ref, run, notification FROM chat_batch_item WHERE batch = ? ORDER BY joined_at, task_ref").all(id)
|
|
22338
|
+
.map(one => ({ taskRef: Number(one["task_ref"]), run: one["run"] == null ? null : Number(one["run"]),
|
|
22339
|
+
notification: one["notification"] == null ? null : Number(one["notification"]) }));
|
|
22340
|
+
return { id, destination: String(row["destination"]), openedAt: String(row["opened_at"]), message: row["message"] == null ? null : String(row["message"]),
|
|
22341
|
+
digest: row["digest"] == null ? null : String(row["digest"]), items };
|
|
22342
|
+
}
|
|
22343
|
+
setChatBatchMessage(id, message, digest) {
|
|
22344
|
+
this.db.prepare("UPDATE chat_batch SET message = ?, digest = ? WHERE id = ?").run(message, digest, id);
|
|
22345
|
+
}
|
|
22169
22346
|
/** The message now showing this card, and what it shows. */
|
|
22170
22347
|
setChatCardMessage(id, message, digest) {
|
|
22171
22348
|
this.db.prepare("UPDATE chat_card SET message = ?, digest = ? WHERE id = ?").run(message, digest, id);
|
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
|
}
|
package/dist/teams-chat.js
CHANGED
|
@@ -195,7 +195,7 @@ export async function deliverTeamsPart(options) {
|
|
|
195
195
|
const flow = content.flow ? chatFlowButtons(state, row.id, now).map(one => ({ type: "Action.Submit", title: one.label, data: { so: one.token }, ...(one.action === "approve" ? { style: "positive" } : {}) })) : [];
|
|
196
196
|
// A teammate's question (v93): its options, then "Answer in words".
|
|
197
197
|
const asked = content.question ? chatQuestionButtons(state, row.id, now).map(one => ({ type: "Action.Submit", title: one.label.slice(0, 80), data: { so: one.token } })) : [];
|
|
198
|
-
actions = [...flow, ...asked, ...openUrlAction(options.origin(), content.link)];
|
|
198
|
+
actions = [...flow, ...asked, ...openUrlAction(options.origin(), content.link), ...(content.also ?? []).flatMap(one => openUrlAction(options.origin(), one))];
|
|
199
199
|
}
|
|
200
200
|
const target = content.edit ?? row.message;
|
|
201
201
|
const body = actions.length || content.proposal ? teamsCard(text, actions) : { type: "message", text, textFormat: "plain" };
|