toolroll 0.9.76 → 0.9.78
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 +273 -0
- package/README.md +11 -4
- package/THIRD_PARTY_NOTICES.md +5 -5
- package/dist/agent-fence.js +46 -10
- package/dist/assignment.js +3 -0
- package/dist/backup-ui.d.ts +9 -1
- package/dist/backup-ui.js +28 -3
- package/dist/backup.js +4 -3
- package/dist/browser/THIRD_PARTY_NOTICES.txt +5 -5
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +31 -31
- package/dist/browser-workspace.d.ts +2 -0
- package/dist/builder.d.ts +18 -3
- package/dist/builder.js +152 -47
- package/dist/chat-decide.js +4 -0
- package/dist/check-failure.d.ts +92 -0
- package/dist/check-failure.js +295 -0
- package/dist/check-progress.d.ts +28 -1
- package/dist/check-progress.js +105 -6
- package/dist/check-report.d.ts +67 -6
- package/dist/check-report.js +254 -9
- package/dist/checkout-cleanup.js +2 -2
- package/dist/claim.d.ts +9 -0
- package/dist/claim.js +9 -1
- package/dist/cli.js +8 -5
- package/dist/coding-handoff.d.ts +1 -0
- package/dist/coding-handoff.js +20 -11
- package/dist/commit-extract.d.ts +6 -0
- package/dist/commit-extract.js +63 -8
- package/dist/console-chrome.d.ts +41 -0
- package/dist/console-chrome.js +166 -0
- package/dist/contracts/console-api.d.ts +1 -1
- package/dist/contracts/console-api.js +1 -1
- package/dist/contracts/release-check.d.ts +12 -0
- package/dist/contracts/release-check.js +17 -0
- package/dist/contracts/release-terms.d.ts +41 -0
- package/dist/contracts/release-terms.js +53 -0
- package/dist/contracts/verification-receipt.d.ts +20 -2
- package/dist/contracts/verification-receipt.js +15 -3
- package/dist/contracts/verification-view.d.ts +8 -0
- package/dist/controller-service.js +4 -1
- package/dist/controller-supervisor.d.ts +8 -0
- package/dist/controller-supervisor.js +33 -1
- package/dist/demo.js +3 -2
- package/dist/deploy-gate.js +5 -0
- package/dist/deploy-rescue.d.ts +41 -0
- package/dist/deploy-rescue.js +94 -0
- package/dist/desktop-update.d.ts +1 -1
- package/dist/dispose.d.ts +3 -0
- package/dist/dispose.js +9 -5
- package/dist/doctor.d.ts +7 -0
- package/dist/doctor.js +10 -2
- package/dist/envelope.js +2 -1
- package/dist/evidence.d.ts +9 -0
- package/dist/evidence.js +19 -4
- package/dist/exec.d.ts +9 -0
- package/dist/exec.js +22 -2
- package/dist/git-floor.d.ts +16 -0
- package/dist/git-floor.js +36 -0
- package/dist/grant.js +5 -1
- package/dist/guides.js +22 -11
- package/dist/latency-timeline.d.ts +46 -0
- package/dist/latency-timeline.js +120 -0
- package/dist/live-backup.d.ts +39 -0
- package/dist/live-backup.js +271 -0
- package/dist/operate.d.ts +13 -0
- package/dist/operate.js +969 -328
- package/dist/planner.js +2 -2
- package/dist/prepared-evidence.d.ts +2 -1
- package/dist/prepared-evidence.js +8 -3
- package/dist/project-knowledge.js +6 -2
- package/dist/project-learning.js +2 -2
- package/dist/proof.js +6 -0
- package/dist/read-executor-worker.js +8 -4
- package/dist/read-executor.d.ts +18 -1
- package/dist/read-executor.js +8 -0
- package/dist/release-adapters.d.ts +80 -2
- package/dist/release-adapters.js +441 -16
- package/dist/release-authority.js +6 -2
- package/dist/release-auto.d.ts +38 -7
- package/dist/release-auto.js +131 -15
- package/dist/release-card.d.ts +15 -3
- package/dist/release-card.js +25 -9
- package/dist/release-carry.d.ts +21 -1
- package/dist/release-carry.js +22 -0
- package/dist/release-ci-hold.d.ts +41 -0
- package/dist/release-ci-hold.js +53 -0
- package/dist/release-cli.d.ts +3 -0
- package/dist/release-cli.js +95 -17
- package/dist/release-coverage.js +5 -2
- package/dist/release-gate.d.ts +5 -0
- package/dist/release-gate.js +1 -1
- package/dist/release-recheck.d.ts +1 -1
- package/dist/release-recheck.js +8 -2
- package/dist/release.d.ts +144 -14
- package/dist/release.js +421 -81
- package/dist/review-context.js +8 -6
- package/dist/runner-process.d.ts +67 -0
- package/dist/runner-process.js +219 -0
- package/dist/runner.d.ts +48 -6
- package/dist/runner.js +73 -9
- package/dist/sandbox-runtime.d.ts +8 -0
- package/dist/sandbox-runtime.js +14 -2
- package/dist/scope.d.ts +19 -1
- package/dist/scope.js +26 -3
- package/dist/scout.js +2 -2
- package/dist/secret-mint.d.ts +1 -0
- package/dist/secret-mint.js +14 -0
- package/dist/serve.js +112 -51
- package/dist/server/chat.js +5 -5
- package/dist/server/http.d.ts +2 -0
- package/dist/server/pages.js +7 -3
- package/dist/server/remote-hooks.d.ts +1 -1
- package/dist/server/remote-hooks.js +5 -3
- package/dist/server/render-results.d.ts +2 -0
- package/dist/server/render-results.js +1 -0
- package/dist/server/render-tasks.d.ts +2 -0
- package/dist/server/render-tasks.js +2 -1
- package/dist/server/request-context.d.ts +8 -0
- package/dist/server/runtime.d.ts +2 -0
- package/dist/server/settings.js +22 -2
- package/dist/server/task-views.js +7 -2
- package/dist/server/tasks.js +4 -1
- package/dist/setup-guide.d.ts +2 -2
- package/dist/storage-sweep.js +1 -1
- package/dist/storage.d.ts +5 -0
- package/dist/storage.js +28 -1
- package/dist/store.d.ts +135 -2
- package/dist/store.js +275 -13
- package/dist/surface.js +3 -1
- package/dist/task-base.d.ts +4 -0
- package/dist/task-base.js +22 -2
- package/dist/teardown-git.d.ts +52 -3
- package/dist/teardown-git.js +145 -33
- package/dist/test-infra.js +1 -1
- package/dist/toolroll-update.js +2 -2
- package/dist/up-processes.d.ts +137 -0
- package/dist/up-processes.js +165 -0
- package/dist/up-role-child.d.ts +1 -0
- package/dist/up-role-child.js +81 -0
- package/dist/verification-evidence.d.ts +1 -0
- package/dist/verification-evidence.js +13 -1
- package/dist/workspace-revision.js +3 -1
- package/package.json +2 -1
package/dist/store.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mintSecret } from "./secret-mint.js";
|
|
1
2
|
import { WRITES_DEFAULT } from "./writes-mode.js";
|
|
2
3
|
import { checkAccessOf, widerThan } from "./check-access.js";
|
|
3
4
|
import { PROJECT_SANDBOX_DEFAULT, sandboxOf } from "./project-sandbox.js";
|
|
@@ -72,6 +73,7 @@ import { hasDisguisedText, hasForbiddenControls, validateNote } from "./decision
|
|
|
72
73
|
import { parseReviewContext, reviewContextCustodyProblem } from "./review-context.js";
|
|
73
74
|
import { foldReview, manualReviewOnly } from "./proof.js";
|
|
74
75
|
import { liveReleaseOf } from "./scope.js";
|
|
76
|
+
import { releaseCiHoldOf, releaseCiRunningOf } from "./release-ci-hold.js";
|
|
75
77
|
import { APPROVAL_BASES, approvalOf, machineActing, personProof, proofDetailOf, quarantinedAuthor, sealOriginOf, digestOf, canonicalProfileJson, chainFromJson, profileDigestOf, profileFromJson, scopeAuthorityOf, routeParityProblem, parseAcceptanceCriteria, exactAcceptance, exactStringList, exactSafeIntegerOrNull } from "./scope.js";
|
|
76
78
|
import { resolveScopeProfile, resolveRouteCandidates, exactPinOf, routeOfTask, agentChoicesFor } from "./agentconfig.js";
|
|
77
79
|
import { canonicalOverridesJson, canonicalRouteJson, isRiskLevel, isTaskSize, headroomFrom, makesNoPlan, legOf, overridesFromJson, phaseOfRole, recommendRoute, routeDigestOf, routeFromJson, routeProblems, routeStampProblem, ROUTE_ERA, PROVIDER_ID_LIST, } from "./phase-routing.js";
|
|
@@ -83,7 +85,7 @@ import { authenticate as runnerAuthenticate } from "./runner.js";
|
|
|
83
85
|
import { stampNewProjectLevel } from "./check-levels.js";
|
|
84
86
|
import { parseCheckProgressSnapshot } from "./check-progress.js";
|
|
85
87
|
import { authorityStandsSql } from "./approval-authority.js";
|
|
86
|
-
import { releasePolicyDigest, storedReleaseTerms, readReleaseTerms, readReleasePolicy } from "./contracts/release-terms.js";
|
|
88
|
+
import { releasePolicyDigest, serializeReleaseCheckPurpose, storedReleaseCheckPurpose, storedReleaseTerms, readReleaseTerms, readReleasePolicy } from "./contracts/release-terms.js";
|
|
87
89
|
import { accountReachesSql, normalizeProjectAccess, projectAccessAllows, readProjectAccess } from "./project-access.js";
|
|
88
90
|
import { LEDGER_COLUMNS, LEDGER_SCHEMA, LEDGER_TABLE, LEDGER_V99_TABLE, installLedgerTriggers } from "./action-ledger.js";
|
|
89
91
|
import { PLAN_AUTO_SCHEMA } from "./plan-auto.js";
|
|
@@ -457,6 +459,49 @@ CREATE TABLE IF NOT EXISTS run_live_detail (
|
|
|
457
459
|
CREATE INDEX IF NOT EXISTS run_live_open ON run (id) WHERE outcome IS NULL AND id > 0;
|
|
458
460
|
CREATE INDEX IF NOT EXISTS run_live_recent ON run (finished_at) WHERE outcome IS NOT NULL AND id > 0;
|
|
459
461
|
`;
|
|
462
|
+
/** v137 (docs/plans/actors-lessons.md): explicit lifecycle, latency timelines and the continuous database copy. New
|
|
463
|
+
* tables only. A lifecycle row's `holder` binds it to one registration (a worker's registered_at) or one console
|
|
464
|
+
* process; a row whose holder no longer matches says nothing. Timelines are bounded and pruned as they are written. */
|
|
465
|
+
export const V137_SCHEMA = `
|
|
466
|
+
CREATE TABLE IF NOT EXISTS service_lifecycle (
|
|
467
|
+
component TEXT PRIMARY KEY,
|
|
468
|
+
state TEXT NOT NULL CHECK (state IN ('started','draining','stopped')),
|
|
469
|
+
holder TEXT NOT NULL,
|
|
470
|
+
pid INTEGER,
|
|
471
|
+
changed_at TEXT NOT NULL,
|
|
472
|
+
beat_at TEXT NOT NULL,
|
|
473
|
+
drain_until TEXT
|
|
474
|
+
);
|
|
475
|
+
CREATE TABLE IF NOT EXISTS latency_timeline (
|
|
476
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
477
|
+
kind TEXT NOT NULL CHECK (kind IN ('console','release-check','deploy')),
|
|
478
|
+
subject TEXT NOT NULL,
|
|
479
|
+
task_id TEXT,
|
|
480
|
+
run_id INTEGER,
|
|
481
|
+
started_at TEXT NOT NULL,
|
|
482
|
+
total_ms INTEGER NOT NULL,
|
|
483
|
+
milestones_json TEXT NOT NULL
|
|
484
|
+
);
|
|
485
|
+
CREATE INDEX IF NOT EXISTS latency_timeline_by_kind ON latency_timeline (kind, id);
|
|
486
|
+
CREATE INDEX IF NOT EXISTS latency_timeline_by_run ON latency_timeline (run_id) WHERE run_id IS NOT NULL;
|
|
487
|
+
CREATE INDEX IF NOT EXISTS latency_timeline_by_task ON latency_timeline (task_id) WHERE task_id IS NOT NULL;
|
|
488
|
+
CREATE TABLE IF NOT EXISTS live_backup (
|
|
489
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
490
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
491
|
+
every_minutes INTEGER NOT NULL DEFAULT 5,
|
|
492
|
+
keep INTEGER NOT NULL DEFAULT 24,
|
|
493
|
+
folder TEXT,
|
|
494
|
+
holder TEXT,
|
|
495
|
+
held_until TEXT,
|
|
496
|
+
latest_file TEXT,
|
|
497
|
+
latest_sha256 TEXT,
|
|
498
|
+
latest_at TEXT,
|
|
499
|
+
latest_schema INTEGER,
|
|
500
|
+
latest_bytes INTEGER,
|
|
501
|
+
tried_at TEXT,
|
|
502
|
+
problem TEXT
|
|
503
|
+
);
|
|
504
|
+
`;
|
|
460
505
|
/** A project's "Show commands in the live view" (live-detail.ts): absent means off. */
|
|
461
506
|
export const LIVE_DETAIL_SCHEMA = `
|
|
462
507
|
CREATE TABLE IF NOT EXISTS live_detail_setting (
|
|
@@ -754,7 +799,14 @@ const REGISTERED_AT_FILING = "again, as it was filed";
|
|
|
754
799
|
// v136 adds, and changes no saved row: each run's own "Show commands in the live view" answer, captured once before its
|
|
755
800
|
// live file opens (run_live_detail; docs/plans/agents-view.md §2.4). Saved runs have none, which every reader takes as
|
|
756
801
|
// commands off: permission is never recovered from a transcript or the project's current setting.
|
|
757
|
-
|
|
802
|
+
// v137 adds, and changes no saved row (docs/plans/actors-lessons.md): each service component's lifecycle
|
|
803
|
+
// (service_lifecycle: started, draining or stopped, with a short heartbeat), bounded per-operation latency timelines
|
|
804
|
+
// (latency_timeline: slow console requests, release check parts, deploy phases) and the continuous database copy's
|
|
805
|
+
// settings and latest verified copy (live_backup, one row; absent reads every 5 minutes, keep 24, beside the database).
|
|
806
|
+
// v138 adds, and changes no saved row: a release check's purpose (task_scope.release_check_json; docs/plans/
|
|
807
|
+
// pipeline-speed.md, P0) — this exact candidate is a release's check, for a release that can't carry. Saved rows read
|
|
808
|
+
// NULL: no purpose (a termsless release task stays exactly as it was approved).
|
|
809
|
+
export const SCHEMA_VERSION = 138;
|
|
758
810
|
/** v118: the tables merged into `credential`, in the order they move (a person's API tokens keep their ids first). */
|
|
759
811
|
export const V118_MERGED_TABLES = Object.freeze({ api_token: "person", coordinator_credential: "coordinator", lead_credential: "lead" });
|
|
760
812
|
/** The foreign keys that named a merged table, exactly as their DDL wrote them; each names `credential(id)` instead. */
|
|
@@ -887,7 +939,7 @@ const V115_DROPPED_REFERENCES = /\s+REFERENCES (?:contestant|contest|tournament_
|
|
|
887
939
|
* unchanged), and adds the nullable column in V125_ADDED_COLUMNS, checked the same way. v126 only adds a new table
|
|
888
940
|
* (task_source) and the columns in V126_ADDED_COLUMNS (null or 0), checked the same way.
|
|
889
941
|
*/
|
|
890
|
-
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]);
|
|
942
|
+
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]);
|
|
891
943
|
/** v119: the columns it adds, by table, with the value every row saved before it reads (toolroll-update's HISTORY_RULES). */
|
|
892
944
|
export const V119_ADDED_COLUMNS = Object.freeze({
|
|
893
945
|
task_ref: { planning_generation: { definition: "INTEGER NOT NULL DEFAULT 0", saved: "0" } },
|
|
@@ -1008,6 +1060,11 @@ export const V134_ADDED_COLUMNS = Object.freeze({
|
|
|
1008
1060
|
export const V135_ADDED_COLUMNS = Object.freeze({
|
|
1009
1061
|
task_scope: { release_terms_json: { definition: "TEXT", saved: "NULL" } },
|
|
1010
1062
|
});
|
|
1063
|
+
/** v138 (docs/plans/pipeline-speed.md, P0): a scope's release check purpose, part of its digest when present. Saved
|
|
1064
|
+
* rows read NULL. */
|
|
1065
|
+
export const V138_ADDED_COLUMNS = Object.freeze({
|
|
1066
|
+
task_scope: { release_check_json: { definition: "TEXT", saved: "NULL" } },
|
|
1067
|
+
});
|
|
1011
1068
|
/**
|
|
1012
1069
|
* v135: one approval per release (docs/plans/release-once.md). `release_authority` is the single source of truth for a
|
|
1013
1070
|
* release a person approved: one row per generation, changed only by compare-and-swap on `rev`, with a lease whose
|
|
@@ -4238,6 +4295,13 @@ CREATE INDEX IF NOT EXISTS run_checkpoint_by_task ON run_checkpoint (task_ref, i
|
|
|
4238
4295
|
}
|
|
4239
4296
|
// v136: each run's own live-view answer (a new table only; saved runs have none: commands off).
|
|
4240
4297
|
db.exec(RUN_LIVE_DETAIL_SCHEMA);
|
|
4298
|
+
// v137: lifecycle, latency timelines and the continuous copy (new tables only).
|
|
4299
|
+
db.exec(V137_SCHEMA);
|
|
4300
|
+
// v138: a scope's release check purpose (V138_ADDED_COLUMNS: saved rows NULL, no purpose).
|
|
4301
|
+
for (const [table, columns] of Object.entries(V138_ADDED_COLUMNS)) {
|
|
4302
|
+
for (const [column, { definition }] of Object.entries(columns))
|
|
4303
|
+
addColumn(db, table, column, definition);
|
|
4304
|
+
}
|
|
4241
4305
|
// Keep the epoch until reclamation succeeds, including a retry after compaction already committed. A new file has
|
|
4242
4306
|
// nothing to reclaim.
|
|
4243
4307
|
if (!fresh)
|
|
@@ -6667,7 +6731,7 @@ export class Store {
|
|
|
6667
6731
|
saveScope(scope, mutation = {}, options = {}) {
|
|
6668
6732
|
this.once(mutation, "saveScope", () => this.transact(() => {
|
|
6669
6733
|
// The text's current author mark, read before this write (approval policy §1.4).
|
|
6670
|
-
const prior = this.db.prepare("SELECT proposed_via, release_terms_json FROM task_scope WHERE task_id = ?").get(scope.taskId);
|
|
6734
|
+
const prior = this.db.prepare("SELECT proposed_via, release_terms_json, release_check_json FROM task_scope WHERE task_id = ?").get(scope.taskId);
|
|
6671
6735
|
// THE filing invariant (foundations findings 5/13/19): every scope
|
|
6672
6736
|
// row leaves this method either RESOLVED (working profile stamped,
|
|
6673
6737
|
// digest recomputed to bind it, version 2) or UNRESOLVED with its
|
|
@@ -6831,6 +6895,12 @@ export class Store {
|
|
|
6831
6895
|
// v135: release terms the caller names, else the ones this scope already carries (a refile keeps them bound).
|
|
6832
6896
|
const priorTerms = storedReleaseTerms(prior?.["release_terms_json"]);
|
|
6833
6897
|
const releaseTerms = options.releaseTerms !== undefined ? options.releaseTerms : priorTerms.ok ? priorTerms.terms : null;
|
|
6898
|
+
// v138: the release check purpose likewise; terms named now replace a kept purpose (a scope carries one or the other).
|
|
6899
|
+
const priorPurpose = storedReleaseCheckPurpose(prior?.["release_check_json"]);
|
|
6900
|
+
const releaseCheck = options.releaseCheck !== undefined ? options.releaseCheck
|
|
6901
|
+
: options.releaseTerms != null ? null : priorPurpose.ok ? priorPurpose.purpose : null;
|
|
6902
|
+
if (releaseTerms !== null && releaseCheck !== null)
|
|
6903
|
+
throw new Error("A scope carries release terms or a release check purpose, never both.");
|
|
6834
6904
|
const digestInput = {
|
|
6835
6905
|
goal: scope.goal,
|
|
6836
6906
|
outOfScope: scope.outOfScope,
|
|
@@ -6840,6 +6910,7 @@ export class Store {
|
|
|
6840
6910
|
qualityMode,
|
|
6841
6911
|
candidate: scope.candidate ?? null,
|
|
6842
6912
|
releaseTerms,
|
|
6913
|
+
releaseCheck,
|
|
6843
6914
|
};
|
|
6844
6915
|
// EVERY route filed since v47 binds into the digest — routine-shaped
|
|
6845
6916
|
// ones included — so approval freezes exactly which agent runs each
|
|
@@ -6869,8 +6940,8 @@ export class Store {
|
|
|
6869
6940
|
.prepare(`INSERT INTO task_scope
|
|
6870
6941
|
(task_id, goal, out_of_scope, touches, budget_microusd, proposed_at, digest, approved_at, approved_by, approved_digest,
|
|
6871
6942
|
profile_json, profile_state, unresolved_reason, digest_version, profile_provenance, proposed_chain_json, acceptance_json, quality_mode,
|
|
6872
|
-
risk_level, proposed_route_json, route_era, candidate, release_terms_json)
|
|
6873
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
6943
|
+
risk_level, proposed_route_json, route_era, candidate, release_terms_json, release_check_json)
|
|
6944
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
6874
6945
|
ON CONFLICT (task_id) DO UPDATE SET
|
|
6875
6946
|
goal = excluded.goal, out_of_scope = excluded.out_of_scope,
|
|
6876
6947
|
touches = excluded.touches, budget_microusd = excluded.budget_microusd,
|
|
@@ -6882,7 +6953,7 @@ export class Store {
|
|
|
6882
6953
|
profile_provenance = excluded.profile_provenance, proposed_chain_json = excluded.proposed_chain_json,
|
|
6883
6954
|
acceptance_json = excluded.acceptance_json, quality_mode = excluded.quality_mode,
|
|
6884
6955
|
risk_level = excluded.risk_level, proposed_route_json = excluded.proposed_route_json, route_era = excluded.route_era,
|
|
6885
|
-
candidate = excluded.candidate, release_terms_json = excluded.release_terms_json`)
|
|
6956
|
+
candidate = excluded.candidate, release_terms_json = excluded.release_terms_json, release_check_json = excluded.release_check_json`)
|
|
6886
6957
|
.run(scope.taskId, scope.goal, scope.outOfScope, JSON.stringify(scope.touches), scope.budgetMicrousd ?? null, scope.proposedAt, boundDigest, scope.approvedAt, scope.approvedBy, scope.approvedDigest, profile === null ? null : canonicalProfileJson(profile), profile === null ? "unresolved" : "resolved", unresolvedReason, profile === null ? 1 : 2, provenance,
|
|
6887
6958
|
// Fallback chains (v30-v114) are gone: no filing names one.
|
|
6888
6959
|
null, scope.acceptance.length === 0 ? null : JSON.stringify(scope.acceptance), qualityMode,
|
|
@@ -6890,7 +6961,7 @@ export class Store {
|
|
|
6890
6961
|
// routine risk, the value its signed terms always carried.
|
|
6891
6962
|
"routine", route === null ? null : canonicalRouteJson(route),
|
|
6892
6963
|
// The durable era: every row this method writes is a routed row.
|
|
6893
|
-
ROUTE_ERA, scope.candidate ?? null, releaseTerms === null ? null : serializeReleaseTerms(releaseTerms));
|
|
6964
|
+
ROUTE_ERA, scope.candidate ?? null, releaseTerms === null ? null : serializeReleaseTerms(releaseTerms), releaseCheck === null ? null : serializeReleaseCheckPurpose(releaseCheck));
|
|
6894
6965
|
// Who wrote THIS text (mate arc, ruling 2): set per write, so a human
|
|
6895
6966
|
// rewrite clears the lead's mark and a mate rewrite sets it — but only a
|
|
6896
6967
|
// person's write clears one (approval policy §1.4): the lead (its token)
|
|
@@ -6932,6 +7003,10 @@ export class Store {
|
|
|
6932
7003
|
// road it came by.
|
|
6933
7004
|
if (basis === undefined && machineActing())
|
|
6934
7005
|
return false;
|
|
7006
|
+
// A RELEASE WAITING FOR CI (Codex review round 1, finding 4): filed while its pull request's checks still ran,
|
|
7007
|
+
// only the automatic road (which proves CI itself) seals it until the plane read exact-head CI passing.
|
|
7008
|
+
if (basis?.kind !== "auto-check" && basis?.kind !== "auto-green" && releaseCiHoldOf(this, taskId) !== null)
|
|
7009
|
+
return false;
|
|
6935
7010
|
// `carried` is written by the carry transaction alone (carryReleaseApproval), never through a seal.
|
|
6936
7011
|
if (basis?.kind === "carried")
|
|
6937
7012
|
return false;
|
|
@@ -7011,7 +7086,14 @@ export class Store {
|
|
|
7011
7086
|
// A DEPLOY GATE FROM CHAT (chat-management.md §0a.5): a signed candidate or release terms seal from chat only
|
|
7012
7087
|
// with the release card's evidence (its act and card digest), never through the plain plan card or a generic
|
|
7013
7088
|
// one. Read from the raw row, so a terms column that doesn't parse is a deploy gate too.
|
|
7014
|
-
const deployGate = (typeof working["candidate"] === "string" && working["candidate"] !== "") || working["release_terms_json"] != null;
|
|
7089
|
+
const deployGate = (typeof working["candidate"] === "string" && working["candidate"] !== "") || working["release_terms_json"] != null || working["release_check_json"] != null;
|
|
7090
|
+
// A RELEASE CHECK PURPOSE (v138) seals, like terms, only on a person's own yes: the ceremony or a paired-chat tap
|
|
7091
|
+
// through the release card — never a mode, a grant, a carry or an automatic seal (those need terms).
|
|
7092
|
+
const purpose = storedReleaseCheckPurpose(working["release_check_json"]);
|
|
7093
|
+
if (!purpose.ok)
|
|
7094
|
+
return false;
|
|
7095
|
+
if (purpose.purpose !== null && basis !== undefined && basis.kind !== "chat")
|
|
7096
|
+
return false;
|
|
7015
7097
|
if (deployGate && basis?.kind === "chat" && !(basis.card === "release" && typeof basis.cardDigest === "string" && /^[0-9a-f]{64}$/.test(basis.cardDigest)))
|
|
7016
7098
|
return false;
|
|
7017
7099
|
// AUTOMATIC RELEASE SEALS (auto-approve-green.md): only on a release task (a candidate and release terms signed
|
|
@@ -8018,7 +8100,7 @@ export class Store {
|
|
|
8018
8100
|
/** A single-use door into the instance (U2): 128-bit token, sha256
|
|
8019
8101
|
* stored, role pinned at mint, 72 hours. Only the caller's own
|
|
8020
8102
|
* authentication gates this — an approver mints; a viewer cannot. */
|
|
8021
|
-
mintInvite(role, mintedBy, now, token = () =>
|
|
8103
|
+
mintInvite(role, mintedBy, now, token = () => mintSecret(16), projects = null) {
|
|
8022
8104
|
const access = normalizeProjectAccess(projects);
|
|
8023
8105
|
return this.transact(() => {
|
|
8024
8106
|
if (!this.isInstanceOperator(mintedBy))
|
|
@@ -11646,7 +11728,7 @@ export class Store {
|
|
|
11646
11728
|
if (route === null) {
|
|
11647
11729
|
return { ok: false, reason: "unreadable", detail: scope.approvedRouteJson == null ? "the approval sealed no agent route — re-file the scope and approve it again" : "the approval's sealed agent route cannot be read — re-file the scope and approve it again" };
|
|
11648
11730
|
}
|
|
11649
|
-
const fields = { goal: scope.goal, outOfScope: scope.outOfScope, touches: scope.touches, budgetMicrousd: scope.budgetMicrousd, acceptance: scope.acceptance, candidate: scope.candidate ?? null, qualityMode: scope.qualityMode ?? "default", releaseTerms: scope.releaseTerms ?? null };
|
|
11731
|
+
const fields = { goal: scope.goal, outOfScope: scope.outOfScope, touches: scope.touches, budgetMicrousd: scope.budgetMicrousd, acceptance: scope.acceptance, candidate: scope.candidate ?? null, qualityMode: scope.qualityMode ?? "default", releaseTerms: scope.releaseTerms ?? null, releaseCheck: scope.releaseCheck ?? null };
|
|
11650
11732
|
const chain = scope.approvalKind === "chain" ? chainFromJson(scope.approvedChainJson ?? null) : null;
|
|
11651
11733
|
const target = chain !== null ? { chain } : (scope.approvedProfile ?? null);
|
|
11652
11734
|
if (target === null)
|
|
@@ -17502,7 +17584,8 @@ export class Store {
|
|
|
17502
17584
|
acquireWatchLease(runner, repo, owner, ttlMs, now) {
|
|
17503
17585
|
return this.transact(() => {
|
|
17504
17586
|
const stamp = now.toISOString();
|
|
17505
|
-
|
|
17587
|
+
// Never longer than MAX_WATCH_LEASE_MS, whatever a caller asks: a crashed holder's project is free within a minute.
|
|
17588
|
+
const expires = new Date(now.getTime() + Math.min(ttlMs, MAX_WATCH_LEASE_MS)).toISOString();
|
|
17506
17589
|
const row = this.db
|
|
17507
17590
|
.prepare("SELECT * FROM watch_lease WHERE runner = ? AND repo = ?")
|
|
17508
17591
|
.get(runner, repo);
|
|
@@ -17539,7 +17622,7 @@ export class Store {
|
|
|
17539
17622
|
const { changes } = this.db
|
|
17540
17623
|
.prepare(`UPDATE watch_lease SET expires_at = ?, heartbeat_at = ?
|
|
17541
17624
|
WHERE runner = ? AND repo = ? AND owner = ? AND expires_at > ?`)
|
|
17542
|
-
.run(new Date(now.getTime() + ttlMs).toISOString(), now.toISOString(), runner, repo, owner, now.toISOString());
|
|
17625
|
+
.run(new Date(now.getTime() + Math.min(ttlMs, MAX_WATCH_LEASE_MS)).toISOString(), now.toISOString(), runner, repo, owner, now.toISOString());
|
|
17543
17626
|
return Number(changes) > 0;
|
|
17544
17627
|
}
|
|
17545
17628
|
releaseWatchLease(runner, repo, owner, now) {
|
|
@@ -17547,6 +17630,12 @@ export class Store {
|
|
|
17547
17630
|
.prepare("UPDATE watch_lease SET expires_at = ? WHERE runner = ? AND repo = ? AND owner = ?")
|
|
17548
17631
|
.run(now.toISOString(), runner, repo, owner);
|
|
17549
17632
|
}
|
|
17633
|
+
/** Lapse every watch lease a runner holds now: its holder was proved gone (runner.ts registerRunnerIfIdle). The rows
|
|
17634
|
+
* stay, so the next acquire takes them over and recovers the superseded incarnation as after any crash. */
|
|
17635
|
+
expireWatchLeasesOf(runner, now) {
|
|
17636
|
+
const stamp = now.toISOString();
|
|
17637
|
+
return Number(this.db.prepare("UPDATE watch_lease SET expires_at = ? WHERE runner = ? AND expires_at > ?").run(stamp, runner, stamp).changes);
|
|
17638
|
+
}
|
|
17550
17639
|
/** The first still-unexpired watch lease this runner holds, if any (arc 2:
|
|
17551
17640
|
* the takeover door refuses while one exists). */
|
|
17552
17641
|
liveWatchLeaseOf(runner, now) {
|
|
@@ -20183,6 +20272,14 @@ export function scopeTermsProblem(row) {
|
|
|
20183
20272
|
if (!terms.ok)
|
|
20184
20273
|
return terms.problem;
|
|
20185
20274
|
}
|
|
20275
|
+
// v138: likewise the release check purpose, and never beside terms.
|
|
20276
|
+
if ("release_check_json" in row) {
|
|
20277
|
+
const purpose = storedReleaseCheckPurpose(row["release_check_json"]);
|
|
20278
|
+
if (!purpose.ok)
|
|
20279
|
+
return purpose.problem;
|
|
20280
|
+
if (purpose.purpose !== null && row["release_terms_json"] != null)
|
|
20281
|
+
return "the scope carries both release terms and a release check purpose";
|
|
20282
|
+
}
|
|
20186
20283
|
for (const column of ["approved_at", "approved_by", "approved_digest", "profile_json", "approved_profile_json", "proposed_chain_json", "approved_chain_json", "proposed_route_json", "approved_route_json", "unresolved_reason"]) {
|
|
20187
20284
|
if (row[column] !== null && row[column] !== undefined && typeof row[column] !== "string")
|
|
20188
20285
|
return `${column.replace(/_/g, " ")} is not stored as text`;
|
|
@@ -20228,6 +20325,7 @@ function readScope(row) {
|
|
|
20228
20325
|
routeEra: row["route_era"] === null || row["route_era"] === undefined ? null : Number(row["route_era"]),
|
|
20229
20326
|
candidate: typeof row["candidate"] === "string" && row["candidate"] !== "" ? row["candidate"] : null,
|
|
20230
20327
|
releaseTerms: (() => { const read = storedReleaseTerms(row["release_terms_json"]); return read.ok ? read.terms : null; })(),
|
|
20328
|
+
releaseCheck: (() => { const read = storedReleaseCheckPurpose(row["release_check_json"]); return read.ok ? read.purpose : null; })(),
|
|
20231
20329
|
proposedVia: row["proposed_via"] === "mate" || row["proposed_via"] === "coordinator" || row["proposed_via"] === "scout" || row["proposed_via"] === "lead" ? row["proposed_via"] : null,
|
|
20232
20330
|
};
|
|
20233
20331
|
}
|
|
@@ -20507,6 +20605,9 @@ export function reserveRelease(store, input) {
|
|
|
20507
20605
|
const a = releaseAuthority(store, input.authority);
|
|
20508
20606
|
if (a === null)
|
|
20509
20607
|
return { ok: false, why: `No release ${input.authority}.` };
|
|
20608
|
+
// Early approval authorizes the check, never an irreversible operation before the plane confirms exact-head CI.
|
|
20609
|
+
if (releaseCiRunningOf(store, input.taskId) !== null)
|
|
20610
|
+
return { ok: false, why: "This release still waits for GitHub CI. Resume it with `toolroll release`." };
|
|
20510
20611
|
const at = input.now.toISOString();
|
|
20511
20612
|
// Expiry stops every live state; an operation already reserved settles only through reconciliation.
|
|
20512
20613
|
if (a.expiresAt <= at)
|
|
@@ -20548,6 +20649,8 @@ export function runReleaseOperation(store, id, epoch, now, issuer = currentRelea
|
|
|
20548
20649
|
const a = op === null ? null : releaseAuthority(store, op.authority);
|
|
20549
20650
|
if (op === null || a === null || op.state !== "reserved" || op.epoch !== epoch || a.leaseEpoch !== epoch)
|
|
20550
20651
|
return false;
|
|
20652
|
+
if (releaseCiRunningOf(store, op.taskId) !== null)
|
|
20653
|
+
return false;
|
|
20551
20654
|
// Past the expiry nothing new starts; the reserved operation is settled by reconciliation instead.
|
|
20552
20655
|
if (a.expiresAt <= now.toISOString())
|
|
20553
20656
|
return false;
|
|
@@ -20858,3 +20961,162 @@ export function agentsLiveRuns(store, endedSince) {
|
|
|
20858
20961
|
repo: row["live_task_repo"] === null || row["live_task_repo"] === undefined ? null : String(row["live_task_repo"]),
|
|
20859
20962
|
}));
|
|
20860
20963
|
}
|
|
20964
|
+
/**
|
|
20965
|
+
* This runner's watches a predecessor left needing recovery (Codex review round 1, finding 2): a watch lease still
|
|
20966
|
+
* live, or one whose holder left a claim open (or a run open under a claim a reaper already released). A replacement
|
|
20967
|
+
* worker takes each over — recovering the holder's incarnation in the same transaction — before it admits new work,
|
|
20968
|
+
* whichever projects its start names (a project being removed when the predecessor died is in none of them).
|
|
20969
|
+
*/
|
|
20970
|
+
export function predecessorWatches(store, runner, now) {
|
|
20971
|
+
return store.handle.prepare(`SELECT w.repo AS repo, w.owner AS owner FROM watch_lease w
|
|
20972
|
+
WHERE w.runner = ? AND (w.expires_at > ? OR EXISTS (
|
|
20973
|
+
SELECT 1 FROM claim c WHERE c.runner = w.runner AND c.incarnation = w.owner
|
|
20974
|
+
AND (c.released_at IS NULL OR EXISTS (SELECT 1 FROM run r WHERE r.lease_id = c.lease_id AND r.outcome IS NULL))))
|
|
20975
|
+
ORDER BY w.repo`).all(runner, now.toISOString()).map(row => ({ repo: String(row["repo"]), owner: String(row["owner"]) }));
|
|
20976
|
+
}
|
|
20977
|
+
// ---- v137: lifecycle, latency timelines, the continuous copy (docs/plans/actors-lessons.md) ----------------------
|
|
20978
|
+
/** The longest a watch lease is taken or renewed for (durable-actors' MAX_HOST_LEASE_DURATION_MS). */
|
|
20979
|
+
export const MAX_WATCH_LEASE_MS = 60_000;
|
|
20980
|
+
function lifecycleRowOf(row) {
|
|
20981
|
+
return { component: String(row["component"]), state: String(row["state"]), holder: String(row["holder"]), pid: row["pid"] == null ? null : Number(row["pid"]),
|
|
20982
|
+
changedAt: String(row["changed_at"]), beatAt: String(row["beat_at"]), drainUntil: row["drain_until"] == null ? null : String(row["drain_until"]) };
|
|
20983
|
+
}
|
|
20984
|
+
/** One component's lifecycle row, or null (never reported: a component from before v137 says nothing). */
|
|
20985
|
+
export function lifecycleOf(store, component) {
|
|
20986
|
+
const row = store.handle.prepare("SELECT * FROM service_lifecycle WHERE component = ?").get(component);
|
|
20987
|
+
return row === undefined ? null : lifecycleRowOf(row);
|
|
20988
|
+
}
|
|
20989
|
+
/** Every reported component, by name. */
|
|
20990
|
+
export function lifecycles(store) {
|
|
20991
|
+
return store.handle.prepare("SELECT * FROM service_lifecycle ORDER BY component").all().map(lifecycleRowOf);
|
|
20992
|
+
}
|
|
20993
|
+
/**
|
|
20994
|
+
* Report a component's state. `started` replaces whatever an earlier holder left; `draining` and `stopped` only move the
|
|
20995
|
+
* current holder's row (a stale process can't mark its successor draining), and never back from stopped. Returns
|
|
20996
|
+
* whether the row now says what was asked.
|
|
20997
|
+
*/
|
|
20998
|
+
export function reportLifecycle(store, component, state, holder, now, extra = {}) {
|
|
20999
|
+
const stamp = now.toISOString();
|
|
21000
|
+
const until = extra.drainUntil == null ? null : extra.drainUntil.toISOString();
|
|
21001
|
+
if (state === "started") {
|
|
21002
|
+
store.handle.prepare(`INSERT INTO service_lifecycle (component, state, holder, pid, changed_at, beat_at, drain_until) VALUES (?, 'started', ?, ?, ?, ?, NULL)
|
|
21003
|
+
ON CONFLICT(component) DO UPDATE SET state = 'started', holder = excluded.holder, pid = excluded.pid, changed_at = excluded.changed_at, beat_at = excluded.beat_at, drain_until = NULL`)
|
|
21004
|
+
.run(component, holder, extra.pid ?? null, stamp, stamp);
|
|
21005
|
+
return true;
|
|
21006
|
+
}
|
|
21007
|
+
const changed = store.handle.prepare(`UPDATE service_lifecycle SET state = ?, changed_at = ?, beat_at = ?, drain_until = CASE WHEN ? = 'draining' THEN ? ELSE drain_until END
|
|
21008
|
+
WHERE component = ? AND holder = ? AND state <> 'stopped'`).run(state, stamp, stamp, state, until, component, holder).changes;
|
|
21009
|
+
return Number(changed) === 1;
|
|
21010
|
+
}
|
|
21011
|
+
/** The short heartbeat: the current holder's row, while it is not stopped. False means another holder took it. */
|
|
21012
|
+
export function beatLifecycle(store, component, holder, now) {
|
|
21013
|
+
return Number(store.handle.prepare("UPDATE service_lifecycle SET beat_at = ? WHERE component = ? AND holder = ? AND state <> 'stopped'")
|
|
21014
|
+
.run(now.toISOString(), component, holder).changes) === 1;
|
|
21015
|
+
}
|
|
21016
|
+
/** At most this many milestones per timeline (the rest are dropped, the last kept), and timelines kept per kind. */
|
|
21017
|
+
export const MAX_MILESTONES = 40;
|
|
21018
|
+
export const TIMELINES_KEPT = 200;
|
|
21019
|
+
/** Timelines older than this go whatever their count. */
|
|
21020
|
+
export const TIMELINE_DAYS = 14;
|
|
21021
|
+
const plainText = (value, max) => value.replace(/[\u0000-\u001f\u007f]+/g, " ").trim().slice(0, max);
|
|
21022
|
+
/** Milestones made safe to keep: bounded in number and length, offsets whole non-negative milliseconds, in order. */
|
|
21023
|
+
export function boundedMilestones(milestones) {
|
|
21024
|
+
const clean = milestones.filter(one => typeof one.name === "string" && Number.isFinite(one.ms)).map(one => ({
|
|
21025
|
+
name: plainText(one.name, 80), ms: Math.max(0, Math.round(one.ms)),
|
|
21026
|
+
...(typeof one.detail === "string" && one.detail.trim() !== "" ? { detail: plainText(one.detail, 160) } : {}),
|
|
21027
|
+
})).filter(one => one.name !== "");
|
|
21028
|
+
return clean.length <= MAX_MILESTONES ? clean : [...clean.slice(0, MAX_MILESTONES - 1), clean[clean.length - 1]];
|
|
21029
|
+
}
|
|
21030
|
+
/** Keep one timeline, then prune its kind to the newest TIMELINES_KEPT within TIMELINE_DAYS. */
|
|
21031
|
+
export function recordLatencyTimeline(store, entry, now) {
|
|
21032
|
+
return store.transact(() => {
|
|
21033
|
+
const id = Number(store.handle.prepare(`INSERT INTO latency_timeline (kind, subject, task_id, run_id, started_at, total_ms, milestones_json) VALUES (?, ?, ?, ?, ?, ?, ?)`)
|
|
21034
|
+
.run(entry.kind, plainText(entry.subject, 120), entry.taskId ?? null, entry.runId ?? null, entry.startedAt.toISOString(), Math.max(0, Math.round(entry.totalMs)), JSON.stringify(boundedMilestones(entry.milestones))).lastInsertRowid);
|
|
21035
|
+
const cutoff = new Date(now.getTime() - TIMELINE_DAYS * 86_400_000).toISOString();
|
|
21036
|
+
store.handle.prepare(`DELETE FROM latency_timeline WHERE kind = ? AND (started_at < ? OR id <= (SELECT id FROM latency_timeline WHERE kind = ? ORDER BY id DESC LIMIT 1 OFFSET ?))`)
|
|
21037
|
+
.run(entry.kind, cutoff, entry.kind, TIMELINES_KEPT);
|
|
21038
|
+
return id;
|
|
21039
|
+
});
|
|
21040
|
+
}
|
|
21041
|
+
/** Timelines, newest first: of one kind, one run's, or one task's. A row that doesn't read is left out. */
|
|
21042
|
+
export function latencyTimelines(store, filter) {
|
|
21043
|
+
const where = [], args = [];
|
|
21044
|
+
if (filter.kind !== undefined) {
|
|
21045
|
+
where.push("kind = ?");
|
|
21046
|
+
args.push(filter.kind);
|
|
21047
|
+
}
|
|
21048
|
+
if (filter.runId !== undefined && filter.taskId !== undefined) {
|
|
21049
|
+
where.push("(run_id = ? OR task_id = ?)");
|
|
21050
|
+
args.push(filter.runId, filter.taskId);
|
|
21051
|
+
}
|
|
21052
|
+
else if (filter.runId !== undefined) {
|
|
21053
|
+
where.push("run_id = ?");
|
|
21054
|
+
args.push(filter.runId);
|
|
21055
|
+
}
|
|
21056
|
+
else if (filter.taskId !== undefined) {
|
|
21057
|
+
where.push("task_id = ?");
|
|
21058
|
+
args.push(filter.taskId);
|
|
21059
|
+
}
|
|
21060
|
+
const rows = store.handle.prepare(`SELECT * FROM latency_timeline${where.length === 0 ? "" : ` WHERE ${where.join(" AND ")}`} ORDER BY id DESC LIMIT ?`).all(...args, Math.min(200, Math.max(1, filter.limit ?? 20)));
|
|
21061
|
+
return rows.flatMap(row => {
|
|
21062
|
+
let milestones;
|
|
21063
|
+
try {
|
|
21064
|
+
const parsed = JSON.parse(String(row["milestones_json"]));
|
|
21065
|
+
milestones = Array.isArray(parsed) ? boundedMilestones(parsed) : [];
|
|
21066
|
+
}
|
|
21067
|
+
catch {
|
|
21068
|
+
return [];
|
|
21069
|
+
}
|
|
21070
|
+
return [{ id: Number(row["id"]), kind: String(row["kind"]), subject: String(row["subject"]), taskId: row["task_id"] == null ? null : String(row["task_id"]),
|
|
21071
|
+
runId: row["run_id"] == null ? null : Number(row["run_id"]), startedAt: String(row["started_at"]), totalMs: Number(row["total_ms"]), milestones }];
|
|
21072
|
+
});
|
|
21073
|
+
}
|
|
21074
|
+
export const DEFAULT_LIVE_BACKUP = { enabled: true, everyMinutes: 5, keep: 24, folder: null };
|
|
21075
|
+
export function liveBackupState(store) {
|
|
21076
|
+
const row = store.handle.prepare("SELECT * FROM live_backup WHERE id = 1").get();
|
|
21077
|
+
if (row === undefined)
|
|
21078
|
+
return { settings: { ...DEFAULT_LIVE_BACKUP }, latest: null, triedAt: null, problem: null };
|
|
21079
|
+
const latest = row["latest_file"] == null || row["latest_sha256"] == null || row["latest_at"] == null || row["latest_schema"] == null ? null
|
|
21080
|
+
: { file: String(row["latest_file"]), sha256: String(row["latest_sha256"]), madeAt: String(row["latest_at"]), schemaVersion: Number(row["latest_schema"]), bytes: Number(row["latest_bytes"] ?? 0) };
|
|
21081
|
+
return { settings: { enabled: Number(row["enabled"]) === 1, everyMinutes: Number(row["every_minutes"]), keep: Number(row["keep"]), folder: row["folder"] == null ? null : String(row["folder"]) },
|
|
21082
|
+
latest, triedAt: row["tried_at"] == null ? null : String(row["tried_at"]), problem: row["problem"] == null ? null : String(row["problem"]) };
|
|
21083
|
+
}
|
|
21084
|
+
const liveBackupWords = (one) => `${one.enabled ? `every ${one.everyMinutes} min` : "off"}, keep ${one.keep}, ${one.folder ?? "the default folder"}`;
|
|
21085
|
+
/** Change the continuous copy's settings; the ledger keeps before → after. A new folder forgets the latest copy (it is
|
|
21086
|
+
* in the old one). */
|
|
21087
|
+
export function setLiveBackupSettings(store, next, by, now) {
|
|
21088
|
+
return store.transact(() => {
|
|
21089
|
+
const before = liveBackupState(store).settings;
|
|
21090
|
+
store.handle.prepare(`INSERT INTO live_backup (id, enabled, every_minutes, keep, folder) VALUES (1, ?, ?, ?, ?)
|
|
21091
|
+
ON CONFLICT(id) DO UPDATE SET enabled = excluded.enabled, every_minutes = excluded.every_minutes, keep = excluded.keep, folder = excluded.folder`)
|
|
21092
|
+
.run(next.enabled ? 1 : 0, next.everyMinutes, next.keep, next.folder);
|
|
21093
|
+
if (before.folder !== next.folder)
|
|
21094
|
+
store.handle.prepare("UPDATE live_backup SET latest_file = NULL, latest_sha256 = NULL, latest_at = NULL, latest_schema = NULL, latest_bytes = NULL WHERE id = 1").run();
|
|
21095
|
+
if (liveBackupWords(before) !== liveBackupWords(next)) {
|
|
21096
|
+
store.recordAction({ at: now.toISOString(), actor: by, repo: null, taskId: null, runId: null, action: "continuous backup settings changed", outcome: "changed", source: "policy", detail: `${liveBackupWords(before)} → ${liveBackupWords(next)}` });
|
|
21097
|
+
}
|
|
21098
|
+
return liveBackupState(store).settings;
|
|
21099
|
+
});
|
|
21100
|
+
}
|
|
21101
|
+
/** Take (or keep) the continuous copy's lease until `until`: one process copies at a time. */
|
|
21102
|
+
export function holdLiveBackup(store, holder, now, until) {
|
|
21103
|
+
return store.transact(() => {
|
|
21104
|
+
store.handle.prepare("INSERT OR IGNORE INTO live_backup (id) VALUES (1)").run();
|
|
21105
|
+
return Number(store.handle.prepare("UPDATE live_backup SET holder = ?, held_until = ? WHERE id = 1 AND (holder IS NULL OR holder = ? OR held_until IS NULL OR held_until < ?)")
|
|
21106
|
+
.run(holder, until.toISOString(), holder, now.toISOString()).changes) === 1;
|
|
21107
|
+
});
|
|
21108
|
+
}
|
|
21109
|
+
export function releaseLiveBackup(store, holder) {
|
|
21110
|
+
store.handle.prepare("UPDATE live_backup SET holder = NULL, held_until = NULL WHERE id = 1 AND holder = ?").run(holder);
|
|
21111
|
+
}
|
|
21112
|
+
/** A copy's outcome: a verified copy becomes the latest; a failure is kept as the problem (the latest stays). */
|
|
21113
|
+
export function recordLiveBackup(store, outcome, now) {
|
|
21114
|
+
store.handle.prepare("INSERT OR IGNORE INTO live_backup (id) VALUES (1)").run();
|
|
21115
|
+
if (outcome.ok) {
|
|
21116
|
+
store.handle.prepare("UPDATE live_backup SET latest_file = ?, latest_sha256 = ?, latest_at = ?, latest_schema = ?, latest_bytes = ?, tried_at = ?, problem = NULL WHERE id = 1")
|
|
21117
|
+
.run(outcome.copy.file, outcome.copy.sha256, outcome.copy.madeAt, outcome.copy.schemaVersion, outcome.copy.bytes, now.toISOString());
|
|
21118
|
+
}
|
|
21119
|
+
else {
|
|
21120
|
+
store.handle.prepare("UPDATE live_backup SET tried_at = ?, problem = ? WHERE id = 1").run(now.toISOString(), plainText(outcome.problem, 300));
|
|
21121
|
+
}
|
|
21122
|
+
}
|
package/dist/surface.js
CHANGED
|
@@ -110,7 +110,9 @@ const GUIDE_ROWS = [
|
|
|
110
110
|
{ name: "limit", takesValue: true, meaning: "<step>=<minutes>: one waiting step's time limit" },
|
|
111
111
|
{ name: "tap", takesValue: true, meaning: "the Homebrew tap, owner/name" },
|
|
112
112
|
{ name: "release-gate", takesValue: true, meaning: "<id>: show why an update pause left before a deployment's swap is safe to lift" },
|
|
113
|
-
{ name: "yes", takesValue: false, meaning: "with --release-gate: lift that one pause once it is proved" }
|
|
113
|
+
{ name: "yes", takesValue: false, meaning: "with --release-gate: lift that one pause once it is proved; with --recover-deploy: recover" },
|
|
114
|
+
{ name: "recover-deploy", takesValue: false, meaning: "put back a deployment that stopped half way: stop only its own proved leftovers, restart the previous service, lift the pause" },
|
|
115
|
+
{ name: "stage", takesValue: true, meaning: "with --recover-deploy: the deployment's staging folder" }],
|
|
114
116
|
notableReasons: ["usage", "ambiguous", "drift", "timeout", "check-failed", "ci-failed", "tree-mismatch", "conflict", "mixed-runtime", "gate-held", "after-swap"] },
|
|
115
117
|
operator("demo", "a seeded throwaway sandbox"),
|
|
116
118
|
...KNOWLEDGE_DESCRIPTORS.map(spec => ({ invocation: `knowledge ${spec.action}`, synopsis: spec.synopsis, audience: "audience" in spec ? spec.audience : "agent", agentMayInvoke: !("audience" in spec), mutation: spec.mutation, flags: spec.flags, ...(spec.takesQuery ? { positionals: [{ name: "query", required: true, meaning: "search text or source file for impact" }] } : {}) })),
|
package/dist/task-base.d.ts
CHANGED
|
@@ -33,3 +33,7 @@ export declare function freshBaseFor(store: Store, taskId: string, branch: strin
|
|
|
33
33
|
};
|
|
34
34
|
} | Record<string, never>;
|
|
35
35
|
export declare function taskBaseOf(store: Store, taskId: string, git?: GitRead): TaskBaseView | null;
|
|
36
|
+
/** `git`'s answers kept for `ttlMs` (at most `entries` of them), for a page that shows the base line on every refresh:
|
|
37
|
+
* each read still goes through the same frozen view when it runs; only its answer is reused for a moment. Display only —
|
|
38
|
+
* never for a decision. */
|
|
39
|
+
export declare function recentGitReads(git?: GitRead, ttlMs?: number, entries?: number, now?: () => number): GitRead;
|
package/dist/task-base.js
CHANGED
|
@@ -4,10 +4,13 @@
|
|
|
4
4
|
* branch's pinned origin, or the newest plan's base — and the behind count is
|
|
5
5
|
* read from Git now. Reads only.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { viewedGitSync } from "./teardown-git.js";
|
|
8
8
|
export const readGit = (cwd, args) => {
|
|
9
9
|
try {
|
|
10
|
-
|
|
10
|
+
// Every git version through a frozen read view, bounded (eval-harness review 1, finding 7): a partial clone, an
|
|
11
|
+
// include or a command-naming key is refused from the config git then runs with.
|
|
12
|
+
const ran = viewedGitSync(cwd, args, { timeoutMs: 5_000, maxBuffer: 65_536 });
|
|
13
|
+
return ran.status === 0 ? ran.stdout.trim() : null;
|
|
11
14
|
}
|
|
12
15
|
catch {
|
|
13
16
|
return null;
|
|
@@ -49,3 +52,20 @@ export function taskBaseOf(store, taskId, git = readGit) {
|
|
|
49
52
|
const view = { base, branch, main: behind === null ? null : main, behind };
|
|
50
53
|
return { ...view, words: `Base ${baseStandingWords(view)}` };
|
|
51
54
|
}
|
|
55
|
+
/** `git`'s answers kept for `ttlMs` (at most `entries` of them), for a page that shows the base line on every refresh:
|
|
56
|
+
* each read still goes through the same frozen view when it runs; only its answer is reused for a moment. Display only —
|
|
57
|
+
* never for a decision. */
|
|
58
|
+
export function recentGitReads(git = readGit, ttlMs = 15_000, entries = 64, now = Date.now) {
|
|
59
|
+
const kept = new Map();
|
|
60
|
+
return (cwd, args) => {
|
|
61
|
+
const key = JSON.stringify([cwd, args]), at = now(), hit = kept.get(key);
|
|
62
|
+
if (hit !== undefined && at - hit.at < ttlMs && at >= hit.at)
|
|
63
|
+
return hit.value;
|
|
64
|
+
const value = git(cwd, args);
|
|
65
|
+
kept.delete(key);
|
|
66
|
+
if (kept.size >= entries)
|
|
67
|
+
kept.delete(kept.keys().next().value);
|
|
68
|
+
kept.set(key, { at, value });
|
|
69
|
+
return value;
|
|
70
|
+
};
|
|
71
|
+
}
|
package/dist/teardown-git.d.ts
CHANGED
|
@@ -38,7 +38,7 @@ export declare function teardownGit(runner?: Runner, binding?: GitBinding): Git;
|
|
|
38
38
|
* git at any file, a pipe that never ends among them. Each file is read only as the regular file it is, bounded; one
|
|
39
39
|
* that can't be read so counts as including.
|
|
40
40
|
*/
|
|
41
|
-
export declare function checkoutIncludes(checkout: string): boolean;
|
|
41
|
+
export declare function checkoutIncludes(checkout: string, gitFile?: string): boolean;
|
|
42
42
|
export type GitBinding = {
|
|
43
43
|
repo?: string | null;
|
|
44
44
|
};
|
|
@@ -50,7 +50,7 @@ export declare const WRITE_VIEW_FLAGS: readonly string[];
|
|
|
50
50
|
/** Whether the checkout's .lfsconfig (read as git parses it, that file only) sets any git-lfs key that could name a
|
|
51
51
|
* program (anything but INERT_LFS). A file that
|
|
52
52
|
* isn't a plain one, or doesn't parse, counts as naming one. */
|
|
53
|
-
export declare function lfsExtensionIn(checkout: string): boolean;
|
|
53
|
+
export declare function lfsExtensionIn(checkout: string, gitFile?: string): boolean;
|
|
54
54
|
/** Every command runs against a view of the checkout's repository (gitView) whose config and info are copies the
|
|
55
55
|
* plane made, inspected there, so neither can change between the inspection and the run. `mode` "read" is for reading
|
|
56
56
|
* a checkout an agent may still be writing (the code page): every command also takes readFlags(), and the view holds
|
|
@@ -69,7 +69,7 @@ export declare const PAGE_READ_FLAGS: readonly string[];
|
|
|
69
69
|
* its config as it starts, so the flags alone don't stop a filter planted in both after the inspection: every read
|
|
70
70
|
* also runs against a view of the checkout's repository (gitView; checkoutGitSync, checkoutGit "read"), whose config and info are the plane's. The cost is cosmetic: a checkout's line-ending or encoding rules are not applied, so
|
|
71
71
|
* a file they would normalize can read as changed (fail closed: shipping then asks for a clean commit). */
|
|
72
|
-
export declare function readFlags(): readonly string[];
|
|
72
|
+
export declare function readFlags(gitFile?: string): readonly string[];
|
|
73
73
|
/**
|
|
74
74
|
* checkoutGit, synchronously, for reads a page shows from a checkout an agent writes (docs/plans/get-side-effects.md):
|
|
75
75
|
* the same refusals (an include, an .lfsconfig extension, a config key that can run a command), no system or global
|
|
@@ -81,6 +81,30 @@ export declare function checkoutGitSync(cwd: string, args: readonly string[], op
|
|
|
81
81
|
maxBuffer?: number;
|
|
82
82
|
binding?: GitBinding;
|
|
83
83
|
}): string;
|
|
84
|
+
/**
|
|
85
|
+
* checkoutGitSync's run, answering git's exit status instead of throwing on a non-zero one (a refusal, or a repository
|
|
86
|
+
* that changed while git ran, still throws): for reads whose "no" is an exit code (`merge-base --is-ancestor`), and,
|
|
87
|
+
* with `buffer`, for blobs read as bytes (their output is never rewritten).
|
|
88
|
+
*/
|
|
89
|
+
export declare function viewedGitSync(cwd: string, args: readonly string[], options: {
|
|
90
|
+
timeoutMs?: number;
|
|
91
|
+
maxBuffer?: number;
|
|
92
|
+
binding?: GitBinding;
|
|
93
|
+
}): {
|
|
94
|
+
status: number | null;
|
|
95
|
+
stdout: string;
|
|
96
|
+
};
|
|
97
|
+
export declare function viewedGitSync(cwd: string, args: readonly string[], options: {
|
|
98
|
+
timeoutMs?: number;
|
|
99
|
+
maxBuffer?: number;
|
|
100
|
+
binding?: GitBinding;
|
|
101
|
+
buffer: true;
|
|
102
|
+
input?: Buffer;
|
|
103
|
+
git?: string;
|
|
104
|
+
}): {
|
|
105
|
+
status: number | null;
|
|
106
|
+
stdout: Buffer;
|
|
107
|
+
};
|
|
84
108
|
/**
|
|
85
109
|
* checkoutGit ("build": the repository written through its view) for callers that name the repository with a leading
|
|
86
110
|
* `git -C <dir>` instead of a cwd: worktree add and remove in a project's repository, a merge in a checkout of it.
|
|
@@ -88,3 +112,28 @@ export declare function checkoutGitSync(cwd: string, args: readonly string[], op
|
|
|
88
112
|
* what they name. Anything but git goes to `inner` as given.
|
|
89
113
|
*/
|
|
90
114
|
export declare function hardenedGit(inner?: Runner, mode?: "build" | "read", binding?: GitBinding): Runner;
|
|
115
|
+
/** Asked once per git executable (eval-harness review 2, follow-up): a fixed git given to viewedGitSync is probed
|
|
116
|
+
* itself, never answered for by the one PATH names. */
|
|
117
|
+
export declare function lazyFetchSupported(gitFile?: string): boolean;
|
|
118
|
+
/** For tests: forget the probes' answers, or set one for every executable (null forgets). */
|
|
119
|
+
export declare function setLazyFetchSupportedForTest(value: boolean | null): void;
|
|
120
|
+
/** The words for a partial clone refused on a git without `--no-lazy-fetch`. */
|
|
121
|
+
export declare const PARTIAL_CLONE_REFUSAL = "this project is a partial clone; protected reads need git 2.45 or newer";
|
|
122
|
+
/** `--no-lazy-fetch` where the installed git has it, else nothing (and lazyFetchSetting refuses partial clones). */
|
|
123
|
+
export declare function noLazyFetch(): readonly string[];
|
|
124
|
+
/**
|
|
125
|
+
* The repository setting (local or worktree scope of a `git config --list --show-scope -z` listing) that makes lazy
|
|
126
|
+
* fetching possible, or null: `extensions.partialClone` set at all, any `remote.<name>.promisor` not plainly false, or
|
|
127
|
+
* any `remote.<name>.partialCloneFilter` set (git makes that remote a promisor). Fails closed: a listing that can't be
|
|
128
|
+
* read names a setting. `remote.<name>.vcs` (a foreign-VCS helper) is named too, though the classifier already refuses
|
|
129
|
+
* it (commandSetting: not in the remote allowlist), as it does every `protocol.*` key and every include.
|
|
130
|
+
*/
|
|
131
|
+
export declare function lazyFetchSetting(listing: string): string | null;
|
|
132
|
+
/**
|
|
133
|
+
* For the reads that run git directly on a repository rather than through a view: with `--no-lazy-fetch` they run as
|
|
134
|
+
* they always have (the flag stops any fetch). Without it, they must not check a repository's config and then run git
|
|
135
|
+
* on it (the config could change in between, or an include could set what the check never saw): `inner`, wrapped so
|
|
136
|
+
* every git command runs through a frozen read view (checkoutGit "read"), whose own config listing (includes refused,
|
|
137
|
+
* command and protocol settings refused, a partial clone refused) is the config git then runs with.
|
|
138
|
+
*/
|
|
139
|
+
export declare function lazyFetchSafeGit(inner?: Runner): Runner;
|