toolroll 0.9.85 → 0.9.87
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 +83 -0
- package/dist/agent-fence.d.ts +6 -2
- package/dist/agent-fence.js +3 -2
- package/dist/agents-board.d.ts +12 -0
- package/dist/agents-board.js +26 -0
- package/dist/approval-rules-ui.d.ts +4 -1
- package/dist/approval-rules-ui.js +31 -15
- package/dist/assignment-ui.js +1 -1
- package/dist/assignment.js +4 -3
- package/dist/backup-ui.d.ts +5 -4
- package/dist/backup-ui.js +65 -32
- package/dist/browser/terminal.css +1 -1
- package/dist/browser/terminal.js +57 -54
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +35 -68
- package/dist/browser-crew.js +1 -1
- package/dist/browser-workspace.d.ts +363 -177
- package/dist/browser-workspace.js +10 -9
- package/dist/chat-actions.d.ts +11 -1
- package/dist/chat-actions.js +53 -1
- package/dist/chat-channel.js +1 -0
- package/dist/chat-controls.d.ts +1 -1
- package/dist/chat-controls.js +1 -1
- package/dist/chat-quiet.js +3 -2
- package/dist/chat-settings-page.d.ts +29 -9
- package/dist/chat-settings-page.js +59 -28
- package/dist/check-levels-ui.d.ts +2 -1
- package/dist/check-levels-ui.js +67 -58
- package/dist/claim.d.ts +3 -0
- package/dist/claim.js +4 -1
- package/dist/contracts/chat-actions.d.ts +9 -0
- package/dist/contracts/chat-actions.js +3 -0
- package/dist/contracts/cli.d.ts +43 -0
- package/dist/contracts/cli.js +4 -0
- package/dist/contracts/console-api.d.ts +4 -1
- package/dist/contracts/console-api.js +4 -1
- package/dist/contracts/flow-propose.d.ts +4 -1
- package/dist/contracts/flow-propose.js +1 -1
- package/dist/contracts/flow.d.ts +41 -0
- package/dist/contracts/flow.js +4 -2
- package/dist/contracts/lead-tools.d.ts +21 -0
- package/dist/contracts/lead-tools.js +2 -0
- package/dist/contracts/store-json.d.ts +2 -0
- package/dist/contracts/trigger-payloads.d.ts +20 -0
- package/dist/contracts/trigger-payloads.js +4 -0
- package/dist/coordinator.js +1 -0
- package/dist/credentials-ui.d.ts +5 -3
- package/dist/credentials-ui.js +58 -31
- package/dist/deploy-gate.js +4 -0
- package/dist/design/kit.css +685 -0
- package/dist/design/kit.d.ts +208 -0
- package/dist/design/kit.js +177 -0
- package/dist/design/tokens.css +19 -1
- package/dist/discord-settings.js +8 -4
- package/dist/dispatch.d.ts +3 -0
- package/dist/dispatch.js +4 -1
- package/dist/export-ui.d.ts +6 -4
- package/dist/export-ui.js +18 -8
- package/dist/flow-browser.d.ts +103 -0
- package/dist/flow-browser.js +204 -0
- package/dist/flow-changed.d.ts +14 -0
- package/dist/flow-changed.js +53 -0
- package/dist/flow-code.d.ts +5 -1
- package/dist/flow-code.js +4 -1
- package/dist/flow-engine.js +16 -0
- package/dist/flow-gallery-ui.d.ts +41 -4
- package/dist/flow-gallery-ui.js +95 -63
- package/dist/flow-live.js +9 -1
- package/dist/flow-pull-request.js +15 -7
- package/dist/flow-share.js +3 -1
- package/dist/flow-starters-ui.d.ts +3 -9
- package/dist/flow-starters-ui.js +12 -23
- package/dist/flow-steps.d.ts +9 -1
- package/dist/flow-steps.js +55 -5
- package/dist/flow-triggers.js +55 -0
- package/dist/flow-unattended-words.d.ts +20 -0
- package/dist/flow-unattended-words.js +24 -0
- package/dist/flow-unattended.d.ts +9 -5
- package/dist/flow-unattended.js +45 -23
- package/dist/flows-cli.d.ts +19 -0
- package/dist/flows-cli.js +54 -5
- package/dist/flows-ui.d.ts +12 -6
- package/dist/flows-ui.js +152 -32
- package/dist/flows.js +5 -2
- package/dist/integrations-ui.d.ts +6 -4
- package/dist/integrations-ui.js +34 -40
- package/dist/integrations.d.ts +2 -2
- package/dist/integrations.js +4 -4
- package/dist/kits-ui.d.ts +8 -10
- package/dist/kits-ui.js +29 -33
- package/dist/kits.js +1 -1
- package/dist/knowledge-ui.js +76 -19
- package/dist/lead-tools.js +35 -6
- package/dist/lead-voice.js +16 -5
- package/dist/ledger-view.d.ts +2 -0
- package/dist/ledger-view.js +41 -38
- package/dist/models-ui.d.ts +8 -3
- package/dist/models-ui.js +82 -59
- package/dist/monitoring-ui.d.ts +4 -2
- package/dist/monitoring-ui.js +44 -13
- package/dist/needs-you.d.ts +1 -1
- package/dist/needs-you.js +8 -0
- package/dist/operate-remote-arguments.js +1 -0
- package/dist/operate-remote.js +1 -1
- package/dist/operate.d.ts +1 -1
- package/dist/operate.js +249 -6
- package/dist/passwordless.d.ts +1 -1
- package/dist/passwordless.js +1 -1
- package/dist/people-audit-ui.d.ts +2 -1
- package/dist/people-audit-ui.js +25 -23
- package/dist/policy-ui.d.ts +3 -2
- package/dist/policy-ui.js +27 -21
- package/dist/policy.d.ts +3 -0
- package/dist/policy.js +14 -1
- package/dist/project-delete-ui.d.ts +4 -2
- package/dist/project-delete-ui.js +135 -126
- package/dist/project-delete.js +7 -3
- package/dist/project-knowledge.d.ts +10 -10
- package/dist/publish.d.ts +11 -0
- package/dist/publish.js +16 -3
- package/dist/pull-request-flow.js +35 -4
- package/dist/pull-request-ui.d.ts +2 -1
- package/dist/pull-request-ui.js +39 -28
- package/dist/recipe-ui.d.ts +4 -2
- package/dist/recipe-ui.js +14 -4
- package/dist/release-adapters.d.ts +3 -0
- package/dist/release-adapters.js +22 -2
- package/dist/release-cli.js +81 -9
- package/dist/release.d.ts +21 -0
- package/dist/release.js +40 -4
- package/dist/releases.js +1 -1
- package/dist/request-budget-ui.d.ts +4 -2
- package/dist/request-budget-ui.js +30 -19
- package/dist/result-follow-ups.js +4 -0
- package/dist/retention-ui.d.ts +4 -3
- package/dist/retention-ui.js +15 -12
- package/dist/scope.d.ts +8 -0
- package/dist/scope.js +13 -0
- package/dist/serve.d.ts +0 -1
- package/dist/serve.js +51 -7
- package/dist/server/agents-computers.d.ts +9 -0
- package/dist/server/agents-computers.js +67 -0
- package/dist/server/chat.js +20 -18
- package/dist/server/chrome.d.ts +1 -1
- package/dist/server/chrome.js +19 -9
- package/dist/server/console.css +31 -2
- package/dist/server/flows.js +79 -36
- package/dist/server/home.d.ts +35 -0
- package/dist/server/home.js +104 -0
- package/dist/server/http.d.ts +3 -0
- package/dist/server/http.js +5 -0
- package/dist/server/kit-standalone.d.ts +22 -0
- package/dist/server/kit-standalone.js +69 -0
- package/dist/server/kit.d.ts +405 -0
- package/dist/server/kit.js +361 -0
- package/dist/server/pages.d.ts +2 -1
- package/dist/server/pages.js +213 -174
- package/dist/server/people-tokens.js +26 -85
- package/dist/server/render-history.d.ts +51 -0
- package/dist/server/render-history.js +228 -0
- package/dist/server/render-new-task.d.ts +29 -0
- package/dist/server/render-new-task.js +96 -0
- package/dist/server/render-next.d.ts +63 -0
- package/dist/server/render-next.js +109 -0
- package/dist/server/render-pages.d.ts +35 -190
- package/dist/server/render-pages.js +154 -563
- package/dist/server/render-people.d.ts +24 -3
- package/dist/server/render-people.js +93 -72
- package/dist/server/render-projects.d.ts +103 -0
- package/dist/server/render-projects.js +247 -0
- package/dist/server/render-results.d.ts +5 -24
- package/dist/server/render-results.js +48 -185
- package/dist/server/render-settings.d.ts +5 -74
- package/dist/server/render-settings.js +102 -493
- package/dist/server/render-tasks.d.ts +43 -5
- package/dist/server/render-tasks.js +131 -91
- package/dist/server/route-table.js +31 -5
- package/dist/server/settings.js +276 -255
- package/dist/server/task-views.d.ts +8 -0
- package/dist/server/task-views.js +90 -2
- package/dist/server/tasks.d.ts +18 -0
- package/dist/server/tasks.js +148 -41
- package/dist/server/terminals.js +1 -1
- package/dist/server/welcome.d.ts +22 -0
- package/dist/server/welcome.js +69 -0
- package/dist/settings-groups-ui.d.ts +279 -0
- package/dist/settings-groups-ui.js +615 -0
- package/dist/settings-kit.d.ts +42 -6
- package/dist/settings-kit.js +69 -7
- package/dist/settings-proofs.js +1 -0
- package/dist/setup-guide.d.ts +2 -2
- package/dist/skills-ui.js +59 -7
- package/dist/slack-settings.js +12 -5
- package/dist/spend-ui.d.ts +45 -26
- package/dist/spend-ui.js +124 -56
- package/dist/sso-ui.d.ts +5 -2
- package/dist/sso-ui.js +63 -31
- package/dist/storage-ui.d.ts +3 -2
- package/dist/storage-ui.js +56 -50
- package/dist/store.d.ts +182 -5
- package/dist/store.js +512 -38
- package/dist/surface.js +23 -3
- package/dist/task-archive.d.ts +10 -0
- package/dist/task-archive.js +16 -0
- package/dist/task-control.d.ts +1 -1
- package/dist/teams-settings.d.ts +2 -1
- package/dist/teams-settings.js +24 -41
- package/dist/telegram-settings.d.ts +0 -4
- package/dist/telegram-settings.js +52 -25
- package/dist/tidy.d.ts +130 -0
- package/dist/tidy.js +503 -0
- package/dist/toolroll-update-ui.d.ts +8 -4
- package/dist/toolroll-update-ui.js +55 -45
- package/dist/tools-ui.d.ts +5 -3
- package/dist/tools-ui.js +107 -42
- package/dist/work-index.d.ts +24 -0
- package/dist/work-index.js +56 -30
- package/dist/workspace-ui.d.ts +1 -1
- package/dist/workspace-ui.js +8 -4
- package/package.json +1 -1
package/dist/store.js
CHANGED
|
@@ -9,6 +9,8 @@ import { isAcpProfile, isAcpProvider } from "./acp-agents.js";
|
|
|
9
9
|
import { ACP_CONFORMANCE_VERSION, launchFromJson } from "./acp-attest.js";
|
|
10
10
|
import { reclaimDatabase } from "./database-reclaim.js";
|
|
11
11
|
import { BASELINE_SCHEMA, BASELINE_SCHEMA_VERSION } from "./store-baseline.js";
|
|
12
|
+
import { refArchived, taskArchived } from "./task-archive.js";
|
|
13
|
+
export { refArchived, taskArchived } from "./task-archive.js";
|
|
12
14
|
import { CHAT_PROVIDERS, CHAT_SCHEMA } from "./contracts/chat-tables.js";
|
|
13
15
|
import {} from "./chat-core.js";
|
|
14
16
|
import { ChatMessages, TELEGRAM_HOLD_REASONS, TELEGRAM_SKIPPED_ELSEWHERE, TELEGRAM_SKIPPED_OTHER_CHAT, TELEGRAM_SKIPPED_QUIET, TELEGRAM_UNSETTLED } from "./chat-messages.js";
|
|
@@ -94,6 +96,8 @@ import { LEDGER_COLUMNS, LEDGER_SCHEMA, LEDGER_TABLE, LEDGER_V99_TABLE, installL
|
|
|
94
96
|
import { PLAN_AUTO_SCHEMA } from "./plan-auto.js";
|
|
95
97
|
import { CONSOLE_INSTANCE_SCHEMA } from "./console-instance.js";
|
|
96
98
|
import { endUnattendedGrants, FLOW_UNATTENDED_SCHEMA, grantStandsSql } from "./flow-unattended.js";
|
|
99
|
+
import { flowChanged } from "./flow-changed.js";
|
|
100
|
+
import { FLOW_BROWSER_SCHEMA } from "./flow-browser.js";
|
|
97
101
|
import { RECIPE_SCHEMA } from "./recipes.js";
|
|
98
102
|
import { POLICY_SCHEMA, agentRefusal, approvalRefusal, policyParts, readPolicy, underCeiling, sessionCeilingRefusal } from "./policy.js";
|
|
99
103
|
import { PROVIDER_AUTH_SCHEMA } from "./provider-auth.js";
|
|
@@ -1092,7 +1096,15 @@ const REGISTERED_AT_FILING = "again, as it was filed";
|
|
|
1092
1096
|
// project's intake choice and last answer, and each request taken, with the task it became.
|
|
1093
1097
|
// v143 adds, and changes no saved row: optional exact-screenshot context on result feedback
|
|
1094
1098
|
// (diff_comment.image_json, V143_ADDED_COLUMNS; saved notes read NULL and keep their file/line target).
|
|
1095
|
-
|
|
1099
|
+
// v144 adds, and changes no saved row: a person's approval of browser use in a flow's script zones (flow_browser_grant;
|
|
1100
|
+
// flow-browser.ts), the one sandbox rule a project check gets for Chromium; saved flows have none and get nothing.
|
|
1101
|
+
// v145 adds, and changes no saved row (docs/plans/task-tidy.md): put-away tasks (task_archive, one row per archived task
|
|
1102
|
+
// version; no row is every saved task: not archived), the completed workflow runs on a watched branch the checks
|
|
1103
|
+
// trigger saw (branch_check_run), and the reservations that hold work back from being put away (merge_in_flight,
|
|
1104
|
+
// deploy_in_flight, flow_step_task; V145_SCHEMA, new tables only).
|
|
1105
|
+
// v146 adds, and changes no saved row: each pull request check's own timing as GitHub last reported it for the head the
|
|
1106
|
+
// plane observed (ci_check), and each release step's start, end and outcome (release_step; V146_SCHEMA, new tables only).
|
|
1107
|
+
export const SCHEMA_VERSION = 146;
|
|
1096
1108
|
/** v118: the tables merged into `credential`, in the order they move (a person's API tokens keep their ids first). */
|
|
1097
1109
|
export const V118_MERGED_TABLES = Object.freeze({ api_token: "person", coordinator_credential: "coordinator", lead_credential: "lead" });
|
|
1098
1110
|
/** The foreign keys that named a merged table, exactly as their DDL wrote them; each names `credential(id)` instead. */
|
|
@@ -1225,7 +1237,7 @@ const V115_DROPPED_REFERENCES = /\s+REFERENCES (?:contestant|contest|tournament_
|
|
|
1225
1237
|
* unchanged), and adds the nullable column in V125_ADDED_COLUMNS, checked the same way. v126 only adds a new table
|
|
1226
1238
|
* (task_source) and the columns in V126_ADDED_COLUMNS (null or 0), checked the same way.
|
|
1227
1239
|
*/
|
|
1228
|
-
export const UPDATE_SAFE_MIGRATIONS = Object.freeze([108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143]);
|
|
1240
|
+
export const UPDATE_SAFE_MIGRATIONS = Object.freeze([108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146]);
|
|
1229
1241
|
/** v119: the columns it adds, by table, with the value every row saved before it reads (toolroll-update's HISTORY_RULES). */
|
|
1230
1242
|
export const V119_ADDED_COLUMNS = Object.freeze({
|
|
1231
1243
|
task_ref: { planning_generation: { definition: "INTEGER NOT NULL DEFAULT 0", saved: "0" } },
|
|
@@ -1365,6 +1377,92 @@ export const V140_ADDED_COLUMNS = Object.freeze({
|
|
|
1365
1377
|
export const V143_ADDED_COLUMNS = Object.freeze({
|
|
1366
1378
|
diff_comment: { image_json: { definition: "TEXT", saved: "NULL" } },
|
|
1367
1379
|
});
|
|
1380
|
+
/** v145 (docs/plans/task-tidy.md): `task_archive` is a put-away task version. A row hides it from every list and count
|
|
1381
|
+
* and refuses every door that would dispatch, claim or approve it; restore deletes the row. Its history, results,
|
|
1382
|
+
* approvals and ledger stay exactly as they were. `merge_in_flight` is a direct merge between its last check and GitHub's
|
|
1383
|
+
* answer, owned by its token: a task can't be put away under it (a row older than 15 minutes is a crash, and no
|
|
1384
|
+
* longer counts). `deploy_in_flight` is a deployment whose candidate passed its gate (30 minutes, refreshed by every
|
|
1385
|
+
* phase that proves it again). `flow_step_task` is the tasks a flow step was admitted for, kept whatever the card does
|
|
1386
|
+
* next, so a step still running holds them. `branch_check_run` keeps each completed GitHub workflow run (pushed to
|
|
1387
|
+
* the watched branch of the watched repository) the "checks failed" trigger saw, green or red, so tidy can tell a
|
|
1388
|
+
* failure notice was fixed by a later commit. */
|
|
1389
|
+
export const V145_SCHEMA = `
|
|
1390
|
+
CREATE TABLE IF NOT EXISTS task_archive (
|
|
1391
|
+
task_ref INTEGER PRIMARY KEY REFERENCES task_ref(id),
|
|
1392
|
+
by TEXT NOT NULL,
|
|
1393
|
+
by_kind TEXT NOT NULL CHECK (by_kind IN ('person','tidy')),
|
|
1394
|
+
rule TEXT,
|
|
1395
|
+
reason TEXT NOT NULL,
|
|
1396
|
+
at TEXT NOT NULL
|
|
1397
|
+
);
|
|
1398
|
+
CREATE TABLE IF NOT EXISTS branch_check_run (
|
|
1399
|
+
repo TEXT NOT NULL,
|
|
1400
|
+
run_id INTEGER NOT NULL,
|
|
1401
|
+
branch TEXT NOT NULL,
|
|
1402
|
+
workflow TEXT NOT NULL,
|
|
1403
|
+
head_sha TEXT NOT NULL,
|
|
1404
|
+
conclusion TEXT NOT NULL CHECK (conclusion IN ('success','failure')),
|
|
1405
|
+
run_created_at TEXT NOT NULL,
|
|
1406
|
+
observed_at TEXT NOT NULL,
|
|
1407
|
+
PRIMARY KEY (repo, run_id)
|
|
1408
|
+
);
|
|
1409
|
+
CREATE INDEX IF NOT EXISTS branch_check_run_by_workflow ON branch_check_run (repo, branch, workflow, run_created_at);
|
|
1410
|
+
CREATE TABLE IF NOT EXISTS merge_in_flight (
|
|
1411
|
+
publication INTEGER PRIMARY KEY REFERENCES publication(id),
|
|
1412
|
+
task_ref INTEGER NOT NULL REFERENCES task_ref(id),
|
|
1413
|
+
token TEXT NOT NULL,
|
|
1414
|
+
by TEXT NOT NULL,
|
|
1415
|
+
at TEXT NOT NULL
|
|
1416
|
+
);
|
|
1417
|
+
CREATE TABLE IF NOT EXISTS deploy_in_flight (
|
|
1418
|
+
task_ref INTEGER PRIMARY KEY REFERENCES task_ref(id),
|
|
1419
|
+
run INTEGER NOT NULL,
|
|
1420
|
+
at TEXT NOT NULL
|
|
1421
|
+
);
|
|
1422
|
+
CREATE TABLE IF NOT EXISTS flow_step_task (
|
|
1423
|
+
card INTEGER NOT NULL,
|
|
1424
|
+
entry INTEGER NOT NULL,
|
|
1425
|
+
task_id TEXT NOT NULL,
|
|
1426
|
+
PRIMARY KEY (card, entry, task_id)
|
|
1427
|
+
);
|
|
1428
|
+
`;
|
|
1429
|
+
/** v146: `ci_check` is one pull request check (a GitHub check run or commit status) as the plane's winning observation of
|
|
1430
|
+
* that head read it: written only with the observation that won its generation, replacing that head's rows wholesale.
|
|
1431
|
+
* `release_step` is one step of a release (release.ts RELEASE_STEPS) as `toolroll release` moved through it: when it
|
|
1432
|
+
* started, when it ended and how; recording it never changes the release. Both are read-only history for the Ship and
|
|
1433
|
+
* Release views and their CLI twins. */
|
|
1434
|
+
export const V146_SCHEMA = `
|
|
1435
|
+
CREATE TABLE IF NOT EXISTS ci_check (
|
|
1436
|
+
github_repo TEXT NOT NULL,
|
|
1437
|
+
pr_number INTEGER NOT NULL,
|
|
1438
|
+
head_sha TEXT NOT NULL,
|
|
1439
|
+
-- A check's identity: its kind (check run or commit status), its workflow and its name, so two workflows' checks
|
|
1440
|
+
-- with the same name stay two rows (a failing one never hides behind a passing namesake).
|
|
1441
|
+
ident TEXT NOT NULL,
|
|
1442
|
+
name TEXT NOT NULL,
|
|
1443
|
+
workflow TEXT,
|
|
1444
|
+
state TEXT NOT NULL CHECK (state IN ('queued','running','passed','failed','skipped','cancelled','neutral')),
|
|
1445
|
+
started_at TEXT,
|
|
1446
|
+
completed_at TEXT,
|
|
1447
|
+
url TEXT,
|
|
1448
|
+
observed_at TEXT NOT NULL,
|
|
1449
|
+
PRIMARY KEY (github_repo, pr_number, head_sha, ident)
|
|
1450
|
+
);
|
|
1451
|
+
CREATE TABLE IF NOT EXISTS release_step (
|
|
1452
|
+
task_id TEXT NOT NULL,
|
|
1453
|
+
tag TEXT NOT NULL,
|
|
1454
|
+
version TEXT NOT NULL,
|
|
1455
|
+
pr INTEGER,
|
|
1456
|
+
step TEXT NOT NULL,
|
|
1457
|
+
state TEXT NOT NULL CHECK (state IN ('running','done','failed','waiting')),
|
|
1458
|
+
started_at TEXT NOT NULL,
|
|
1459
|
+
ended_at TEXT,
|
|
1460
|
+
detail TEXT,
|
|
1461
|
+
updated_at TEXT NOT NULL,
|
|
1462
|
+
PRIMARY KEY (task_id, tag, step)
|
|
1463
|
+
);
|
|
1464
|
+
CREATE INDEX IF NOT EXISTS release_step_by_task ON release_step (task_id, updated_at);
|
|
1465
|
+
`;
|
|
1368
1466
|
/**
|
|
1369
1467
|
* v135: one approval per release (docs/plans/release-once.md). `release_authority` is the single source of truth for a
|
|
1370
1468
|
* release a person approved: one row per generation, changed only by compare-and-swap on `rev`, with a lease whose
|
|
@@ -4622,6 +4720,12 @@ CREATE INDEX IF NOT EXISTS run_checkpoint_by_task ON run_checkpoint (task_ref, i
|
|
|
4622
4720
|
for (const [column, { definition }] of Object.entries(columns))
|
|
4623
4721
|
addColumn(db, table, column, definition);
|
|
4624
4722
|
}
|
|
4723
|
+
// v144: approvals of browser use in flows' script zones (a new table only).
|
|
4724
|
+
db.exec(FLOW_BROWSER_SCHEMA);
|
|
4725
|
+
// v145: put-away tasks, green checks per branch and the reservations that hold work back (new tables only).
|
|
4726
|
+
db.exec(V145_SCHEMA);
|
|
4727
|
+
// v146: pull request checks' own timing and release steps (new tables only).
|
|
4728
|
+
db.exec(V146_SCHEMA);
|
|
4625
4729
|
// Keep the epoch until reclamation succeeds, including a retry after compaction already committed. A new file has
|
|
4626
4730
|
// nothing to reclaim.
|
|
4627
4731
|
if (!fresh)
|
|
@@ -6361,6 +6465,7 @@ export class Store {
|
|
|
6361
6465
|
FROM task JOIN task_ref ON task_ref.backend = ? AND task_ref.external_id = task.id
|
|
6362
6466
|
WHERE task.state = 'queued'
|
|
6363
6467
|
AND (? IS NULL OR task_ref.repo IS NULL OR task_ref.repo = ?)
|
|
6468
|
+
AND NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref = task_ref.id)
|
|
6364
6469
|
ORDER BY task.priority DESC, task.created_at, task_ref.id
|
|
6365
6470
|
LIMIT 200`)
|
|
6366
6471
|
.all(stamp, BUILT_IN, repo, repo)
|
|
@@ -6385,7 +6490,8 @@ export class Store {
|
|
|
6385
6490
|
.prepare(`SELECT task.priority AS priority, task.created_at AS created_at, task_ref.id AS ref,
|
|
6386
6491
|
task_ref.repo AS repo, task_ref.assigned_runner AS assigned
|
|
6387
6492
|
FROM task JOIN task_ref ON task_ref.backend = ? AND task_ref.external_id = task.id
|
|
6388
|
-
WHERE task.id = ? AND task.state = 'queued'
|
|
6493
|
+
WHERE task.id = ? AND task.state = 'queued'
|
|
6494
|
+
AND NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref = task_ref.id)`)
|
|
6389
6495
|
.get(BUILT_IN, taskId);
|
|
6390
6496
|
if (me === undefined)
|
|
6391
6497
|
return null;
|
|
@@ -6397,7 +6503,8 @@ export class Store {
|
|
|
6397
6503
|
OR (task.priority = ? AND task.created_at = ? AND task_ref.id < ?)
|
|
6398
6504
|
THEN 1 ELSE 0 END) AS ahead
|
|
6399
6505
|
FROM task JOIN task_ref ON task_ref.backend = ? AND task_ref.external_id = task.id
|
|
6400
|
-
WHERE task.state = 'queued' AND (task_ref.assigned_runner IS ?) AND (task_ref.repo IS ?)
|
|
6506
|
+
WHERE task.state = 'queued' AND (task_ref.assigned_runner IS ?) AND (task_ref.repo IS ?)
|
|
6507
|
+
AND NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref = task_ref.id)`)
|
|
6401
6508
|
.get(Number(me["priority"]), Number(me["priority"]), String(me["created_at"]), Number(me["priority"]), String(me["created_at"]), Number(me["ref"]), BUILT_IN, me["assigned"], me["repo"]);
|
|
6402
6509
|
return {
|
|
6403
6510
|
position: Number(row?.["ahead"] ?? 0) + 1,
|
|
@@ -6547,6 +6654,8 @@ export class Store {
|
|
|
6547
6654
|
JOIN task_ref ON task_ref.backend = ? AND task_ref.external_id = task.id
|
|
6548
6655
|
WHERE task.state = 'queued'
|
|
6549
6656
|
${reservation}
|
|
6657
|
+
-- A put-away task (v145) never dispatches.
|
|
6658
|
+
AND NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref = task_ref.id)
|
|
6550
6659
|
AND NOT EXISTS (
|
|
6551
6660
|
SELECT 1 FROM hold
|
|
6552
6661
|
WHERE hold.task_ref = task_ref.id
|
|
@@ -7328,6 +7437,9 @@ export class Store {
|
|
|
7328
7437
|
// road it came by.
|
|
7329
7438
|
if (basis === undefined && machineActing())
|
|
7330
7439
|
return false;
|
|
7440
|
+
// A PUT-AWAY TASK (v145, task-tidy.md) is never approved, by any road: restore it first.
|
|
7441
|
+
if (taskArchived(this, taskId))
|
|
7442
|
+
return false;
|
|
7331
7443
|
// A RELEASE WAITING FOR CI (Codex review round 1, finding 4): filed while its pull request's checks still ran,
|
|
7332
7444
|
// only the automatic road (which proves CI itself) seals it until the plane read exact-head CI passing.
|
|
7333
7445
|
if (basis?.kind !== "auto-check" && basis?.kind !== "auto-green" && releaseCiHoldOf(this, taskId) !== null)
|
|
@@ -10587,7 +10699,7 @@ export class Store {
|
|
|
10587
10699
|
versions.sort((a, b) => a.id === root.id ? -1 : b.id === root.id ? 1 : a.createdAt.localeCompare(b.createdAt) || a.refId - b.refId);
|
|
10588
10700
|
const current = versions.at(-1);
|
|
10589
10701
|
return { root, current, versions,
|
|
10590
|
-
otherActive: versions.filter(one => one.id !== current.id && (one.state === "queued" || one.state === "running")),
|
|
10702
|
+
otherActive: versions.filter(one => one.id !== current.id && (one.state === "queued" || one.state === "running") && !refArchived(this, one.refId)),
|
|
10591
10703
|
problem: Number(rows[0]["broken"]) === 0 ? null : "Task history is unavailable or incomplete. This task is shown separately." };
|
|
10592
10704
|
}
|
|
10593
10705
|
/** Group and filter BEFORE the family limit; hydrate only that page's
|
|
@@ -10597,6 +10709,8 @@ export class Store {
|
|
|
10597
10709
|
const order = options.order === "updated" ? "updated_at" : "created_at";
|
|
10598
10710
|
const rows = this.db.prepare(`${projection.sql}, selected AS MATERIALIZED (
|
|
10599
10711
|
SELECT * FROM ordered WHERE version_rank = 1
|
|
10712
|
+
-- A put-away family (v145) is out of every list, before the page is cut.
|
|
10713
|
+
AND NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref = ordered.ref_id)
|
|
10600
10714
|
${options.states === undefined ? "" : `AND state IN (${options.states.map(() => "?").join(",")})`}
|
|
10601
10715
|
ORDER BY ${order} DESC, ref_id DESC LIMIT ?
|
|
10602
10716
|
) SELECT task.*, members.repo, members.ref_id, members.root_ref, members.broken
|
|
@@ -10687,7 +10801,8 @@ export class Store {
|
|
|
10687
10801
|
COUNT(CASE WHEN live_families.root_ref IS NOT NULL THEN 1 END) AS running,
|
|
10688
10802
|
COUNT(CASE WHEN live_families.root_ref IS NULL AND ordered.state = 'queued' THEN 1 END) AS queued,
|
|
10689
10803
|
COUNT(CASE WHEN live_families.root_ref IS NULL AND ordered.state = 'done' AND ordered.updated_at >= ? THEN 1 END) AS doneRecently
|
|
10690
|
-
FROM ordered LEFT JOIN live_families ON live_families.root_ref = ordered.root_ref WHERE version_rank = 1
|
|
10804
|
+
FROM ordered LEFT JOIN live_families ON live_families.root_ref = ordered.root_ref WHERE version_rank = 1
|
|
10805
|
+
AND NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref = ordered.ref_id)`)
|
|
10691
10806
|
.get(...projection.params, JSON.stringify(liveTasks), new Date(now.getTime() - 24 * 60 * 60_000).toISOString());
|
|
10692
10807
|
return { running: Number(row["running"]), queued: Number(row["queued"]), doneRecently: Number(row["doneRecently"]) };
|
|
10693
10808
|
}
|
|
@@ -11121,6 +11236,9 @@ export class Store {
|
|
|
11121
11236
|
if (ref === undefined)
|
|
11122
11237
|
return { ok: false, reason: "unknown-task" };
|
|
11123
11238
|
const taskRef = Number(ref["id"]);
|
|
11239
|
+
// A put-away task (v145) is restored before it runs again.
|
|
11240
|
+
if (refArchived(this, taskRef))
|
|
11241
|
+
return { ok: false, reason: "archived" };
|
|
11124
11242
|
const stamp = now.toISOString();
|
|
11125
11243
|
if (this.hasLiveClaim(taskRef, now))
|
|
11126
11244
|
return { ok: false, reason: "claimed" };
|
|
@@ -12732,10 +12850,12 @@ export class Store {
|
|
|
12732
12850
|
: source !== undefined && "project" in source && source.project !== "" ? "project"
|
|
12733
12851
|
: source !== undefined && "installation" in source ? "installation" : "unknown";
|
|
12734
12852
|
const project = scope === "task" ? String(ref["repo"]) : scope === "project" && source !== undefined && "project" in source ? source.project : null;
|
|
12853
|
+
// A put-away task (v145) nudges nobody: no notification about it is enqueued, whoever produced it. The check is
|
|
12854
|
+
// part of the insert itself, so a put away that commits first always wins.
|
|
12735
12855
|
const { changes, lastInsertRowid } = this.db
|
|
12736
12856
|
.prepare(`INSERT OR IGNORE INTO notification (dedupe_key, kind, subject, body, created_at, push_class, link, provenance_scope, project, task_ref, task_id, source_run, recipient)
|
|
12737
|
-
|
|
12738
|
-
.run(notification.dedupeKey, notification.kind, notification.subject, notification.body, now.toISOString(), notification.pushClass ?? null, notification.link ?? null, scope, project, ref === undefined ? null : taskRef, ref === undefined ? null : String(ref["external_id"]), run?.id ?? null, notification.recipient ?? null);
|
|
12857
|
+
SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? WHERE NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref = ?)`)
|
|
12858
|
+
.run(notification.dedupeKey, notification.kind, notification.subject, notification.body, now.toISOString(), notification.pushClass ?? null, notification.link ?? null, scope, project, ref === undefined ? null : taskRef, ref === undefined ? null : String(ref["external_id"]), run?.id ?? null, notification.recipient ?? null, ref === undefined ? null : Number(ref["id"]));
|
|
12739
12859
|
// Whose act made this fact: pings follow it (see pingAllowed).
|
|
12740
12860
|
const actor = currentActor();
|
|
12741
12861
|
if (Number(changes) > 0 && actor !== null) {
|
|
@@ -14227,6 +14347,7 @@ export class Store {
|
|
|
14227
14347
|
JOIN task_ref ON task_ref.backend = ? AND task_ref.external_id = task.id
|
|
14228
14348
|
WHERE (? IS NULL OR task_ref.repo IS NULL OR task_ref.repo = ?)
|
|
14229
14349
|
AND (? IS NULL OR task.state = ?)
|
|
14350
|
+
AND NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref = task_ref.id)
|
|
14230
14351
|
ORDER BY task.created_at DESC, task.id DESC LIMIT ?
|
|
14231
14352
|
)
|
|
14232
14353
|
UNION
|
|
@@ -14395,8 +14516,10 @@ export class Store {
|
|
|
14395
14516
|
resolvePlanRevision(id, outcome, resolvedBy, now) {
|
|
14396
14517
|
const { changes } = this.db
|
|
14397
14518
|
.prepare(`UPDATE plan_revision SET status = ?, resolved_at = ?, resolved_by = ?
|
|
14398
|
-
WHERE id = ? AND status = 'blocked'
|
|
14399
|
-
|
|
14519
|
+
WHERE id = ? AND status = 'blocked'
|
|
14520
|
+
-- A put-away task's (v145) plan change is never applied; rejecting it still is.
|
|
14521
|
+
AND (? = 'rejected' OR NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref = plan_revision.task_ref))`)
|
|
14522
|
+
.run(outcome, now.toISOString(), resolvedBy, id, outcome);
|
|
14400
14523
|
return Number(changes) > 0;
|
|
14401
14524
|
}
|
|
14402
14525
|
/** Stamp the exact plan revision and authority snapshot a run's brief was
|
|
@@ -15626,6 +15749,12 @@ export class Store {
|
|
|
15626
15749
|
if (row === undefined || String(row["state"]) !== "claimed" || Number(row["generation"]) !== generation) {
|
|
15627
15750
|
return { ok: false, outcome: "raced", why: "the claim moved" };
|
|
15628
15751
|
}
|
|
15752
|
+
// A put-away task (v145) merges nothing: the intent waits for a person, who restores it first.
|
|
15753
|
+
const archivedTask = this.db.prepare("SELECT 1 FROM publication p JOIN task_archive a ON a.task_ref = p.task_ref WHERE p.id = ?").get(Number(row["publication"]));
|
|
15754
|
+
if (archivedTask !== undefined) {
|
|
15755
|
+
this.settleMergeIntent(id, generation, "waiting-human", now, { error: "the task is put away", from: "claimed", bumpGeneration: true });
|
|
15756
|
+
return { ok: false, outcome: "waiting-human", why: "the task is put away" };
|
|
15757
|
+
}
|
|
15629
15758
|
const supersede = (why) => {
|
|
15630
15759
|
this.settleMergeIntent(id, generation, "superseded", now, { error: why, from: "claimed" });
|
|
15631
15760
|
return { ok: false, outcome: "superseded", why };
|
|
@@ -16040,9 +16169,12 @@ export class Store {
|
|
|
16040
16169
|
}
|
|
16041
16170
|
/** Save a new drawing of the flow only over the revision the editor saw (two editors never overwrite each other silently). */
|
|
16042
16171
|
saveFlow(id, change, now) {
|
|
16043
|
-
|
|
16044
|
-
|
|
16045
|
-
|
|
16172
|
+
// The one road for a changed flow (flow-changed.ts): what a person approved for the flow as it was ends with it.
|
|
16173
|
+
return flowChanged(this, { flow: id }, change.by, now, () => {
|
|
16174
|
+
const { changes } = this.db.prepare("UPDATE flow SET name = ?, definition_json = ?, revision = revision + 1, updated_by = ?, updated_at = ? WHERE id = ? AND revision = ? AND state = 'active'")
|
|
16175
|
+
.run(change.name, flowDefinitionForStore(change.definitionJson), change.by, now.toISOString(), id, change.sawRevision);
|
|
16176
|
+
return Number(changes) === 1;
|
|
16177
|
+
});
|
|
16046
16178
|
}
|
|
16047
16179
|
archiveFlow(id, by, now) {
|
|
16048
16180
|
const { changes } = this.db.prepare("UPDATE flow SET state = 'archived', updated_by = ?, updated_at = ? WHERE id = ? AND state = 'active'").run(by, now.toISOString(), id);
|
|
@@ -16465,26 +16597,35 @@ export class Store {
|
|
|
16465
16597
|
return this.transact(() => {
|
|
16466
16598
|
const current = this.flowScript(script.repo, script.name);
|
|
16467
16599
|
const version = (current?.version ?? Number(this.db.prepare("SELECT COALESCE(MAX(version), 0) AS v FROM flow_script WHERE repo = ? AND name = ?").get(script.repo, script.name)?.["v"] ?? 0)) + 1;
|
|
16468
|
-
|
|
16469
|
-
|
|
16470
|
-
|
|
16471
|
-
|
|
16472
|
-
|
|
16473
|
-
|
|
16474
|
-
|
|
16600
|
+
// A new version ends what was approved for the flows that run this script (flow-changed.ts): "runs on its own"
|
|
16601
|
+
// grants that signed its code, and browser use that sealed it.
|
|
16602
|
+
return flowChanged(this, { script: { repo: script.repo, name: script.name } }, script.by, now, () => {
|
|
16603
|
+
if (current !== null)
|
|
16604
|
+
this.db.prepare("UPDATE flow_script SET state = 'removed' WHERE id = ?").run(current.id);
|
|
16605
|
+
this.db.prepare(`INSERT INTO flow_script (repo, name, about, body, timeout_minutes, version, digest, saved_by, saved_at, state, language, file, held)
|
|
16606
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', ?, ?, ?)`).run(script.repo, script.name, script.about, script.body, script.timeoutMinutes, version, script.digest, script.by, now.toISOString(), script.language ?? "shell", script.file ?? null, script.held ?? null);
|
|
16607
|
+
return version;
|
|
16608
|
+
}, () => true);
|
|
16475
16609
|
});
|
|
16476
16610
|
}
|
|
16477
|
-
/** A person approves a held script (one an imported flow brought): from now on it runs.
|
|
16478
|
-
|
|
16479
|
-
|
|
16480
|
-
|
|
16611
|
+
/** A person approves a held script (one an imported flow brought): from now on it runs. The approval covers exactly
|
|
16612
|
+
* the version and content the person read (`seen`): the check and the release of the hold are one statement in one
|
|
16613
|
+
* transaction, so a script removed and brought back under the same name is never approved unseen. */
|
|
16614
|
+
approveFlowScript(repo, name, seen) {
|
|
16615
|
+
return this.transact(() => {
|
|
16616
|
+
const current = this.flowScript(repo, name);
|
|
16617
|
+
if (current === null || current.held === null)
|
|
16618
|
+
return "not-held";
|
|
16619
|
+
if (current.version !== seen.version || current.digest !== seen.digest)
|
|
16620
|
+
return "changed";
|
|
16621
|
+
const { changes } = this.db.prepare("UPDATE flow_script SET held = NULL WHERE id = ? AND version = ? AND digest = ? AND state = 'active' AND held IS NOT NULL").run(current.id, seen.version, seen.digest);
|
|
16622
|
+
return Number(changes) === 1 ? "approved" : "changed";
|
|
16623
|
+
});
|
|
16481
16624
|
}
|
|
16482
16625
|
removeFlowScript(repo, name, by = "flow", now = new Date()) {
|
|
16483
|
-
|
|
16626
|
+
// A grant that signed this script's code covers nothing without it: it ends, as does browser use that sealed it (flow-changed.ts).
|
|
16627
|
+
return flowChanged(this, { script: { repo, name } }, by, now, () => {
|
|
16484
16628
|
const { changes } = this.db.prepare("UPDATE flow_script SET state = 'removed' WHERE repo = ? AND name = ? AND state = 'active'").run(repo, name);
|
|
16485
|
-
// A grant that signed this script's code covers nothing without it: it ends (flow-unattended.ts).
|
|
16486
|
-
if (Number(changes) === 1)
|
|
16487
|
-
endUnattendedGrants(this, { script: { repo, name } }, by, "script changed", now);
|
|
16488
16629
|
return Number(changes) === 1;
|
|
16489
16630
|
});
|
|
16490
16631
|
}
|
|
@@ -17384,9 +17525,10 @@ export class Store {
|
|
|
17384
17525
|
const page = Math.max(1, Math.min(Math.floor(limit), 50));
|
|
17385
17526
|
// Roll-up admission binds BEFORE the LIMIT (Codex roll-up review,
|
|
17386
17527
|
// finding 2): a repo outside the ceiling must not consume the page.
|
|
17387
|
-
|
|
17528
|
+
// A put-away task (v145, task-tidy.md) is left out here, as from every list and count.
|
|
17529
|
+
const admission = (admitted === null
|
|
17388
17530
|
? "(? IS NULL OR task_ref.repo IS NULL OR task_ref.repo = ?)"
|
|
17389
|
-
: `(task_ref.repo IS NULL${admitted.length === 0 ? "" : ` OR task_ref.repo IN (${admitted.map(() => "?").join(",")})`})
|
|
17531
|
+
: `(task_ref.repo IS NULL${admitted.length === 0 ? "" : ` OR task_ref.repo IN (${admitted.map(() => "?").join(",")})`})`) + " AND NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref = task_ref.id)";
|
|
17390
17532
|
const params = admitted === null ? [repo, repo] : admitted;
|
|
17391
17533
|
return this.db
|
|
17392
17534
|
.prepare(`SELECT task.id AS task_id, task.title, task_scope.goal, task_scope.digest, task_scope.proposed_at, task_ref.repo AS task_repo
|
|
@@ -17416,9 +17558,10 @@ export class Store {
|
|
|
17416
17558
|
*/
|
|
17417
17559
|
listRequeueablesScoped(repo, now, limit = 10, admitted = null) {
|
|
17418
17560
|
const page = Math.max(1, Math.min(Math.floor(limit), 50));
|
|
17419
|
-
|
|
17561
|
+
// A put-away task (v145, task-tidy.md) is left out here, as from every list and count.
|
|
17562
|
+
const admission = (admitted === null
|
|
17420
17563
|
? "(? IS NULL OR task_ref.repo IS NULL OR task_ref.repo = ?)"
|
|
17421
|
-
: `(task_ref.repo IS NULL${admitted.length === 0 ? "" : ` OR task_ref.repo IN (${admitted.map(() => "?").join(",")})`})
|
|
17564
|
+
: `(task_ref.repo IS NULL${admitted.length === 0 ? "" : ` OR task_ref.repo IN (${admitted.map(() => "?").join(",")})`})`) + " AND NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref = task_ref.id)";
|
|
17422
17565
|
const params = admitted === null ? [repo, repo] : admitted;
|
|
17423
17566
|
return this.db
|
|
17424
17567
|
.prepare(`WITH open_incidents AS (
|
|
@@ -17482,6 +17625,8 @@ export class Store {
|
|
|
17482
17625
|
JOIN task_ref AS blocker_ref ON blocker_ref.backend = ? AND blocker_ref.external_id = task_edge.blocker
|
|
17483
17626
|
WHERE ${admissionOf("dependent_ref")}
|
|
17484
17627
|
AND ${admissionOf("blocker_ref")}
|
|
17628
|
+
-- Put-away tasks (v145) need no repair card, on either side.
|
|
17629
|
+
AND NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref IN (dependent_ref.id, blocker_ref.id))
|
|
17485
17630
|
GROUP BY task_edge.blocker, dependent_ref.repo, blocker_ref.repo
|
|
17486
17631
|
ORDER BY task_edge.blocker LIMIT ?`)
|
|
17487
17632
|
.all(BUILT_IN, BUILT_IN, ...paramsOf(), ...paramsOf(), page)
|
|
@@ -17514,6 +17659,7 @@ export class Store {
|
|
|
17514
17659
|
WHERE task.state = 'done'
|
|
17515
17660
|
AND (? IS NULL OR task_ref.repo IS NULL OR task_ref.repo = ?)
|
|
17516
17661
|
${admission}
|
|
17662
|
+
AND NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref = task_ref.id)
|
|
17517
17663
|
)
|
|
17518
17664
|
SELECT completed.*, run.id AS run_id, run.outcome, run.handoff, run.cost_usd, run.provider, run.auth_mode, run.started_at, run.finished_at,
|
|
17519
17665
|
publication.state AS pub_state, publication.pr_number, publication.pr_url,
|
|
@@ -17620,9 +17766,10 @@ export class Store {
|
|
|
17620
17766
|
// finding 11): in the roll-up, rows outside the ceiling must not
|
|
17621
17767
|
// consume the page and crowd out admitted cards. Serve still filters
|
|
17622
17768
|
// per-row afterward — this is the bound, that is the law.
|
|
17623
|
-
|
|
17769
|
+
// A put-away task (v145, task-tidy.md) is left out here, as from every list and count.
|
|
17770
|
+
const admission = (admitted === null
|
|
17624
17771
|
? "(? IS NULL OR task_ref.repo IS NULL OR task_ref.repo = ?)"
|
|
17625
|
-
: `(task_ref.repo IS NULL OR task_ref.repo IN (${admitted.map(() => "?").join(",")}))
|
|
17772
|
+
: `(task_ref.repo IS NULL OR task_ref.repo IN (${admitted.map(() => "?").join(",")}))`) + " AND NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref = task_ref.id)";
|
|
17626
17773
|
const admissionParams = admitted === null ? [repo, repo] : admitted;
|
|
17627
17774
|
const columns = `
|
|
17628
17775
|
SELECT task.id, task.title, task.state, task.updated_at, task.priority, task_ref.strikes, task_ref.repo AS task_repo,
|
|
@@ -17826,14 +17973,15 @@ export class Store {
|
|
|
17826
17973
|
SUM(CASE WHEN task.state = 'queued' THEN 1 ELSE 0 END) AS queued,
|
|
17827
17974
|
SUM(CASE WHEN task.state = 'running' THEN 1 ELSE 0 END) AS running
|
|
17828
17975
|
FROM task JOIN task_ref ON task_ref.backend = 'built-in' AND task_ref.external_id = task.id
|
|
17829
|
-
WHERE task_ref.repo =
|
|
17976
|
+
WHERE task_ref.repo = ? AND NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref = task_ref.id)`)
|
|
17830
17977
|
.get(repo);
|
|
17831
17978
|
const since = new Date(now.getTime() - 24 * 60 * 60_000).toISOString();
|
|
17832
17979
|
const done = this.db
|
|
17833
17980
|
.prepare(`SELECT COUNT(*) AS n FROM run
|
|
17834
17981
|
JOIN task_ref ON task_ref.id = run.task_ref
|
|
17835
17982
|
WHERE task_ref.repo = ? AND run.role IN ('builder','scout')
|
|
17836
|
-
AND run.outcome IN ('built','no-change') AND run.finished_at IS NOT NULL AND run.finished_at >=
|
|
17983
|
+
AND run.outcome IN ('built','no-change') AND run.finished_at IS NOT NULL AND run.finished_at >= ?
|
|
17984
|
+
AND NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref = task_ref.id)`)
|
|
17837
17985
|
.get(repo, since);
|
|
17838
17986
|
return { waiting: waiting, queued: Number(q.queued ?? 0), running: Number(q.running ?? 0), doneRecently: Number(done.n) };
|
|
17839
17987
|
}
|
|
@@ -17842,9 +17990,10 @@ export class Store {
|
|
|
17842
17990
|
// review, finding 11): with repo NULL the old predicate admitted every
|
|
17843
17991
|
// stored project, ceiling or not. An empty admitted list reduces to
|
|
17844
17992
|
// unplaced rows only — never IN ().
|
|
17845
|
-
|
|
17993
|
+
// A put-away task (v145, task-tidy.md) is left out here, as from every list and count.
|
|
17994
|
+
const admission = (admitted === null
|
|
17846
17995
|
? "(? IS NULL OR task_ref.repo IS NULL OR task_ref.repo = ?)"
|
|
17847
|
-
: `(task_ref.repo IS NULL${admitted.length === 0 ? "" : ` OR task_ref.repo IN (${admitted.map(() => "?").join(",")})`})
|
|
17996
|
+
: `(task_ref.repo IS NULL${admitted.length === 0 ? "" : ` OR task_ref.repo IN (${admitted.map(() => "?").join(",")})`})`) + " AND NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref = task_ref.id)";
|
|
17848
17997
|
const params = () => (admitted === null ? [repo, repo] : [...admitted]);
|
|
17849
17998
|
const row = this.db
|
|
17850
17999
|
.prepare(`SELECT COUNT(*) AS n FROM (
|
|
@@ -18697,6 +18846,8 @@ export class Store {
|
|
|
18697
18846
|
*/
|
|
18698
18847
|
resumeRunStop(args, now) {
|
|
18699
18848
|
return this.transact(() => {
|
|
18849
|
+
if (refArchived(this, args.taskRef))
|
|
18850
|
+
return { ok: false, reason: "archived", detail: "this task is put away — restore it first" };
|
|
18700
18851
|
const stop = this.stopOf(args.runId);
|
|
18701
18852
|
const run = this.getRun(args.runId);
|
|
18702
18853
|
if (stop === null || run === null)
|
|
@@ -21669,3 +21820,326 @@ export function dropFeedbackImage(store, runId, commentId, actor, unusable, now)
|
|
|
21669
21820
|
return { ok: true, id };
|
|
21670
21821
|
});
|
|
21671
21822
|
}
|
|
21823
|
+
/** The put-away record of this task, or null when it is not put away. */
|
|
21824
|
+
export function archiveOf(store, taskId) {
|
|
21825
|
+
const row = store.handle.prepare(`SELECT a.*, r.external_id FROM task_archive a JOIN task_ref r ON r.id = a.task_ref
|
|
21826
|
+
WHERE r.backend = 'built-in' AND r.external_id = ?`).get(taskId);
|
|
21827
|
+
return row === undefined ? null : { taskRef: Number(row["task_ref"]), taskId: String(row["external_id"]), by: String(row["by"]),
|
|
21828
|
+
byKind: row["by_kind"] === "tidy" ? "tidy" : "person", rule: row["rule"] == null ? null : String(row["rule"]), reason: String(row["reason"]), at: String(row["at"]) };
|
|
21829
|
+
}
|
|
21830
|
+
/** Every version of a task's family, by the same proven lineage the task page reads (`taskFamilyOf`: same project, each
|
|
21831
|
+
* revision bound to its parent's own run by its brief). A family whose lineage doesn't prove is just this task. */
|
|
21832
|
+
export function taskFamilyRefs(store, taskId) {
|
|
21833
|
+
const family = store.taskFamilyOf(taskId, null, true);
|
|
21834
|
+
return family === null ? [] : family.versions.map(one => ({ id: one.refId, taskId: one.id })).sort((a, b) => a.id - b.id);
|
|
21835
|
+
}
|
|
21836
|
+
/** The first family version with something still going on, or null: running, a live newest claim, an unfinished run,
|
|
21837
|
+
* a process Toolroll hasn't seen exit, a build whose processes were never recorded, a stop still settling, a follow-up
|
|
21838
|
+
* check asked for and not finished, a flow step running for its card, a release in flight (a live release approval
|
|
21839
|
+
* that names it, or a deploy or merge reserved, running or unknown), or a pull request merging (an intent claimed or
|
|
21840
|
+
* firing, or a person's merge between its last check and GitHub's answer). */
|
|
21841
|
+
export function liveFamilyAttempt(store, refs, now) {
|
|
21842
|
+
if (refs.length === 0)
|
|
21843
|
+
return null;
|
|
21844
|
+
const row = store.handle.prepare(`SELECT r.external_id FROM task_ref r JOIN task t ON t.id = r.external_id
|
|
21845
|
+
WHERE r.id IN (SELECT value FROM json_each(?)) AND (t.state = 'running'
|
|
21846
|
+
OR EXISTS (SELECT 1 FROM claim c WHERE c.task_ref = r.id AND c.released_at IS NULL AND c.expires_at > ?
|
|
21847
|
+
AND c.lease_generation = (SELECT MAX(n.lease_generation) FROM claim n WHERE n.task_ref = r.id))
|
|
21848
|
+
OR EXISTS (SELECT 1 FROM run x WHERE x.task_ref = r.id AND x.outcome IS NULL)
|
|
21849
|
+
OR EXISTS (SELECT 1 FROM run_process p JOIN run x ON x.id = p.run WHERE x.task_ref = r.id AND p.exited_at IS NULL AND p.container_empty_at IS NULL)
|
|
21850
|
+
OR EXISTS (SELECT 1 FROM run x WHERE x.task_ref = r.id AND x.provider_started_at IS NOT NULL
|
|
21851
|
+
AND NOT EXISTS (SELECT 1 FROM run_process p WHERE p.run = x.id) AND NOT EXISTS (SELECT 1 FROM run_process_summary s WHERE s.run = x.id))
|
|
21852
|
+
OR EXISTS (SELECT 1 FROM run_stop s WHERE s.task_ref = r.id AND s.resumed_at IS NULL AND s.settled_at IS NULL)
|
|
21853
|
+
OR EXISTS (SELECT 1 FROM action_ledger q JOIN run x ON x.id = q.run_id WHERE x.task_ref = r.id AND q.action = 'checks requested'
|
|
21854
|
+
AND NOT EXISTS (SELECT 1 FROM action_ledger f WHERE f.run_id = q.run_id AND f.action = 'checks finished' AND f.outcome LIKE q.id || ':%'))
|
|
21855
|
+
OR EXISTS (SELECT 1 FROM flow_card c JOIN flow_step_run s ON s.card = c.id
|
|
21856
|
+
WHERE s.state = 'running' AND (c.task = r.external_id OR c.primary_task = r.external_id))
|
|
21857
|
+
OR EXISTS (SELECT 1 FROM flow_step_task t JOIN flow_step_run s ON s.card = t.card AND s.entry = t.entry
|
|
21858
|
+
WHERE s.state = 'running' AND t.task_id = r.external_id)
|
|
21859
|
+
OR EXISTS (SELECT 1 FROM deploy_in_flight d WHERE d.task_ref = r.id AND d.at > ?)
|
|
21860
|
+
OR EXISTS (SELECT 1 FROM release_authority a WHERE a.state IN ('open','deploying','merge-reserved') AND (a.anchor_task = r.external_id OR a.tip_task = r.external_id))
|
|
21861
|
+
OR EXISTS (SELECT 1 FROM release_operation o WHERE o.task_id = r.external_id AND o.state IN ('reserved','running','unknown'))
|
|
21862
|
+
OR EXISTS (SELECT 1 FROM merge_intent i JOIN publication p ON p.id = i.publication WHERE p.task_ref = r.id AND i.state IN ('claimed','firing'))
|
|
21863
|
+
OR EXISTS (SELECT 1 FROM merge_in_flight m WHERE m.task_ref = r.id AND m.at > ?))
|
|
21864
|
+
LIMIT 1`).get(JSON.stringify(refs), now.toISOString(), new Date(now.getTime() - 30 * 60_000).toISOString(), new Date(now.getTime() - 15 * 60_000).toISOString());
|
|
21865
|
+
return row === undefined ? null : String(row["external_id"]);
|
|
21866
|
+
}
|
|
21867
|
+
/** Puts a task away (v145): every version of its family, in one write transaction that re-checks it has no live
|
|
21868
|
+
* attempt, that its versions are exactly `expect` when the caller showed a person a family (a preview), and that
|
|
21869
|
+
* `guard` (the caller's own rule, re-read under the lock) still allows it. Its state, history, results, approvals and
|
|
21870
|
+
* ledger are untouched; its open notifications are resolved; one ledger row records who and why. */
|
|
21871
|
+
export function archiveTaskFamily(store, taskId, by, now, options = {}) {
|
|
21872
|
+
return store.transact(() => {
|
|
21873
|
+
const family = taskFamilyRefs(store, taskId);
|
|
21874
|
+
if (family.length === 0)
|
|
21875
|
+
return { ok: false, reason: "unknown-task", message: `No task ${taskId}.` };
|
|
21876
|
+
const refs = family.map(one => one.id);
|
|
21877
|
+
if (options.expect !== undefined && [...options.expect].sort((a, b) => a - b).join(",") !== refs.join(",")) {
|
|
21878
|
+
return { ok: false, reason: "blocked", message: `${taskId}'s versions changed since the list was shown.` };
|
|
21879
|
+
}
|
|
21880
|
+
// Already put away, every version: nothing to do. A family a person revived with a new revision puts away the rest.
|
|
21881
|
+
const remaining = refs.filter(ref => !refArchived(store, ref));
|
|
21882
|
+
if (remaining.length === 0)
|
|
21883
|
+
return { ok: false, reason: "archived", message: `${taskId} is already put away.` };
|
|
21884
|
+
const live = liveFamilyAttempt(store, refs, now);
|
|
21885
|
+
if (live !== null)
|
|
21886
|
+
return { ok: false, reason: "live", message: `${taskId} still has work going on. Stop it or let it finish, then put it away.` };
|
|
21887
|
+
const blocked = options.guard?.(refs) ?? null;
|
|
21888
|
+
if (blocked !== null)
|
|
21889
|
+
return { ok: false, reason: "blocked", message: blocked };
|
|
21890
|
+
const reason = by.reason.replace(/[\u0000-\u001f\u007f]+/g, " ").trim().slice(0, 300) || "Put away";
|
|
21891
|
+
const at = now.toISOString();
|
|
21892
|
+
const insert = store.handle.prepare("INSERT INTO task_archive (task_ref, by, by_kind, rule, reason, at) VALUES (?, ?, ?, ?, ?, ?)");
|
|
21893
|
+
for (const ref of remaining)
|
|
21894
|
+
insert.run(ref, by.by, by.byKind, by.rule, reason, at);
|
|
21895
|
+
store.handle.prepare("UPDATE notification SET resolved_at = ? WHERE resolved_at IS NULL AND task_ref IN (SELECT value FROM json_each(?))")
|
|
21896
|
+
.run(at, JSON.stringify(refs));
|
|
21897
|
+
// The family's flow cards hold while it is put away, so what they queued for people is settled too.
|
|
21898
|
+
store.handle.prepare(`UPDATE notification SET resolved_at = ? WHERE resolved_at IS NULL AND EXISTS (SELECT 1 FROM flow_card c
|
|
21899
|
+
WHERE (c.task IN (SELECT value FROM json_each(?)) OR c.primary_task IN (SELECT value FROM json_each(?)))
|
|
21900
|
+
AND (notification.dedupe_key LIKE 'flow:' || c.id || ':%' OR notification.dedupe_key LIKE 'flow-card:' || c.id || ':%'))`)
|
|
21901
|
+
.run(at, JSON.stringify(family.map(one => one.taskId)), JSON.stringify(family.map(one => one.taskId)));
|
|
21902
|
+
const repo = store.refById(refs[0])?.repo ?? null;
|
|
21903
|
+
store.recordAction({ at, actor: by.byKind === "tidy" ? "tidy" : `operator:${by.by}`, repo, taskId, runId: null, action: "task archived",
|
|
21904
|
+
outcome: by.rule ?? "by hand", source: "work", detail: `${reason}${refs.length > 1 ? ` (${refs.length} versions)` : ""}` });
|
|
21905
|
+
return { ok: true, refs };
|
|
21906
|
+
});
|
|
21907
|
+
}
|
|
21908
|
+
/** Brings a put-away task back (v145): every version of its family, with a ledger row. Tidy never puts a restored
|
|
21909
|
+
* task away again (tidy.ts reads the `task restored` row). */
|
|
21910
|
+
export function restoreTaskFamily(store, taskId, by, now, guard) {
|
|
21911
|
+
return store.transact(() => {
|
|
21912
|
+
const family = taskFamilyRefs(store, taskId);
|
|
21913
|
+
if (family.length === 0)
|
|
21914
|
+
return { ok: false, reason: "unknown-task", message: `No task ${taskId}.` };
|
|
21915
|
+
const refs = family.map(one => one.id).filter(ref => refArchived(store, ref));
|
|
21916
|
+
if (refs.length === 0)
|
|
21917
|
+
return { ok: false, reason: "not-archived", message: `${taskId} isn't put away.` };
|
|
21918
|
+
const blocked = guard?.(refs) ?? null;
|
|
21919
|
+
if (blocked !== null)
|
|
21920
|
+
return { ok: false, reason: "blocked", message: blocked };
|
|
21921
|
+
store.handle.prepare("DELETE FROM task_archive WHERE task_ref IN (SELECT value FROM json_each(?))").run(JSON.stringify(refs));
|
|
21922
|
+
const at = now.toISOString();
|
|
21923
|
+
store.recordAction({ at, actor: `operator:${by}`, repo: store.refById(refs[0])?.repo ?? null, taskId, runId: null, action: "task restored",
|
|
21924
|
+
outcome: "recorded", source: "work", detail: refs.length > 1 ? `${refs.length} versions` : null });
|
|
21925
|
+
return { ok: true, refs };
|
|
21926
|
+
});
|
|
21927
|
+
}
|
|
21928
|
+
/**
|
|
21929
|
+
* What a LIVE invite grants, for its own join page (Wave E, "Your access"): the role, who invited, the projects and
|
|
21930
|
+
* when it expires. Read without spending an attempt and under exactly inviteIsLive's predicate, so every dead shape
|
|
21931
|
+
* reads as null and still gets the one indistinguishable page (D6). Only a holder of the live token sees it.
|
|
21932
|
+
*/
|
|
21933
|
+
export function liveInviteTerms(store, tokenValue, now) {
|
|
21934
|
+
const row = store.handle
|
|
21935
|
+
.prepare("SELECT role, minted_by, expires_at, projects_json FROM invite WHERE token_hash = ? AND revoked_at IS NULL AND consumed_at IS NULL AND expires_at > ? AND attempts < 10")
|
|
21936
|
+
.get(createHash("sha256").update(tokenValue, "utf8").digest("hex"), now.toISOString());
|
|
21937
|
+
if (row === undefined)
|
|
21938
|
+
return null;
|
|
21939
|
+
return { role: String(row["role"]) === "approver" ? "approver" : "viewer", mintedBy: String(row["minted_by"]), expiresAt: String(row["expires_at"]), projects: readProjectAccess(row["projects_json"]) };
|
|
21940
|
+
}
|
|
21941
|
+
/** At most this many checks are kept for one head; GitHub's rollup is far smaller in practice. */
|
|
21942
|
+
export const CI_CHECKS_MAX = 200;
|
|
21943
|
+
const CI_CHECK_TEXT_MAX = 200;
|
|
21944
|
+
const CI_CHECK_URL_MAX = 2048;
|
|
21945
|
+
const CI_FAILED_CONCLUSIONS = new Set(["FAILURE", "ERROR", "TIMED_OUT", "ACTION_REQUIRED", "STARTUP_FAILURE"]);
|
|
21946
|
+
const CI_PENDING = new Set(["QUEUED", "WAITING", "PENDING", "REQUESTED"]);
|
|
21947
|
+
const ISO_INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
21948
|
+
/** A timestamp GitHub gave, as an exact ISO instant, or null (GitHub's zero time "0001-01-01T00:00:00Z" is no time). */
|
|
21949
|
+
function ciInstant(value) {
|
|
21950
|
+
if (typeof value !== "string" || value.length > 40 || !ISO_INSTANT.test(value))
|
|
21951
|
+
return null;
|
|
21952
|
+
const at = Date.parse(value);
|
|
21953
|
+
if (!Number.isFinite(at) || at < Date.UTC(2000, 0, 1))
|
|
21954
|
+
return null;
|
|
21955
|
+
return new Date(at).toISOString();
|
|
21956
|
+
}
|
|
21957
|
+
/** The same ordering in SQL, for a column holding a check state. */
|
|
21958
|
+
const CI_SEVERITY_SQL = (column) => `CASE ${column} ${Object.entries({ failed: 6, cancelled: 5, running: 4, queued: 3, neutral: 2, skipped: 1, passed: 0 }).map(([state, n]) => `WHEN '${state}' THEN ${n}`).join(" ")} ELSE 0 END`;
|
|
21959
|
+
/** How bad a check state is: when one check identity appears twice, the worse stands. */
|
|
21960
|
+
const CI_SEVERITY = { failed: 6, cancelled: 5, running: 4, queued: 3, neutral: 2, skipped: 1, passed: 0 };
|
|
21961
|
+
/** A check's identity: its kind, its workflow and its name. */
|
|
21962
|
+
function ciCheckIdent(kind, row) {
|
|
21963
|
+
// Unit separators, never NUL (SQLite text ends at one): names and workflows had every control character removed.
|
|
21964
|
+
return `${kind}\u001f${row.workflow ?? ""}\u001f${row.name}`;
|
|
21965
|
+
}
|
|
21966
|
+
/** Short, printable text, or null. Every control (C0, DEL, C1) and format character (bidi overrides and isolates,
|
|
21967
|
+
* zero-width marks) is removed, so a check name can't move the terminal's cursor or reorder what `task ship` prints.
|
|
21968
|
+
* Check names never need a zero-width joiner, so emoji sequences lose theirs: that is the accepted cost. */
|
|
21969
|
+
function ciText(value) {
|
|
21970
|
+
if (typeof value !== "string")
|
|
21971
|
+
return null;
|
|
21972
|
+
const text = value.replace(/[\p{Cc}\p{Cf}]/gu, "").trim();
|
|
21973
|
+
return text === "" || text.length > CI_CHECK_TEXT_MAX ? null : text;
|
|
21974
|
+
}
|
|
21975
|
+
/** Only an https link to GitHub itself is kept; anything else reads null. */
|
|
21976
|
+
function ciUrl(value) {
|
|
21977
|
+
if (typeof value !== "string" || value.length > CI_CHECK_URL_MAX)
|
|
21978
|
+
return null;
|
|
21979
|
+
try {
|
|
21980
|
+
const url = new URL(value);
|
|
21981
|
+
return url.protocol === "https:" && url.username === "" && url.password === "" && url.port === "" && url.hostname === "github.com" ? url.toString() : null;
|
|
21982
|
+
}
|
|
21983
|
+
catch {
|
|
21984
|
+
return null;
|
|
21985
|
+
}
|
|
21986
|
+
}
|
|
21987
|
+
/**
|
|
21988
|
+
* GitHub's statusCheckRollup (gh pr view --json statusCheckRollup) as check rows: CheckRun items (name, workflowName,
|
|
21989
|
+
* status, conclusion, startedAt, completedAt, detailsUrl) and StatusContext items (context, state, startedAt, targetUrl).
|
|
21990
|
+
* Strict: an item that is not one of those shapes, has no usable name, or names a state it doesn't know is dropped; one
|
|
21991
|
+
* row per identity (kind, workflow and name), and when one identity appears twice (a re-run) the worse state stands;
|
|
21992
|
+
* at most CI_CHECKS_MAX.
|
|
21993
|
+
*/
|
|
21994
|
+
export function parseCiRollup(rollup) {
|
|
21995
|
+
if (!Array.isArray(rollup))
|
|
21996
|
+
return [];
|
|
21997
|
+
const rows = [];
|
|
21998
|
+
const seen = new Map();
|
|
21999
|
+
for (const item of rollup.slice(0, CI_CHECKS_MAX * 2)) {
|
|
22000
|
+
if (rows.length >= CI_CHECKS_MAX)
|
|
22001
|
+
break;
|
|
22002
|
+
if (typeof item !== "object" || item === null || Array.isArray(item))
|
|
22003
|
+
continue;
|
|
22004
|
+
const raw = item;
|
|
22005
|
+
const kind = raw["__typename"];
|
|
22006
|
+
let row = null;
|
|
22007
|
+
if (kind === "CheckRun" || (kind === undefined && typeof raw["status"] === "string" && raw["context"] === undefined)) {
|
|
22008
|
+
const name = ciText(raw["name"]);
|
|
22009
|
+
const status = typeof raw["status"] === "string" ? raw["status"].toUpperCase() : "";
|
|
22010
|
+
const conclusion = typeof raw["conclusion"] === "string" ? raw["conclusion"].toUpperCase() : "";
|
|
22011
|
+
const state = status === "IN_PROGRESS" ? "running"
|
|
22012
|
+
: CI_PENDING.has(status) ? "queued"
|
|
22013
|
+
: status !== "COMPLETED" ? null
|
|
22014
|
+
: conclusion === "SUCCESS" ? "passed"
|
|
22015
|
+
: CI_FAILED_CONCLUSIONS.has(conclusion) ? "failed"
|
|
22016
|
+
: conclusion === "CANCELLED" ? "cancelled"
|
|
22017
|
+
: conclusion === "SKIPPED" ? "skipped"
|
|
22018
|
+
: conclusion === "NEUTRAL" || conclusion === "STALE" ? "neutral"
|
|
22019
|
+
: null;
|
|
22020
|
+
if (name === null || state === null)
|
|
22021
|
+
continue;
|
|
22022
|
+
const completedAt = state === "running" || state === "queued" ? null : ciInstant(raw["completedAt"]);
|
|
22023
|
+
row = { name, workflow: ciText(raw["workflowName"]), state, startedAt: state === "queued" ? null : ciInstant(raw["startedAt"]), completedAt, url: ciUrl(raw["detailsUrl"]), kind: "run" };
|
|
22024
|
+
}
|
|
22025
|
+
else if (kind === "StatusContext" || (kind === undefined && typeof raw["context"] === "string")) {
|
|
22026
|
+
const name = ciText(raw["context"]);
|
|
22027
|
+
const value = typeof raw["state"] === "string" ? raw["state"].toUpperCase() : "";
|
|
22028
|
+
const state = value === "SUCCESS" ? "passed" : value === "FAILURE" || value === "ERROR" ? "failed"
|
|
22029
|
+
: value === "PENDING" || value === "EXPECTED" ? "running" : null;
|
|
22030
|
+
if (name === null || state === null)
|
|
22031
|
+
continue;
|
|
22032
|
+
row = { name, workflow: null, state, startedAt: ciInstant(raw["startedAt"] ?? raw["createdAt"]), completedAt: null, url: ciUrl(raw["targetUrl"]), kind: "status" };
|
|
22033
|
+
}
|
|
22034
|
+
if (row === null)
|
|
22035
|
+
continue;
|
|
22036
|
+
// A completion before its start is GitHub's clock disagreeing with itself: keep the state, drop the end.
|
|
22037
|
+
if (row.startedAt !== null && row.completedAt !== null && row.completedAt < row.startedAt)
|
|
22038
|
+
row = { ...row, completedAt: null };
|
|
22039
|
+
const ident = ciCheckIdent(row.kind ?? "run", row);
|
|
22040
|
+
const at = seen.get(ident);
|
|
22041
|
+
if (at !== undefined) {
|
|
22042
|
+
if (CI_SEVERITY[row.state] > CI_SEVERITY[rows[at].state])
|
|
22043
|
+
rows[at] = row;
|
|
22044
|
+
continue;
|
|
22045
|
+
}
|
|
22046
|
+
seen.set(ident, rows.length);
|
|
22047
|
+
rows.push(row);
|
|
22048
|
+
}
|
|
22049
|
+
return rows;
|
|
22050
|
+
}
|
|
22051
|
+
/**
|
|
22052
|
+
* Replace one head's checks wholesale (v146). Call ONLY with an observation that won its generation (publish.ts settles
|
|
22053
|
+
* and records in one transaction): a losing, stale answer never writes here.
|
|
22054
|
+
*/
|
|
22055
|
+
export function recordCiChecks(store, githubRepo, prNumber, headSha, rows, now) {
|
|
22056
|
+
store.transact(() => {
|
|
22057
|
+
// One head per pull request is kept: an older head's rows go with the new observation.
|
|
22058
|
+
store.handle.prepare("DELETE FROM ci_check WHERE github_repo = ? AND pr_number = ?").run(githubRepo, prNumber);
|
|
22059
|
+
const insert = store.handle.prepare(`INSERT INTO ci_check (github_repo, pr_number, head_sha, ident, name, workflow, state, started_at, completed_at, url, observed_at)
|
|
22060
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
22061
|
+
ON CONFLICT (github_repo, pr_number, head_sha, ident) DO UPDATE SET state = excluded.state, started_at = excluded.started_at,
|
|
22062
|
+
completed_at = excluded.completed_at, url = excluded.url WHERE ${CI_SEVERITY_SQL("excluded.state")} > ${CI_SEVERITY_SQL("ci_check.state")}`);
|
|
22063
|
+
const at = now.toISOString();
|
|
22064
|
+
// Rows a caller built itself carry no kind: a row with a workflow is a check run, one without may be a status; the
|
|
22065
|
+
// identity keeps the two apart either way.
|
|
22066
|
+
for (const row of rows.slice(0, CI_CHECKS_MAX))
|
|
22067
|
+
insert.run(githubRepo, prNumber, headSha, ciCheckIdent(row.kind ?? (row.workflow === null ? "status" : "run"), row), row.name, row.workflow, row.state, row.startedAt, row.completedAt, row.url, at);
|
|
22068
|
+
});
|
|
22069
|
+
}
|
|
22070
|
+
/** A pull request's checks for the head the plane's last winning observation recorded, or null when none was. */
|
|
22071
|
+
export function ciChecksFor(store, githubRepo, prNumber) {
|
|
22072
|
+
const observation = store.handle.prepare("SELECT head_sha, state, observed_at FROM ci_observation WHERE github_repo = ? AND pr_number = ?").get(githubRepo, prNumber);
|
|
22073
|
+
if (observation === undefined || String(observation["head_sha"]) === "" || String(observation["observed_at"]) === "")
|
|
22074
|
+
return null;
|
|
22075
|
+
const headSha = String(observation["head_sha"]);
|
|
22076
|
+
const rows = store.handle.prepare(`SELECT ident, name, workflow, state, started_at, completed_at, url, observed_at FROM ci_check
|
|
22077
|
+
WHERE github_repo = ? AND pr_number = ? AND head_sha = ? ORDER BY COALESCE(started_at, '9999'), name`).all(githubRepo, prNumber, headSha);
|
|
22078
|
+
const text = (value) => value === null || value === undefined ? null : String(value);
|
|
22079
|
+
return {
|
|
22080
|
+
githubRepo, prNumber, headSha, observedAt: String(observation["observed_at"]),
|
|
22081
|
+
observed: ["passing", "failing", "running"].find(one => one === String(observation["state"])) ?? "none",
|
|
22082
|
+
checks: rows.map(row => ({ name: String(row["name"]), workflow: text(row["workflow"]), state: String(row["state"]),
|
|
22083
|
+
startedAt: text(row["started_at"]), completedAt: text(row["completed_at"]), url: text(row["url"]),
|
|
22084
|
+
kind: String(row["ident"]).startsWith("status\u001f") ? "status" : "run" })),
|
|
22085
|
+
};
|
|
22086
|
+
}
|
|
22087
|
+
/** A task's pull request checks: its family's newest publication that has a pull request number, and that pull
|
|
22088
|
+
* request's checks (ciChecksFor). Null: no pull request, or none observed yet. */
|
|
22089
|
+
export function shipChecksForTask(store, taskId) {
|
|
22090
|
+
const refs = taskFamilyRefs(store, taskId).map(one => one.id);
|
|
22091
|
+
if (refs.length === 0)
|
|
22092
|
+
return null;
|
|
22093
|
+
const row = store.handle.prepare(`SELECT github_repo, pr_number FROM publication WHERE task_ref IN (SELECT value FROM json_each(?))
|
|
22094
|
+
AND pr_number IS NOT NULL ORDER BY id DESC LIMIT 1`).get(JSON.stringify(refs));
|
|
22095
|
+
return row === undefined ? null : ciChecksFor(store, String(row["github_repo"]), Number(row["pr_number"]));
|
|
22096
|
+
}
|
|
22097
|
+
const RELEASE_STEP_NAMES = new Set(["gate", "approval", "check", "complete", "quiet", "deploy", "link", "ci", "merge", "tree", "tag", "publish", "homebrew"]);
|
|
22098
|
+
/**
|
|
22099
|
+
* Record a release step (v146): `running` or `waiting` opens it (its first start is kept), `done` or `failed` ends it.
|
|
22100
|
+
* A step that runs again after a stop starts its row again. A failed write throws here; the release's adapter swallows
|
|
22101
|
+
* it, so recording never changes a release.
|
|
22102
|
+
*/
|
|
22103
|
+
export function recordReleaseStep(store, entry) {
|
|
22104
|
+
if (!RELEASE_STEP_NAMES.has(entry.step) || entry.taskId === "" || entry.tag === "")
|
|
22105
|
+
return;
|
|
22106
|
+
const at = entry.at.toISOString();
|
|
22107
|
+
// A stop message can carry a remote tool's words: no control or format character reaches `release status`.
|
|
22108
|
+
const detail = entry.detail == null ? null : entry.detail.replace(/[\p{Cc}\p{Cf}]+/gu, " ").trim().slice(0, 1000);
|
|
22109
|
+
const ended = entry.state === "done" || entry.state === "failed";
|
|
22110
|
+
store.handle.prepare(`INSERT INTO release_step (task_id, tag, version, pr, step, state, started_at, ended_at, detail, updated_at)
|
|
22111
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
22112
|
+
ON CONFLICT (task_id, tag, step) DO UPDATE SET
|
|
22113
|
+
state = excluded.state, version = excluded.version, pr = excluded.pr, updated_at = excluded.updated_at,
|
|
22114
|
+
started_at = CASE WHEN release_step.ended_at IS NOT NULL AND excluded.ended_at IS NULL THEN excluded.started_at ELSE release_step.started_at END,
|
|
22115
|
+
ended_at = excluded.ended_at,
|
|
22116
|
+
detail = CASE WHEN excluded.detail IS NOT NULL OR excluded.ended_at IS NULL THEN excluded.detail ELSE release_step.detail END`)
|
|
22117
|
+
.run(entry.taskId, entry.tag, entry.version, entry.pr, entry.step, entry.state, at, ended ? at : null, detail, at);
|
|
22118
|
+
}
|
|
22119
|
+
/** A release's steps, for the release task: the newest tag recorded for it, its steps in the order they ran, and the
|
|
22120
|
+
* step it stopped at when the last thing recorded was a failure. Null: no step recorded. */
|
|
22121
|
+
export function releaseProgressFor(store, taskId) {
|
|
22122
|
+
const newest = store.handle.prepare("SELECT tag FROM release_step WHERE task_id = ? ORDER BY updated_at DESC, rowid DESC LIMIT 1").get(taskId);
|
|
22123
|
+
if (newest === undefined)
|
|
22124
|
+
return null;
|
|
22125
|
+
const tag = String(newest["tag"]);
|
|
22126
|
+
const rows = store.handle.prepare("SELECT * FROM release_step WHERE task_id = ? AND tag = ? ORDER BY started_at, rowid").all(taskId, tag);
|
|
22127
|
+
const steps = rows.map(row => ({ step: String(row["step"]), state: String(row["state"]), startedAt: String(row["started_at"]),
|
|
22128
|
+
endedAt: row["ended_at"] === null ? null : String(row["ended_at"]), detail: row["detail"] === null ? null : String(row["detail"]) }));
|
|
22129
|
+
const last = [...rows].sort((a, b) => String(a["updated_at"]).localeCompare(String(b["updated_at"]))).at(-1);
|
|
22130
|
+
const stopped = String(last["state"]) === "failed" ? { step: String(last["step"]), message: last["detail"] === null ? "" : String(last["detail"]), at: String(last["updated_at"]) } : null;
|
|
22131
|
+
const pr = last["pr"] === null ? null : Number(last["pr"]);
|
|
22132
|
+
return { tag, version: String(last["version"]), pr, startedAt: steps[0].startedAt, updatedAt: String(last["updated_at"]), steps, stopped };
|
|
22133
|
+
}
|
|
22134
|
+
/** The release task whose steps were recorded most recently (v146), or null when none was. */
|
|
22135
|
+
export function latestReleaseStepTask(store) {
|
|
22136
|
+
const row = store.handle.prepare("SELECT task_id FROM release_step ORDER BY updated_at DESC, rowid DESC LIMIT 1").get();
|
|
22137
|
+
return row === undefined ? null : String(row["task_id"]);
|
|
22138
|
+
}
|
|
22139
|
+
/** Seconds a step or check took (to its end, or to `now` while it runs); null without a start. */
|
|
22140
|
+
export function elapsedSeconds(startedAt, endedAt, now) {
|
|
22141
|
+
if (startedAt === null)
|
|
22142
|
+
return null;
|
|
22143
|
+
const start = Date.parse(startedAt), end = endedAt === null ? now.getTime() : Date.parse(endedAt);
|
|
22144
|
+
return Number.isFinite(start) && Number.isFinite(end) ? Math.max(0, Math.round((end - start) / 1000)) : null;
|
|
22145
|
+
}
|