toolroll 0.9.84 → 0.9.86
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 +66 -0
- package/README.md +94 -1193
- package/dist/agent-fence.d.ts +9 -5
- package/dist/agent-fence.js +6 -5
- package/dist/agent-onboard.d.ts +2 -0
- package/dist/agent-onboard.js +3 -0
- package/dist/assignment.js +4 -3
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +33 -31
- package/dist/browser-workspace.d.ts +45 -0
- package/dist/chat-actions.d.ts +11 -1
- package/dist/chat-actions.js +53 -1
- package/dist/chat-channel.d.ts +10 -1
- package/dist/chat-channel.js +36 -5
- package/dist/chat-decide.d.ts +16 -4
- package/dist/chat-decide.js +239 -31
- package/dist/chat-messages.d.ts +5 -0
- package/dist/chat-messages.js +1 -1
- package/dist/chat-quiet.js +6 -4
- package/dist/chat-review.d.ts +4 -0
- package/dist/chat-review.js +13 -4
- package/dist/chat-terms.d.ts +9 -0
- package/dist/chat-terms.js +46 -0
- 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/console-api.d.ts +6 -2
- package/dist/contracts/console-api.js +6 -2
- 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 +51 -0
- package/dist/contracts/lead-tools.js +5 -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/contracts/youiyoux-request.d.ts +248 -0
- package/dist/contracts/youiyoux-request.js +152 -0
- package/dist/coordinator.js +1 -0
- package/dist/deploy-gate.js +4 -0
- package/dist/dispatch.d.ts +3 -0
- package/dist/dispatch.js +4 -1
- package/dist/flow-browser.d.ts +103 -0
- package/dist/flow-browser.js +204 -0
- package/dist/flow-cadence.d.ts +8 -2
- package/dist/flow-cadence.js +16 -3
- package/dist/flow-card-task.d.ts +9 -0
- package/dist/flow-card-task.js +15 -6
- 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-live.js +9 -1
- package/dist/flow-pull-request.js +15 -7
- package/dist/flow-share.js +3 -1
- package/dist/flow-steps.d.ts +9 -1
- package/dist/flow-steps.js +55 -5
- package/dist/flow-triggers.js +55 -0
- package/dist/flows-cli.d.ts +19 -0
- package/dist/flows-cli.js +47 -2
- package/dist/flows-ui.js +11 -2
- package/dist/flows.js +5 -2
- package/dist/integrations-ui.js +9 -4
- package/dist/integrations.d.ts +9 -1
- package/dist/integrations.js +43 -2
- package/dist/landed-cleanup.d.ts +4 -0
- package/dist/landed-cleanup.js +3 -2
- package/dist/lead-capabilities.js +2 -1
- package/dist/lead-cli.js +3 -1
- package/dist/lead-tools.d.ts +2 -0
- package/dist/lead-tools.js +65 -10
- package/dist/lead-voice.js +16 -5
- package/dist/lead.d.ts +2 -0
- package/dist/lead.js +3 -3
- package/dist/operate.d.ts +2 -3
- package/dist/operate.js +226 -21
- package/dist/project-activity.d.ts +20 -0
- package/dist/project-activity.js +23 -0
- package/dist/project-cli.d.ts +1 -0
- package/dist/project-cli.js +36 -19
- package/dist/project-delete.js +7 -3
- package/dist/project-knowledge.d.ts +3 -3
- package/dist/project-outcomes.d.ts +119 -0
- package/dist/project-outcomes.js +79 -0
- package/dist/pull-request-flow.js +35 -4
- package/dist/result-actions.d.ts +4 -0
- package/dist/result-actions.js +41 -9
- package/dist/result-follow-ups.js +4 -0
- package/dist/result-review.d.ts +9 -0
- package/dist/result-review.js +45 -5
- package/dist/scope.d.ts +5 -0
- package/dist/scope.js +7 -0
- package/dist/server/chat.js +4 -2
- package/dist/server/console.css +41 -0
- package/dist/server/flows.js +15 -0
- package/dist/server/pages.js +11 -1
- package/dist/server/render-chat.js +3 -2
- package/dist/server/render-pages.d.ts +15 -2
- package/dist/server/render-pages.js +52 -4
- package/dist/server/render-results.d.ts +2 -0
- package/dist/server/render-results.js +20 -7
- package/dist/server/render-tasks.d.ts +9 -0
- package/dist/server/render-tasks.js +32 -11
- package/dist/server/route-table.js +12 -0
- package/dist/server/settings.js +18 -1
- package/dist/server/task-views.js +11 -0
- package/dist/server/tasks.d.ts +10 -0
- package/dist/server/tasks.js +95 -7
- package/dist/settings-proofs.js +1 -0
- package/dist/store.d.ts +116 -5
- package/dist/store.js +385 -24
- package/dist/surface.js +18 -2
- 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/task-source.d.ts +13 -1
- package/dist/task-source.js +17 -1
- package/dist/telegram-lead.js +7 -5
- package/dist/telegram.js +1 -1
- package/dist/tidy.d.ts +130 -0
- package/dist/tidy.js +503 -0
- package/dist/toolroll-update.js +2 -2
- package/dist/webhooks.d.ts +6 -0
- package/dist/webhooks.js +25 -0
- package/dist/work-index.d.ts +28 -0
- package/dist/work-index.js +82 -28
- package/dist/workspace-ui.d.ts +1 -1
- package/dist/workspace-ui.js +3 -2
- package/dist/youiyoux-intake.d.ts +234 -0
- package/dist/youiyoux-intake.js +908 -0
- package/package.json +11 -8
package/dist/store.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { verifyResultImage } from "./chat-evidence.js";
|
|
2
|
+
import { parseFeedbackImage } from "./result-review.js";
|
|
1
3
|
import { mintSecret } from "./secret-mint.js";
|
|
2
4
|
import { WRITES_DEFAULT } from "./writes-mode.js";
|
|
3
5
|
import { checkAccessOf, widerThan } from "./check-access.js";
|
|
@@ -7,6 +9,8 @@ import { isAcpProfile, isAcpProvider } from "./acp-agents.js";
|
|
|
7
9
|
import { ACP_CONFORMANCE_VERSION, launchFromJson } from "./acp-attest.js";
|
|
8
10
|
import { reclaimDatabase } from "./database-reclaim.js";
|
|
9
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";
|
|
10
14
|
import { CHAT_PROVIDERS, CHAT_SCHEMA } from "./contracts/chat-tables.js";
|
|
11
15
|
import {} from "./chat-core.js";
|
|
12
16
|
import { ChatMessages, TELEGRAM_HOLD_REASONS, TELEGRAM_SKIPPED_ELSEWHERE, TELEGRAM_SKIPPED_OTHER_CHAT, TELEGRAM_SKIPPED_QUIET, TELEGRAM_UNSETTLED } from "./chat-messages.js";
|
|
@@ -92,6 +96,7 @@ import { LEDGER_COLUMNS, LEDGER_SCHEMA, LEDGER_TABLE, LEDGER_V99_TABLE, installL
|
|
|
92
96
|
import { PLAN_AUTO_SCHEMA } from "./plan-auto.js";
|
|
93
97
|
import { CONSOLE_INSTANCE_SCHEMA } from "./console-instance.js";
|
|
94
98
|
import { endUnattendedGrants, FLOW_UNATTENDED_SCHEMA, grantStandsSql } from "./flow-unattended.js";
|
|
99
|
+
import { FLOW_BROWSER_SCHEMA } from "./flow-browser.js";
|
|
95
100
|
import { RECIPE_SCHEMA } from "./recipes.js";
|
|
96
101
|
import { POLICY_SCHEMA, agentRefusal, approvalRefusal, policyParts, readPolicy, underCeiling, sessionCeilingRefusal } from "./policy.js";
|
|
97
102
|
import { PROVIDER_AUTH_SCHEMA } from "./provider-auth.js";
|
|
@@ -107,6 +112,8 @@ import { configBase, envValue, namedPath } from "./names.js";
|
|
|
107
112
|
import { PULL_REQUEST_SCHEMA } from "./pull-request-schema.js";
|
|
108
113
|
import { chatTurnAmountOf, combineReportedModels, providerTurnsOf, reportedCostUsdOf, reportedModelOf, runAmountOf } from "./reported-usage.js";
|
|
109
114
|
import { usageOf } from "./run-efficiency.js";
|
|
115
|
+
import { YOUIYOUX_FILED_VIA } from "./contracts/youiyoux-request.js";
|
|
116
|
+
import { shownTermsDigest } from "./chat-terms.js";
|
|
110
117
|
// PR #263 follow-up: the built-ins the Store money seal uses, captured as this module is evaluated, so code that later
|
|
111
118
|
// replaces `Object.defineProperty` and the like can't fool the seal or the lock (lockStoreMoneyMethods refuses when
|
|
112
119
|
// they've been replaced). The four sealed method references are recorded once, at the seal, in SEALED_MONEY_METHODS.
|
|
@@ -717,6 +724,60 @@ export const V141_ADDED_COLUMNS = Object.freeze({
|
|
|
717
724
|
},
|
|
718
725
|
chat_turn: { billing_reported: { definition: "TEXT CHECK (billing_reported IS NULL OR billing_reported IN ('subscription','api-key'))", saved: "NULL" } },
|
|
719
726
|
});
|
|
727
|
+
/** v142 (youiyoux-intake.ts): youiyoux change requests as tasks. `youiyoux_intake` is one row per project the intake
|
|
728
|
+
* has looked at: a person's on/off choice (`enabled` NULL: automatic, on while the project uses youiyoux) and the last
|
|
729
|
+
* pass's answer (whether the project uses youiyoux, how many request files couldn't be read, and the problem, if any).
|
|
730
|
+
* `youiyoux_request` is one row per request taken, keyed by project and request id: the task it became (NULL: left
|
|
731
|
+
* out, `note` says why), never filed again, the status last written beside it, with the console address its links
|
|
732
|
+
* named ('' for none), and where the person pointed (route, component, file:line), cleaned, for the task page. An
|
|
733
|
+
* options request (`kind`) also keeps the options file its agent writes (`proposal`, p-<its hex>), set when filed and
|
|
734
|
+
* never read from the request again: whether it went to the project's main checkout from which result (`proposal_state`,
|
|
735
|
+
* `proposal_run`, and why not in `proposal_note`; `taken`: a file was already there, so it was left and is never read
|
|
736
|
+
* for a decision), the digest of what was copied apart from what a decision changes (`proposal_digest`), and what a
|
|
737
|
+
* person decided on it in youiyoux's hub (`decided`, `decided_note`: what Toolroll did about it). New tables only. */
|
|
738
|
+
export const V142_SCHEMA = `
|
|
739
|
+
CREATE TABLE IF NOT EXISTS youiyoux_intake (
|
|
740
|
+
repo TEXT PRIMARY KEY,
|
|
741
|
+
enabled INTEGER CHECK (enabled IS NULL OR enabled IN (0, 1)),
|
|
742
|
+
uses INTEGER NOT NULL DEFAULT 0 CHECK (uses IN (0, 1)),
|
|
743
|
+
changed_by TEXT,
|
|
744
|
+
changed_at TEXT,
|
|
745
|
+
checked_at TEXT,
|
|
746
|
+
invalid INTEGER NOT NULL DEFAULT 0 CHECK (invalid >= 0),
|
|
747
|
+
problem TEXT
|
|
748
|
+
);
|
|
749
|
+
CREATE TABLE IF NOT EXISTS youiyoux_request (
|
|
750
|
+
repo TEXT NOT NULL,
|
|
751
|
+
request TEXT NOT NULL CHECK (length(request) = 12 AND request GLOB 'r-[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]'),
|
|
752
|
+
task_id TEXT,
|
|
753
|
+
note TEXT,
|
|
754
|
+
filed_at TEXT NOT NULL,
|
|
755
|
+
status TEXT CHECK (status IS NULL OR status IN ('queued','needs-you','building','ready','done','failed','stopped')),
|
|
756
|
+
detail TEXT,
|
|
757
|
+
status_at TEXT,
|
|
758
|
+
written_at TEXT,
|
|
759
|
+
written_base TEXT,
|
|
760
|
+
write_problem TEXT,
|
|
761
|
+
route TEXT,
|
|
762
|
+
component TEXT,
|
|
763
|
+
source_at TEXT,
|
|
764
|
+
kind TEXT NOT NULL DEFAULT 'change' CHECK (kind IN ('change','options')),
|
|
765
|
+
proposal TEXT CHECK (proposal IS NULL OR (length(proposal) = 12 AND proposal GLOB 'p-[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]')),
|
|
766
|
+
proposal_state TEXT CHECK (proposal_state IS NULL OR proposal_state IN ('copied','taken','missing','unsafe','retry')),
|
|
767
|
+
proposal_digest TEXT CHECK (proposal_digest IS NULL OR (length(proposal_digest) = 64 AND proposal_digest NOT GLOB '*[^0-9a-f]*')),
|
|
768
|
+
proposal_run INTEGER,
|
|
769
|
+
proposal_note TEXT,
|
|
770
|
+
proposal_at TEXT,
|
|
771
|
+
decided TEXT CHECK (decided IS NULL OR decided IN ('chosen','dismissed')),
|
|
772
|
+
decided_at TEXT,
|
|
773
|
+
decided_note TEXT,
|
|
774
|
+
PRIMARY KEY (repo, request),
|
|
775
|
+
UNIQUE (task_id),
|
|
776
|
+
CHECK ((kind = 'options') = (proposal IS NOT NULL)),
|
|
777
|
+
CHECK (proposal_state IS NULL OR kind = 'options'),
|
|
778
|
+
CHECK (decided IS NULL OR coalesce(proposal_state = 'copied' AND proposal_digest IS NOT NULL, 0))
|
|
779
|
+
);
|
|
780
|
+
`;
|
|
720
781
|
/** A project's "Show commands in the live view" (live-detail.ts): absent means off. */
|
|
721
782
|
export const LIVE_DETAIL_SCHEMA = `
|
|
722
783
|
CREATE TABLE IF NOT EXISTS live_detail_setting (
|
|
@@ -1030,7 +1091,17 @@ const REGISTERED_AT_FILING = "again, as it was filed";
|
|
|
1030
1091
|
// v141 adds, and changes no saved row: the eval set's records (docs/plans/evals-nightly.md, part C) — campaigns,
|
|
1031
1092
|
// attempts, each attempt's provider dispatches, the baseline pin and eval debt (V141_SCHEMA) — and how a run's or chat
|
|
1032
1093
|
// request's billing was known (V141_ADDED_COLUMNS; saved rows NULL: unknown).
|
|
1033
|
-
|
|
1094
|
+
// v142 adds, and changes no saved row: youiyoux change requests as tasks (V142_SCHEMA, new tables only) — each
|
|
1095
|
+
// project's intake choice and last answer, and each request taken, with the task it became.
|
|
1096
|
+
// v143 adds, and changes no saved row: optional exact-screenshot context on result feedback
|
|
1097
|
+
// (diff_comment.image_json, V143_ADDED_COLUMNS; saved notes read NULL and keep their file/line target).
|
|
1098
|
+
// v144 adds, and changes no saved row: a person's approval of browser use in a flow's script zones (flow_browser_grant;
|
|
1099
|
+
// flow-browser.ts), the one sandbox rule a project check gets for Chromium; saved flows have none and get nothing.
|
|
1100
|
+
// v145 adds, and changes no saved row (docs/plans/task-tidy.md): put-away tasks (task_archive, one row per archived task
|
|
1101
|
+
// version; no row is every saved task: not archived), the completed workflow runs on a watched branch the checks
|
|
1102
|
+
// trigger saw (branch_check_run), and the reservations that hold work back from being put away (merge_in_flight,
|
|
1103
|
+
// deploy_in_flight, flow_step_task; V145_SCHEMA, new tables only).
|
|
1104
|
+
export const SCHEMA_VERSION = 145;
|
|
1034
1105
|
/** v118: the tables merged into `credential`, in the order they move (a person's API tokens keep their ids first). */
|
|
1035
1106
|
export const V118_MERGED_TABLES = Object.freeze({ api_token: "person", coordinator_credential: "coordinator", lead_credential: "lead" });
|
|
1036
1107
|
/** The foreign keys that named a merged table, exactly as their DDL wrote them; each names `credential(id)` instead. */
|
|
@@ -1163,7 +1234,7 @@ const V115_DROPPED_REFERENCES = /\s+REFERENCES (?:contestant|contest|tournament_
|
|
|
1163
1234
|
* unchanged), and adds the nullable column in V125_ADDED_COLUMNS, checked the same way. v126 only adds a new table
|
|
1164
1235
|
* (task_source) and the columns in V126_ADDED_COLUMNS (null or 0), checked the same way.
|
|
1165
1236
|
*/
|
|
1166
|
-
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]);
|
|
1237
|
+
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]);
|
|
1167
1238
|
/** v119: the columns it adds, by table, with the value every row saved before it reads (toolroll-update's HISTORY_RULES). */
|
|
1168
1239
|
export const V119_ADDED_COLUMNS = Object.freeze({
|
|
1169
1240
|
task_ref: { planning_generation: { definition: "INTEGER NOT NULL DEFAULT 0", saved: "0" } },
|
|
@@ -1299,6 +1370,59 @@ export const V140_ADDED_COLUMNS = Object.freeze({
|
|
|
1299
1370
|
reported_cost_usd: { definition: "REAL CHECK (reported_cost_usd IS NULL OR reported_cost_usd >= 0)", saved: "NULL" },
|
|
1300
1371
|
},
|
|
1301
1372
|
});
|
|
1373
|
+
/** v143: optional screenshot context on result feedback; existing feedback stays unchanged (saved rows read NULL). */
|
|
1374
|
+
export const V143_ADDED_COLUMNS = Object.freeze({
|
|
1375
|
+
diff_comment: { image_json: { definition: "TEXT", saved: "NULL" } },
|
|
1376
|
+
});
|
|
1377
|
+
/** v145 (docs/plans/task-tidy.md): `task_archive` is a put-away task version. A row hides it from every list and count
|
|
1378
|
+
* and refuses every door that would dispatch, claim or approve it; restore deletes the row. Its history, results,
|
|
1379
|
+
* approvals and ledger stay exactly as they were. `merge_in_flight` is a direct merge between its last check and GitHub's
|
|
1380
|
+
* 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
|
|
1381
|
+
* longer counts). `deploy_in_flight` is a deployment whose candidate passed its gate (30 minutes, refreshed by every
|
|
1382
|
+
* phase that proves it again). `flow_step_task` is the tasks a flow step was admitted for, kept whatever the card does
|
|
1383
|
+
* next, so a step still running holds them. `branch_check_run` keeps each completed GitHub workflow run (pushed to
|
|
1384
|
+
* the watched branch of the watched repository) the "checks failed" trigger saw, green or red, so tidy can tell a
|
|
1385
|
+
* failure notice was fixed by a later commit. */
|
|
1386
|
+
export const V145_SCHEMA = `
|
|
1387
|
+
CREATE TABLE IF NOT EXISTS task_archive (
|
|
1388
|
+
task_ref INTEGER PRIMARY KEY REFERENCES task_ref(id),
|
|
1389
|
+
by TEXT NOT NULL,
|
|
1390
|
+
by_kind TEXT NOT NULL CHECK (by_kind IN ('person','tidy')),
|
|
1391
|
+
rule TEXT,
|
|
1392
|
+
reason TEXT NOT NULL,
|
|
1393
|
+
at TEXT NOT NULL
|
|
1394
|
+
);
|
|
1395
|
+
CREATE TABLE IF NOT EXISTS branch_check_run (
|
|
1396
|
+
repo TEXT NOT NULL,
|
|
1397
|
+
run_id INTEGER NOT NULL,
|
|
1398
|
+
branch TEXT NOT NULL,
|
|
1399
|
+
workflow TEXT NOT NULL,
|
|
1400
|
+
head_sha TEXT NOT NULL,
|
|
1401
|
+
conclusion TEXT NOT NULL CHECK (conclusion IN ('success','failure')),
|
|
1402
|
+
run_created_at TEXT NOT NULL,
|
|
1403
|
+
observed_at TEXT NOT NULL,
|
|
1404
|
+
PRIMARY KEY (repo, run_id)
|
|
1405
|
+
);
|
|
1406
|
+
CREATE INDEX IF NOT EXISTS branch_check_run_by_workflow ON branch_check_run (repo, branch, workflow, run_created_at);
|
|
1407
|
+
CREATE TABLE IF NOT EXISTS merge_in_flight (
|
|
1408
|
+
publication INTEGER PRIMARY KEY REFERENCES publication(id),
|
|
1409
|
+
task_ref INTEGER NOT NULL REFERENCES task_ref(id),
|
|
1410
|
+
token TEXT NOT NULL,
|
|
1411
|
+
by TEXT NOT NULL,
|
|
1412
|
+
at TEXT NOT NULL
|
|
1413
|
+
);
|
|
1414
|
+
CREATE TABLE IF NOT EXISTS deploy_in_flight (
|
|
1415
|
+
task_ref INTEGER PRIMARY KEY REFERENCES task_ref(id),
|
|
1416
|
+
run INTEGER NOT NULL,
|
|
1417
|
+
at TEXT NOT NULL
|
|
1418
|
+
);
|
|
1419
|
+
CREATE TABLE IF NOT EXISTS flow_step_task (
|
|
1420
|
+
card INTEGER NOT NULL,
|
|
1421
|
+
entry INTEGER NOT NULL,
|
|
1422
|
+
task_id TEXT NOT NULL,
|
|
1423
|
+
PRIMARY KEY (card, entry, task_id)
|
|
1424
|
+
);
|
|
1425
|
+
`;
|
|
1302
1426
|
/**
|
|
1303
1427
|
* v135: one approval per release (docs/plans/release-once.md). `release_authority` is the single source of truth for a
|
|
1304
1428
|
* release a person approved: one row per generation, changed only by compare-and-swap on `rev`, with a lease whose
|
|
@@ -4549,6 +4673,17 @@ CREATE INDEX IF NOT EXISTS run_checkpoint_by_task ON run_checkpoint (task_ref, i
|
|
|
4549
4673
|
for (const [column, { definition }] of Object.entries(columns))
|
|
4550
4674
|
addColumn(db, table, column, definition);
|
|
4551
4675
|
}
|
|
4676
|
+
// v142: youiyoux change requests as tasks (new tables only).
|
|
4677
|
+
db.exec(V142_SCHEMA);
|
|
4678
|
+
// v143: optional exact-screenshot context on result feedback (V143_ADDED_COLUMNS: saved notes read NULL).
|
|
4679
|
+
for (const [table, columns] of Object.entries(V143_ADDED_COLUMNS)) {
|
|
4680
|
+
for (const [column, { definition }] of Object.entries(columns))
|
|
4681
|
+
addColumn(db, table, column, definition);
|
|
4682
|
+
}
|
|
4683
|
+
// v144: approvals of browser use in flows' script zones (a new table only).
|
|
4684
|
+
db.exec(FLOW_BROWSER_SCHEMA);
|
|
4685
|
+
// v145: put-away tasks, green checks per branch and the reservations that hold work back (new tables only).
|
|
4686
|
+
db.exec(V145_SCHEMA);
|
|
4552
4687
|
// Keep the epoch until reclamation succeeds, including a retry after compaction already committed. A new file has
|
|
4553
4688
|
// nothing to reclaim.
|
|
4554
4689
|
if (!fresh)
|
|
@@ -6288,6 +6423,7 @@ export class Store {
|
|
|
6288
6423
|
FROM task JOIN task_ref ON task_ref.backend = ? AND task_ref.external_id = task.id
|
|
6289
6424
|
WHERE task.state = 'queued'
|
|
6290
6425
|
AND (? IS NULL OR task_ref.repo IS NULL OR task_ref.repo = ?)
|
|
6426
|
+
AND NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref = task_ref.id)
|
|
6291
6427
|
ORDER BY task.priority DESC, task.created_at, task_ref.id
|
|
6292
6428
|
LIMIT 200`)
|
|
6293
6429
|
.all(stamp, BUILT_IN, repo, repo)
|
|
@@ -6312,7 +6448,8 @@ export class Store {
|
|
|
6312
6448
|
.prepare(`SELECT task.priority AS priority, task.created_at AS created_at, task_ref.id AS ref,
|
|
6313
6449
|
task_ref.repo AS repo, task_ref.assigned_runner AS assigned
|
|
6314
6450
|
FROM task JOIN task_ref ON task_ref.backend = ? AND task_ref.external_id = task.id
|
|
6315
|
-
WHERE task.id = ? AND task.state = 'queued'
|
|
6451
|
+
WHERE task.id = ? AND task.state = 'queued'
|
|
6452
|
+
AND NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref = task_ref.id)`)
|
|
6316
6453
|
.get(BUILT_IN, taskId);
|
|
6317
6454
|
if (me === undefined)
|
|
6318
6455
|
return null;
|
|
@@ -6324,7 +6461,8 @@ export class Store {
|
|
|
6324
6461
|
OR (task.priority = ? AND task.created_at = ? AND task_ref.id < ?)
|
|
6325
6462
|
THEN 1 ELSE 0 END) AS ahead
|
|
6326
6463
|
FROM task JOIN task_ref ON task_ref.backend = ? AND task_ref.external_id = task.id
|
|
6327
|
-
WHERE task.state = 'queued' AND (task_ref.assigned_runner IS ?) AND (task_ref.repo IS ?)
|
|
6464
|
+
WHERE task.state = 'queued' AND (task_ref.assigned_runner IS ?) AND (task_ref.repo IS ?)
|
|
6465
|
+
AND NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref = task_ref.id)`)
|
|
6328
6466
|
.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"]);
|
|
6329
6467
|
return {
|
|
6330
6468
|
position: Number(row?.["ahead"] ?? 0) + 1,
|
|
@@ -6474,6 +6612,8 @@ export class Store {
|
|
|
6474
6612
|
JOIN task_ref ON task_ref.backend = ? AND task_ref.external_id = task.id
|
|
6475
6613
|
WHERE task.state = 'queued'
|
|
6476
6614
|
${reservation}
|
|
6615
|
+
-- A put-away task (v145) never dispatches.
|
|
6616
|
+
AND NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref = task_ref.id)
|
|
6477
6617
|
AND NOT EXISTS (
|
|
6478
6618
|
SELECT 1 FROM hold
|
|
6479
6619
|
WHERE hold.task_ref = task_ref.id
|
|
@@ -7255,6 +7395,9 @@ export class Store {
|
|
|
7255
7395
|
// road it came by.
|
|
7256
7396
|
if (basis === undefined && machineActing())
|
|
7257
7397
|
return false;
|
|
7398
|
+
// A PUT-AWAY TASK (v145, task-tidy.md) is never approved, by any road: restore it first.
|
|
7399
|
+
if (taskArchived(this, taskId))
|
|
7400
|
+
return false;
|
|
7258
7401
|
// A RELEASE WAITING FOR CI (Codex review round 1, finding 4): filed while its pull request's checks still ran,
|
|
7259
7402
|
// only the automatic road (which proves CI itself) seals it until the plane read exact-head CI passing.
|
|
7260
7403
|
if (basis?.kind !== "auto-check" && basis?.kind !== "auto-green" && releaseCiHoldOf(this, taskId) !== null)
|
|
@@ -7282,6 +7425,14 @@ export class Store {
|
|
|
7282
7425
|
.get(taskId);
|
|
7283
7426
|
if (writtenByLead !== undefined)
|
|
7284
7427
|
return false;
|
|
7428
|
+
// THE OUTSIDE-REQUEST QUARANTINE (youiyoux-intake.ts): a task filed from a youiyoux change request is text
|
|
7429
|
+
// anyone who can write the project's folder wrote, so only a person who read every word of it approves it.
|
|
7430
|
+
// Its provenance is immutable, so no road around this exists. A mode, a grant, a schedule, auto-check and
|
|
7431
|
+
// auto-green (approvals no person read) never seal it. A person's own ceremony does. So does their paired-
|
|
7432
|
+
// chat tap, only with the digest of every word the card showed (`terms`, chat-terms.ts), recomputed below
|
|
7433
|
+
// from the live scope: a card that cut a word, or terms that changed since, seal nothing.
|
|
7434
|
+
if (basis.kind !== "chat" && this.filedViaOf(taskId) === YOUIYOUX_FILED_VIA)
|
|
7435
|
+
return false;
|
|
7285
7436
|
// CHECK ACCESS (v132, check-access.ts): a scope whose checks reach beyond the fence (sign-ins, or no sandbox at
|
|
7286
7437
|
// all) seals only on a person's password ceremony — never a mode, a grant, a chat confirmation or a carry.
|
|
7287
7438
|
const profileText = this.db.prepare("SELECT profile_json FROM task_scope WHERE task_id = ?").get(taskId)?.["profile_json"];
|
|
@@ -7348,6 +7499,10 @@ export class Store {
|
|
|
7348
7499
|
return false;
|
|
7349
7500
|
if (deployGate && basis?.kind === "chat" && !(basis.card === "release" && typeof basis.cardDigest === "string" && /^[0-9a-f]{64}$/.test(basis.cardDigest)))
|
|
7350
7501
|
return false;
|
|
7502
|
+
// The outside-request quarantine's chat road (above): the card showed every word of these live terms.
|
|
7503
|
+
if (basis?.kind === "chat" && this.filedViaOf(taskId) === YOUIYOUX_FILED_VIA
|
|
7504
|
+
&& !(basis.card === "terms" && typeof basis.cardDigest === "string" && basis.cardDigest === shownTermsDigest(readScope(working))))
|
|
7505
|
+
return false;
|
|
7351
7506
|
// AUTOMATIC RELEASE SEALS (auto-approve-green.md): only on a release task (a candidate and release terms signed
|
|
7352
7507
|
// in), only naming the evidence row the plane wrote for exactly this task, kind, digest and candidate.
|
|
7353
7508
|
const automatic = basis?.kind === "auto-check" || basis?.kind === "auto-green";
|
|
@@ -9535,13 +9690,21 @@ export class Store {
|
|
|
9535
9690
|
if (artifact === undefined || artifact.kind !== "terminal-diff") {
|
|
9536
9691
|
throw new Error(`artifact ${args.artifactId} is not run ${args.runId}'s terminal diff — comments bind to the exact bytes reviewed`);
|
|
9537
9692
|
}
|
|
9693
|
+
const image = parseFeedbackImage(args.image);
|
|
9694
|
+
if (image === false)
|
|
9695
|
+
throw new Error("Invalid feedback image.");
|
|
9696
|
+
if (image !== null) {
|
|
9697
|
+
const shot = this.artifactForRun(args.runId, image.artifact);
|
|
9698
|
+
if (image.run !== args.runId || shot?.kind !== "screenshot" || shot.sha256 !== image.sha256)
|
|
9699
|
+
throw new Error("The feedback image does not belong to this result.");
|
|
9700
|
+
}
|
|
9538
9701
|
// The unique index is the real concurrent guard (audit C-9): a race on
|
|
9539
9702
|
// the same source key lands in DO NOTHING, not in a thrown constraint.
|
|
9540
9703
|
const inserted = this.db
|
|
9541
|
-
.prepare(`INSERT INTO diff_comment (artifact, artifact_sha, run, path, line, note, author, created_at, source_key)
|
|
9542
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
9704
|
+
.prepare(`INSERT INTO diff_comment (artifact, artifact_sha, run, path, line, note, author, created_at, source_key, image_json)
|
|
9705
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
9543
9706
|
ON CONFLICT (source_key) WHERE source_key IS NOT NULL DO NOTHING`)
|
|
9544
|
-
.run(args.artifactId, artifact.sha256, args.runId, args.path, args.line, args.note, args.author, now.toISOString(), args.sourceKey ?? null);
|
|
9707
|
+
.run(args.artifactId, artifact.sha256, args.runId, args.path, args.line, args.note, args.author, now.toISOString(), args.sourceKey ?? null, image === null ? null : JSON.stringify(image));
|
|
9545
9708
|
return Number(inserted.changes) === 0 ? null : Number(inserted.lastInsertRowid);
|
|
9546
9709
|
}
|
|
9547
9710
|
/** Live (unsuperseded, unconsumed) comments on a run's terminal diff. */
|
|
@@ -10229,6 +10392,19 @@ export class Store {
|
|
|
10229
10392
|
const row = this.db.prepare("SELECT flow, card, card_entry, source_json FROM task_source WHERE task_id = ?").get(taskId);
|
|
10230
10393
|
return row === undefined ? null : { flow: Number(row["flow"]), card: Number(row["card"]), entry: Number(row["card_entry"]), sourceJson: String(row["source_json"]) };
|
|
10231
10394
|
}
|
|
10395
|
+
/** v142: where a task filed from a youiyoux change or options request came from, as kept when it was filed
|
|
10396
|
+
* (youiyoux-intake.ts; a revision reads its first version's): the request, its kind, and where the person pointed. Null for every other task. */
|
|
10397
|
+
youiyouxOriginOf(taskId) {
|
|
10398
|
+
const text = (value) => typeof value === "string" && value !== "" ? value : null;
|
|
10399
|
+
let id = taskId;
|
|
10400
|
+
for (let hop = 0; id !== null && hop < 20; hop++) {
|
|
10401
|
+
const row = this.db.prepare("SELECT request, kind, route, component, source_at FROM youiyoux_request WHERE task_id = ?").get(id);
|
|
10402
|
+
if (row !== undefined)
|
|
10403
|
+
return { request: String(row["request"]), kind: row["kind"] === "options" ? "options" : "change", route: text(row["route"]), component: text(row["component"]), at: text(row["source_at"]) };
|
|
10404
|
+
id = this.lookupRef(id)?.revisionOf ?? null;
|
|
10405
|
+
}
|
|
10406
|
+
return null;
|
|
10407
|
+
}
|
|
10232
10408
|
// ---- the revision boundary: ONE field-by-field policy -------------------
|
|
10233
10409
|
/**
|
|
10234
10410
|
* Seal a revision in ONE transaction (Codex M5-M8 audit, C-3; contract
|
|
@@ -10320,7 +10496,14 @@ export class Store {
|
|
|
10320
10496
|
if (ids.size !== args.commentIds.length || live.length !== ids.size) {
|
|
10321
10497
|
throw new RevisionRaced("this annotation batch is no longer whole — read the current comments before sealing it");
|
|
10322
10498
|
}
|
|
10323
|
-
const
|
|
10499
|
+
for (const one of live) {
|
|
10500
|
+
if (one.image == null)
|
|
10501
|
+
continue;
|
|
10502
|
+
const image = sourceRef.repo === null ? null : verifyResultImage(this, args.brief.evidenceRoot, [sourceRef.repo], { taskId: args.source.task, ...one.image });
|
|
10503
|
+
if (one.image.run !== sourceRun.id || image === null || !image.ok)
|
|
10504
|
+
return refuse("brief-custody", "A feedback screenshot is unavailable or changed. The notes were not consumed.");
|
|
10505
|
+
}
|
|
10506
|
+
const exactComment = (one) => ({ id: one["id"], path: one["path"], line: one["line"], note: one["note"], author: one["author"], createdAt: one["createdAt"], image: one["image"] ?? null });
|
|
10324
10507
|
const expected = live.map(one => exactComment({ ...one }));
|
|
10325
10508
|
const described = Array.isArray(named.comments) && named.comments.every(one => one !== null && typeof one === "object" && !Array.isArray(one))
|
|
10326
10509
|
? named.comments.map(one => exactComment(one)) : null;
|
|
@@ -10474,7 +10657,7 @@ export class Store {
|
|
|
10474
10657
|
versions.sort((a, b) => a.id === root.id ? -1 : b.id === root.id ? 1 : a.createdAt.localeCompare(b.createdAt) || a.refId - b.refId);
|
|
10475
10658
|
const current = versions.at(-1);
|
|
10476
10659
|
return { root, current, versions,
|
|
10477
|
-
otherActive: versions.filter(one => one.id !== current.id && (one.state === "queued" || one.state === "running")),
|
|
10660
|
+
otherActive: versions.filter(one => one.id !== current.id && (one.state === "queued" || one.state === "running") && !refArchived(this, one.refId)),
|
|
10478
10661
|
problem: Number(rows[0]["broken"]) === 0 ? null : "Task history is unavailable or incomplete. This task is shown separately." };
|
|
10479
10662
|
}
|
|
10480
10663
|
/** Group and filter BEFORE the family limit; hydrate only that page's
|
|
@@ -10484,6 +10667,8 @@ export class Store {
|
|
|
10484
10667
|
const order = options.order === "updated" ? "updated_at" : "created_at";
|
|
10485
10668
|
const rows = this.db.prepare(`${projection.sql}, selected AS MATERIALIZED (
|
|
10486
10669
|
SELECT * FROM ordered WHERE version_rank = 1
|
|
10670
|
+
-- A put-away family (v145) is out of every list, before the page is cut.
|
|
10671
|
+
AND NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref = ordered.ref_id)
|
|
10487
10672
|
${options.states === undefined ? "" : `AND state IN (${options.states.map(() => "?").join(",")})`}
|
|
10488
10673
|
ORDER BY ${order} DESC, ref_id DESC LIMIT ?
|
|
10489
10674
|
) SELECT task.*, members.repo, members.ref_id, members.root_ref, members.broken
|
|
@@ -10574,7 +10759,8 @@ export class Store {
|
|
|
10574
10759
|
COUNT(CASE WHEN live_families.root_ref IS NOT NULL THEN 1 END) AS running,
|
|
10575
10760
|
COUNT(CASE WHEN live_families.root_ref IS NULL AND ordered.state = 'queued' THEN 1 END) AS queued,
|
|
10576
10761
|
COUNT(CASE WHEN live_families.root_ref IS NULL AND ordered.state = 'done' AND ordered.updated_at >= ? THEN 1 END) AS doneRecently
|
|
10577
|
-
FROM ordered LEFT JOIN live_families ON live_families.root_ref = ordered.root_ref WHERE version_rank = 1
|
|
10762
|
+
FROM ordered LEFT JOIN live_families ON live_families.root_ref = ordered.root_ref WHERE version_rank = 1
|
|
10763
|
+
AND NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref = ordered.ref_id)`)
|
|
10578
10764
|
.get(...projection.params, JSON.stringify(liveTasks), new Date(now.getTime() - 24 * 60 * 60_000).toISOString());
|
|
10579
10765
|
return { running: Number(row["running"]), queued: Number(row["queued"]), doneRecently: Number(row["doneRecently"]) };
|
|
10580
10766
|
}
|
|
@@ -11008,6 +11194,9 @@ export class Store {
|
|
|
11008
11194
|
if (ref === undefined)
|
|
11009
11195
|
return { ok: false, reason: "unknown-task" };
|
|
11010
11196
|
const taskRef = Number(ref["id"]);
|
|
11197
|
+
// A put-away task (v145) is restored before it runs again.
|
|
11198
|
+
if (refArchived(this, taskRef))
|
|
11199
|
+
return { ok: false, reason: "archived" };
|
|
11011
11200
|
const stamp = now.toISOString();
|
|
11012
11201
|
if (this.hasLiveClaim(taskRef, now))
|
|
11013
11202
|
return { ok: false, reason: "claimed" };
|
|
@@ -12619,10 +12808,12 @@ export class Store {
|
|
|
12619
12808
|
: source !== undefined && "project" in source && source.project !== "" ? "project"
|
|
12620
12809
|
: source !== undefined && "installation" in source ? "installation" : "unknown";
|
|
12621
12810
|
const project = scope === "task" ? String(ref["repo"]) : scope === "project" && source !== undefined && "project" in source ? source.project : null;
|
|
12811
|
+
// A put-away task (v145) nudges nobody: no notification about it is enqueued, whoever produced it. The check is
|
|
12812
|
+
// part of the insert itself, so a put away that commits first always wins.
|
|
12622
12813
|
const { changes, lastInsertRowid } = this.db
|
|
12623
12814
|
.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)
|
|
12624
|
-
|
|
12625
|
-
.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);
|
|
12815
|
+
SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? WHERE NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref = ?)`)
|
|
12816
|
+
.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"]));
|
|
12626
12817
|
// Whose act made this fact: pings follow it (see pingAllowed).
|
|
12627
12818
|
const actor = currentActor();
|
|
12628
12819
|
if (Number(changes) > 0 && actor !== null) {
|
|
@@ -14114,6 +14305,7 @@ export class Store {
|
|
|
14114
14305
|
JOIN task_ref ON task_ref.backend = ? AND task_ref.external_id = task.id
|
|
14115
14306
|
WHERE (? IS NULL OR task_ref.repo IS NULL OR task_ref.repo = ?)
|
|
14116
14307
|
AND (? IS NULL OR task.state = ?)
|
|
14308
|
+
AND NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref = task_ref.id)
|
|
14117
14309
|
ORDER BY task.created_at DESC, task.id DESC LIMIT ?
|
|
14118
14310
|
)
|
|
14119
14311
|
UNION
|
|
@@ -14282,8 +14474,10 @@ export class Store {
|
|
|
14282
14474
|
resolvePlanRevision(id, outcome, resolvedBy, now) {
|
|
14283
14475
|
const { changes } = this.db
|
|
14284
14476
|
.prepare(`UPDATE plan_revision SET status = ?, resolved_at = ?, resolved_by = ?
|
|
14285
|
-
WHERE id = ? AND status = 'blocked'
|
|
14286
|
-
|
|
14477
|
+
WHERE id = ? AND status = 'blocked'
|
|
14478
|
+
-- A put-away task's (v145) plan change is never applied; rejecting it still is.
|
|
14479
|
+
AND (? = 'rejected' OR NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref = plan_revision.task_ref))`)
|
|
14480
|
+
.run(outcome, now.toISOString(), resolvedBy, id, outcome);
|
|
14287
14481
|
return Number(changes) > 0;
|
|
14288
14482
|
}
|
|
14289
14483
|
/** Stamp the exact plan revision and authority snapshot a run's brief was
|
|
@@ -15513,6 +15707,12 @@ export class Store {
|
|
|
15513
15707
|
if (row === undefined || String(row["state"]) !== "claimed" || Number(row["generation"]) !== generation) {
|
|
15514
15708
|
return { ok: false, outcome: "raced", why: "the claim moved" };
|
|
15515
15709
|
}
|
|
15710
|
+
// A put-away task (v145) merges nothing: the intent waits for a person, who restores it first.
|
|
15711
|
+
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"]));
|
|
15712
|
+
if (archivedTask !== undefined) {
|
|
15713
|
+
this.settleMergeIntent(id, generation, "waiting-human", now, { error: "the task is put away", from: "claimed", bumpGeneration: true });
|
|
15714
|
+
return { ok: false, outcome: "waiting-human", why: "the task is put away" };
|
|
15715
|
+
}
|
|
15516
15716
|
const supersede = (why) => {
|
|
15517
15717
|
this.settleMergeIntent(id, generation, "superseded", now, { error: why, from: "claimed" });
|
|
15518
15718
|
return { ok: false, outcome: "superseded", why };
|
|
@@ -17271,9 +17471,10 @@ export class Store {
|
|
|
17271
17471
|
const page = Math.max(1, Math.min(Math.floor(limit), 50));
|
|
17272
17472
|
// Roll-up admission binds BEFORE the LIMIT (Codex roll-up review,
|
|
17273
17473
|
// finding 2): a repo outside the ceiling must not consume the page.
|
|
17274
|
-
|
|
17474
|
+
// A put-away task (v145, task-tidy.md) is left out here, as from every list and count.
|
|
17475
|
+
const admission = (admitted === null
|
|
17275
17476
|
? "(? IS NULL OR task_ref.repo IS NULL OR task_ref.repo = ?)"
|
|
17276
|
-
: `(task_ref.repo IS NULL${admitted.length === 0 ? "" : ` OR task_ref.repo IN (${admitted.map(() => "?").join(",")})`})
|
|
17477
|
+
: `(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)";
|
|
17277
17478
|
const params = admitted === null ? [repo, repo] : admitted;
|
|
17278
17479
|
return this.db
|
|
17279
17480
|
.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
|
|
@@ -17303,9 +17504,10 @@ export class Store {
|
|
|
17303
17504
|
*/
|
|
17304
17505
|
listRequeueablesScoped(repo, now, limit = 10, admitted = null) {
|
|
17305
17506
|
const page = Math.max(1, Math.min(Math.floor(limit), 50));
|
|
17306
|
-
|
|
17507
|
+
// A put-away task (v145, task-tidy.md) is left out here, as from every list and count.
|
|
17508
|
+
const admission = (admitted === null
|
|
17307
17509
|
? "(? IS NULL OR task_ref.repo IS NULL OR task_ref.repo = ?)"
|
|
17308
|
-
: `(task_ref.repo IS NULL${admitted.length === 0 ? "" : ` OR task_ref.repo IN (${admitted.map(() => "?").join(",")})`})
|
|
17510
|
+
: `(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)";
|
|
17309
17511
|
const params = admitted === null ? [repo, repo] : admitted;
|
|
17310
17512
|
return this.db
|
|
17311
17513
|
.prepare(`WITH open_incidents AS (
|
|
@@ -17369,6 +17571,8 @@ export class Store {
|
|
|
17369
17571
|
JOIN task_ref AS blocker_ref ON blocker_ref.backend = ? AND blocker_ref.external_id = task_edge.blocker
|
|
17370
17572
|
WHERE ${admissionOf("dependent_ref")}
|
|
17371
17573
|
AND ${admissionOf("blocker_ref")}
|
|
17574
|
+
-- Put-away tasks (v145) need no repair card, on either side.
|
|
17575
|
+
AND NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref IN (dependent_ref.id, blocker_ref.id))
|
|
17372
17576
|
GROUP BY task_edge.blocker, dependent_ref.repo, blocker_ref.repo
|
|
17373
17577
|
ORDER BY task_edge.blocker LIMIT ?`)
|
|
17374
17578
|
.all(BUILT_IN, BUILT_IN, ...paramsOf(), ...paramsOf(), page)
|
|
@@ -17401,6 +17605,7 @@ export class Store {
|
|
|
17401
17605
|
WHERE task.state = 'done'
|
|
17402
17606
|
AND (? IS NULL OR task_ref.repo IS NULL OR task_ref.repo = ?)
|
|
17403
17607
|
${admission}
|
|
17608
|
+
AND NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref = task_ref.id)
|
|
17404
17609
|
)
|
|
17405
17610
|
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,
|
|
17406
17611
|
publication.state AS pub_state, publication.pr_number, publication.pr_url,
|
|
@@ -17507,9 +17712,10 @@ export class Store {
|
|
|
17507
17712
|
// finding 11): in the roll-up, rows outside the ceiling must not
|
|
17508
17713
|
// consume the page and crowd out admitted cards. Serve still filters
|
|
17509
17714
|
// per-row afterward — this is the bound, that is the law.
|
|
17510
|
-
|
|
17715
|
+
// A put-away task (v145, task-tidy.md) is left out here, as from every list and count.
|
|
17716
|
+
const admission = (admitted === null
|
|
17511
17717
|
? "(? IS NULL OR task_ref.repo IS NULL OR task_ref.repo = ?)"
|
|
17512
|
-
: `(task_ref.repo IS NULL OR task_ref.repo IN (${admitted.map(() => "?").join(",")}))
|
|
17718
|
+
: `(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)";
|
|
17513
17719
|
const admissionParams = admitted === null ? [repo, repo] : admitted;
|
|
17514
17720
|
const columns = `
|
|
17515
17721
|
SELECT task.id, task.title, task.state, task.updated_at, task.priority, task_ref.strikes, task_ref.repo AS task_repo,
|
|
@@ -17713,14 +17919,15 @@ export class Store {
|
|
|
17713
17919
|
SUM(CASE WHEN task.state = 'queued' THEN 1 ELSE 0 END) AS queued,
|
|
17714
17920
|
SUM(CASE WHEN task.state = 'running' THEN 1 ELSE 0 END) AS running
|
|
17715
17921
|
FROM task JOIN task_ref ON task_ref.backend = 'built-in' AND task_ref.external_id = task.id
|
|
17716
|
-
WHERE task_ref.repo =
|
|
17922
|
+
WHERE task_ref.repo = ? AND NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref = task_ref.id)`)
|
|
17717
17923
|
.get(repo);
|
|
17718
17924
|
const since = new Date(now.getTime() - 24 * 60 * 60_000).toISOString();
|
|
17719
17925
|
const done = this.db
|
|
17720
17926
|
.prepare(`SELECT COUNT(*) AS n FROM run
|
|
17721
17927
|
JOIN task_ref ON task_ref.id = run.task_ref
|
|
17722
17928
|
WHERE task_ref.repo = ? AND run.role IN ('builder','scout')
|
|
17723
|
-
AND run.outcome IN ('built','no-change') AND run.finished_at IS NOT NULL AND run.finished_at >=
|
|
17929
|
+
AND run.outcome IN ('built','no-change') AND run.finished_at IS NOT NULL AND run.finished_at >= ?
|
|
17930
|
+
AND NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref = task_ref.id)`)
|
|
17724
17931
|
.get(repo, since);
|
|
17725
17932
|
return { waiting: waiting, queued: Number(q.queued ?? 0), running: Number(q.running ?? 0), doneRecently: Number(done.n) };
|
|
17726
17933
|
}
|
|
@@ -17729,9 +17936,10 @@ export class Store {
|
|
|
17729
17936
|
// review, finding 11): with repo NULL the old predicate admitted every
|
|
17730
17937
|
// stored project, ceiling or not. An empty admitted list reduces to
|
|
17731
17938
|
// unplaced rows only — never IN ().
|
|
17732
|
-
|
|
17939
|
+
// A put-away task (v145, task-tidy.md) is left out here, as from every list and count.
|
|
17940
|
+
const admission = (admitted === null
|
|
17733
17941
|
? "(? IS NULL OR task_ref.repo IS NULL OR task_ref.repo = ?)"
|
|
17734
|
-
: `(task_ref.repo IS NULL${admitted.length === 0 ? "" : ` OR task_ref.repo IN (${admitted.map(() => "?").join(",")})`})
|
|
17942
|
+
: `(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)";
|
|
17735
17943
|
const params = () => (admitted === null ? [repo, repo] : [...admitted]);
|
|
17736
17944
|
const row = this.db
|
|
17737
17945
|
.prepare(`SELECT COUNT(*) AS n FROM (
|
|
@@ -18584,6 +18792,8 @@ export class Store {
|
|
|
18584
18792
|
*/
|
|
18585
18793
|
resumeRunStop(args, now) {
|
|
18586
18794
|
return this.transact(() => {
|
|
18795
|
+
if (refArchived(this, args.taskRef))
|
|
18796
|
+
return { ok: false, reason: "archived", detail: "this task is put away — restore it first" };
|
|
18587
18797
|
const stop = this.stopOf(args.runId);
|
|
18588
18798
|
const run = this.getRun(args.runId);
|
|
18589
18799
|
if (stop === null || run === null)
|
|
@@ -20632,6 +20842,7 @@ function readDiffComment(row) {
|
|
|
20632
20842
|
id: Number(row["id"]),
|
|
20633
20843
|
artifact: Number(row["artifact"]),
|
|
20634
20844
|
artifactSha: String(row["artifact_sha"]),
|
|
20845
|
+
image: readFeedbackImage(row["image_json"]),
|
|
20635
20846
|
run: Number(row["run"]),
|
|
20636
20847
|
path: row["path"] === null ? null : String(row["path"]),
|
|
20637
20848
|
line: row["line"] === null ? null : Number(row["line"]),
|
|
@@ -21510,3 +21721,153 @@ export function lockStoreMoneyMethods(store, prototype = Store.prototype) {
|
|
|
21510
21721
|
throw new Error(`Store.${name} couldn't be locked`);
|
|
21511
21722
|
}
|
|
21512
21723
|
}
|
|
21724
|
+
/** A saved note's screenshot reference (v143), or null; a damaged one refuses rather than reading as none. */
|
|
21725
|
+
function readFeedbackImage(value) {
|
|
21726
|
+
const image = parseFeedbackImage(value);
|
|
21727
|
+
if (image === false)
|
|
21728
|
+
throw new Error("The saved feedback image is damaged.");
|
|
21729
|
+
return image;
|
|
21730
|
+
}
|
|
21731
|
+
/** Takes a saved note's screenshot off without losing the note or its history (v143). Fail closed, in one write
|
|
21732
|
+
* transaction: the note must be live and unconsumed on this run, its screenshot must be genuinely unusable right now
|
|
21733
|
+
* (`unusable` is result-actions.ts feedbackImageProblem, re-read inside the transaction, never the page's say-so), and
|
|
21734
|
+
* the change is recorded in the action ledger under the person who made it. The note is superseded by an exact
|
|
21735
|
+
* imageless copy; the original stays in history. The caller has already proved the person may request changes on
|
|
21736
|
+
* this run (the same authority that seals any saved note into a revision); that standing is re-proved here too. */
|
|
21737
|
+
export function dropFeedbackImage(store, runId, commentId, actor, unusable, now) {
|
|
21738
|
+
return store.transact(() => {
|
|
21739
|
+
// The person's standing is re-read under the write lock: a revocation, a downgrade or a lost project committed
|
|
21740
|
+
// after the request was admitted refuses here (check and write together).
|
|
21741
|
+
const run = store.getRun(runId);
|
|
21742
|
+
const repo = run === null ? null : store.refById(run.taskRef)?.repo ?? null;
|
|
21743
|
+
const account = store.accountOf(actor);
|
|
21744
|
+
if (run === null || account === null || account.revokedAt !== null || account.role !== "approver" || !store.accountCanAccess(actor, repo)) {
|
|
21745
|
+
return { ok: false, reason: "not-authorized" };
|
|
21746
|
+
}
|
|
21747
|
+
const live = store.handle.prepare(`SELECT id, image_json FROM diff_comment WHERE id = ? AND run = ? AND superseded_by IS NULL AND consumed_by IS NULL
|
|
21748
|
+
AND reviewer_run IS NULL AND image_json IS NOT NULL`).get(commentId, runId);
|
|
21749
|
+
if (live === undefined)
|
|
21750
|
+
return { ok: false, reason: "not-found" };
|
|
21751
|
+
const image = readFeedbackImage(live["image_json"]);
|
|
21752
|
+
if (image === null)
|
|
21753
|
+
return { ok: false, reason: "not-found" };
|
|
21754
|
+
const problem = unusable(image);
|
|
21755
|
+
if (problem === null)
|
|
21756
|
+
return { ok: false, reason: "image-usable" };
|
|
21757
|
+
const copy = store.handle.prepare(`INSERT INTO diff_comment (artifact, artifact_sha, run, path, line, note, author, created_at, reviewer_run, severity)
|
|
21758
|
+
SELECT artifact, artifact_sha, run, path, line, note, author, ?, reviewer_run, severity FROM diff_comment WHERE id = ?`).run(now.toISOString(), commentId);
|
|
21759
|
+
const id = Number(copy.lastInsertRowid);
|
|
21760
|
+
const moved = store.handle.prepare("UPDATE diff_comment SET superseded_by = ? WHERE id = ? AND superseded_by IS NULL AND consumed_by IS NULL").run(id, commentId);
|
|
21761
|
+
if (Number(moved.changes) !== 1)
|
|
21762
|
+
throw new Error("The saved note changed while its screenshot was being removed.");
|
|
21763
|
+
store.recordAction({ at: now.toISOString(), actor, repo,
|
|
21764
|
+
taskId: store.externalIdFor(run.taskRef) ?? null, runId, action: "feedback screenshot removed", outcome: "recorded", source: "work",
|
|
21765
|
+
detail: `note ${commentId} kept as note ${id} without screenshot ${image.artifact}: ${problem}` });
|
|
21766
|
+
return { ok: true, id };
|
|
21767
|
+
});
|
|
21768
|
+
}
|
|
21769
|
+
/** The put-away record of this task, or null when it is not put away. */
|
|
21770
|
+
export function archiveOf(store, taskId) {
|
|
21771
|
+
const row = store.handle.prepare(`SELECT a.*, r.external_id FROM task_archive a JOIN task_ref r ON r.id = a.task_ref
|
|
21772
|
+
WHERE r.backend = 'built-in' AND r.external_id = ?`).get(taskId);
|
|
21773
|
+
return row === undefined ? null : { taskRef: Number(row["task_ref"]), taskId: String(row["external_id"]), by: String(row["by"]),
|
|
21774
|
+
byKind: row["by_kind"] === "tidy" ? "tidy" : "person", rule: row["rule"] == null ? null : String(row["rule"]), reason: String(row["reason"]), at: String(row["at"]) };
|
|
21775
|
+
}
|
|
21776
|
+
/** Every version of a task's family, by the same proven lineage the task page reads (`taskFamilyOf`: same project, each
|
|
21777
|
+
* revision bound to its parent's own run by its brief). A family whose lineage doesn't prove is just this task. */
|
|
21778
|
+
export function taskFamilyRefs(store, taskId) {
|
|
21779
|
+
const family = store.taskFamilyOf(taskId, null, true);
|
|
21780
|
+
return family === null ? [] : family.versions.map(one => ({ id: one.refId, taskId: one.id })).sort((a, b) => a.id - b.id);
|
|
21781
|
+
}
|
|
21782
|
+
/** The first family version with something still going on, or null: running, a live newest claim, an unfinished run,
|
|
21783
|
+
* a process Toolroll hasn't seen exit, a build whose processes were never recorded, a stop still settling, a follow-up
|
|
21784
|
+
* check asked for and not finished, a flow step running for its card, a release in flight (a live release approval
|
|
21785
|
+
* that names it, or a deploy or merge reserved, running or unknown), or a pull request merging (an intent claimed or
|
|
21786
|
+
* firing, or a person's merge between its last check and GitHub's answer). */
|
|
21787
|
+
export function liveFamilyAttempt(store, refs, now) {
|
|
21788
|
+
if (refs.length === 0)
|
|
21789
|
+
return null;
|
|
21790
|
+
const row = store.handle.prepare(`SELECT r.external_id FROM task_ref r JOIN task t ON t.id = r.external_id
|
|
21791
|
+
WHERE r.id IN (SELECT value FROM json_each(?)) AND (t.state = 'running'
|
|
21792
|
+
OR EXISTS (SELECT 1 FROM claim c WHERE c.task_ref = r.id AND c.released_at IS NULL AND c.expires_at > ?
|
|
21793
|
+
AND c.lease_generation = (SELECT MAX(n.lease_generation) FROM claim n WHERE n.task_ref = r.id))
|
|
21794
|
+
OR EXISTS (SELECT 1 FROM run x WHERE x.task_ref = r.id AND x.outcome IS NULL)
|
|
21795
|
+
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)
|
|
21796
|
+
OR EXISTS (SELECT 1 FROM run x WHERE x.task_ref = r.id AND x.provider_started_at IS NOT NULL
|
|
21797
|
+
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))
|
|
21798
|
+
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)
|
|
21799
|
+
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'
|
|
21800
|
+
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 || ':%'))
|
|
21801
|
+
OR EXISTS (SELECT 1 FROM flow_card c JOIN flow_step_run s ON s.card = c.id
|
|
21802
|
+
WHERE s.state = 'running' AND (c.task = r.external_id OR c.primary_task = r.external_id))
|
|
21803
|
+
OR EXISTS (SELECT 1 FROM flow_step_task t JOIN flow_step_run s ON s.card = t.card AND s.entry = t.entry
|
|
21804
|
+
WHERE s.state = 'running' AND t.task_id = r.external_id)
|
|
21805
|
+
OR EXISTS (SELECT 1 FROM deploy_in_flight d WHERE d.task_ref = r.id AND d.at > ?)
|
|
21806
|
+
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))
|
|
21807
|
+
OR EXISTS (SELECT 1 FROM release_operation o WHERE o.task_id = r.external_id AND o.state IN ('reserved','running','unknown'))
|
|
21808
|
+
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'))
|
|
21809
|
+
OR EXISTS (SELECT 1 FROM merge_in_flight m WHERE m.task_ref = r.id AND m.at > ?))
|
|
21810
|
+
LIMIT 1`).get(JSON.stringify(refs), now.toISOString(), new Date(now.getTime() - 30 * 60_000).toISOString(), new Date(now.getTime() - 15 * 60_000).toISOString());
|
|
21811
|
+
return row === undefined ? null : String(row["external_id"]);
|
|
21812
|
+
}
|
|
21813
|
+
/** Puts a task away (v145): every version of its family, in one write transaction that re-checks it has no live
|
|
21814
|
+
* attempt, that its versions are exactly `expect` when the caller showed a person a family (a preview), and that
|
|
21815
|
+
* `guard` (the caller's own rule, re-read under the lock) still allows it. Its state, history, results, approvals and
|
|
21816
|
+
* ledger are untouched; its open notifications are resolved; one ledger row records who and why. */
|
|
21817
|
+
export function archiveTaskFamily(store, taskId, by, now, options = {}) {
|
|
21818
|
+
return store.transact(() => {
|
|
21819
|
+
const family = taskFamilyRefs(store, taskId);
|
|
21820
|
+
if (family.length === 0)
|
|
21821
|
+
return { ok: false, reason: "unknown-task", message: `No task ${taskId}.` };
|
|
21822
|
+
const refs = family.map(one => one.id);
|
|
21823
|
+
if (options.expect !== undefined && [...options.expect].sort((a, b) => a - b).join(",") !== refs.join(",")) {
|
|
21824
|
+
return { ok: false, reason: "blocked", message: `${taskId}'s versions changed since the list was shown.` };
|
|
21825
|
+
}
|
|
21826
|
+
// Already put away, every version: nothing to do. A family a person revived with a new revision puts away the rest.
|
|
21827
|
+
const remaining = refs.filter(ref => !refArchived(store, ref));
|
|
21828
|
+
if (remaining.length === 0)
|
|
21829
|
+
return { ok: false, reason: "archived", message: `${taskId} is already put away.` };
|
|
21830
|
+
const live = liveFamilyAttempt(store, refs, now);
|
|
21831
|
+
if (live !== null)
|
|
21832
|
+
return { ok: false, reason: "live", message: `${taskId} still has work going on. Stop it or let it finish, then put it away.` };
|
|
21833
|
+
const blocked = options.guard?.(refs) ?? null;
|
|
21834
|
+
if (blocked !== null)
|
|
21835
|
+
return { ok: false, reason: "blocked", message: blocked };
|
|
21836
|
+
const reason = by.reason.replace(/[\u0000-\u001f\u007f]+/g, " ").trim().slice(0, 300) || "Put away";
|
|
21837
|
+
const at = now.toISOString();
|
|
21838
|
+
const insert = store.handle.prepare("INSERT INTO task_archive (task_ref, by, by_kind, rule, reason, at) VALUES (?, ?, ?, ?, ?, ?)");
|
|
21839
|
+
for (const ref of remaining)
|
|
21840
|
+
insert.run(ref, by.by, by.byKind, by.rule, reason, at);
|
|
21841
|
+
store.handle.prepare("UPDATE notification SET resolved_at = ? WHERE resolved_at IS NULL AND task_ref IN (SELECT value FROM json_each(?))")
|
|
21842
|
+
.run(at, JSON.stringify(refs));
|
|
21843
|
+
// The family's flow cards hold while it is put away, so what they queued for people is settled too.
|
|
21844
|
+
store.handle.prepare(`UPDATE notification SET resolved_at = ? WHERE resolved_at IS NULL AND EXISTS (SELECT 1 FROM flow_card c
|
|
21845
|
+
WHERE (c.task IN (SELECT value FROM json_each(?)) OR c.primary_task IN (SELECT value FROM json_each(?)))
|
|
21846
|
+
AND (notification.dedupe_key LIKE 'flow:' || c.id || ':%' OR notification.dedupe_key LIKE 'flow-card:' || c.id || ':%'))`)
|
|
21847
|
+
.run(at, JSON.stringify(family.map(one => one.taskId)), JSON.stringify(family.map(one => one.taskId)));
|
|
21848
|
+
const repo = store.refById(refs[0])?.repo ?? null;
|
|
21849
|
+
store.recordAction({ at, actor: by.byKind === "tidy" ? "tidy" : `operator:${by.by}`, repo, taskId, runId: null, action: "task archived",
|
|
21850
|
+
outcome: by.rule ?? "by hand", source: "work", detail: `${reason}${refs.length > 1 ? ` (${refs.length} versions)` : ""}` });
|
|
21851
|
+
return { ok: true, refs };
|
|
21852
|
+
});
|
|
21853
|
+
}
|
|
21854
|
+
/** Brings a put-away task back (v145): every version of its family, with a ledger row. Tidy never puts a restored
|
|
21855
|
+
* task away again (tidy.ts reads the `task restored` row). */
|
|
21856
|
+
export function restoreTaskFamily(store, taskId, by, now, guard) {
|
|
21857
|
+
return store.transact(() => {
|
|
21858
|
+
const family = taskFamilyRefs(store, taskId);
|
|
21859
|
+
if (family.length === 0)
|
|
21860
|
+
return { ok: false, reason: "unknown-task", message: `No task ${taskId}.` };
|
|
21861
|
+
const refs = family.map(one => one.id).filter(ref => refArchived(store, ref));
|
|
21862
|
+
if (refs.length === 0)
|
|
21863
|
+
return { ok: false, reason: "not-archived", message: `${taskId} isn't put away.` };
|
|
21864
|
+
const blocked = guard?.(refs) ?? null;
|
|
21865
|
+
if (blocked !== null)
|
|
21866
|
+
return { ok: false, reason: "blocked", message: blocked };
|
|
21867
|
+
store.handle.prepare("DELETE FROM task_archive WHERE task_ref IN (SELECT value FROM json_each(?))").run(JSON.stringify(refs));
|
|
21868
|
+
const at = now.toISOString();
|
|
21869
|
+
store.recordAction({ at, actor: `operator:${by}`, repo: store.refById(refs[0])?.repo ?? null, taskId, runId: null, action: "task restored",
|
|
21870
|
+
outcome: "recorded", source: "work", detail: refs.length > 1 ? `${refs.length} versions` : null });
|
|
21871
|
+
return { ok: true, refs };
|
|
21872
|
+
});
|
|
21873
|
+
}
|