toolroll 0.9.61 → 0.9.63
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 +46 -0
- package/README.md +51 -3
- package/dist/agent-fence.d.ts +155 -10
- package/dist/agent-fence.js +473 -38
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +21 -21
- package/dist/browser-workspace.d.ts +28 -0
- package/dist/chat-actions.js +2 -2
- package/dist/check-levels.js +3 -0
- package/dist/claim.js +1 -1
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +1 -0
- package/dist/cli.js +31 -11
- package/dist/contracts/cli.d.ts +8 -0
- package/dist/contracts/cli.js +3 -1
- package/dist/contracts/console-api.d.ts +19 -1
- package/dist/contracts/console-api.js +2 -1
- package/dist/contracts/integration-metadata.d.ts +2 -1
- package/dist/contracts/integration-metadata.js +2 -1
- package/dist/desktop-update.d.ts +1 -1
- package/dist/dispatch.js +7 -4
- package/dist/doctor.d.ts +53 -0
- package/dist/doctor.js +427 -0
- package/dist/evidence-pack.d.ts +1 -1
- package/dist/evidence-pack.js +2 -2
- package/dist/exec.d.ts +3 -0
- package/dist/exec.js +5 -4
- package/dist/flow-code.d.ts +2 -2
- package/dist/flow-code.js +4 -15
- package/dist/flow-engine.js +11 -6
- package/dist/flow-triggers.d.ts +8 -4
- package/dist/flow-triggers.js +45 -25
- package/dist/flow-unattended-words.d.ts +28 -0
- package/dist/flow-unattended-words.js +50 -0
- package/dist/flow-unattended.d.ts +227 -0
- package/dist/flow-unattended.js +776 -0
- package/dist/flows-cli.d.ts +28 -1
- package/dist/flows-cli.js +55 -10
- package/dist/flows-ui.d.ts +2 -1
- package/dist/flows-ui.js +4 -1
- package/dist/google-mail.d.ts +1 -0
- package/dist/google-mail.js +61 -18
- package/dist/integrations-ui.d.ts +2 -1
- package/dist/integrations-ui.js +6 -3
- package/dist/integrations.d.ts +17 -3
- package/dist/integrations.js +38 -18
- package/dist/invoke.js +300 -168
- package/dist/landed-cleanup.d.ts +26 -12
- package/dist/landed-cleanup.js +199 -85
- package/dist/lead-contract.d.ts +1 -1
- package/dist/lead-contract.js +3 -3
- package/dist/lead-tools.js +11 -3
- package/dist/mcp-connect.d.ts +8 -7
- package/dist/mcp-connect.js +49 -51
- package/dist/oauth-run-token.d.ts +51 -0
- package/dist/oauth-run-token.js +127 -0
- package/dist/operate.js +74 -17
- package/dist/plan-auto.d.ts +11 -0
- package/dist/plan-auto.js +62 -11
- package/dist/process-recovery-eligibility.d.ts +1 -1
- package/dist/project-access.d.ts +3 -0
- package/dist/project-access.js +3 -0
- package/dist/project-admission.d.ts +10 -6
- package/dist/project-admission.js +33 -11
- package/dist/project-delete-ui.d.ts +1 -0
- package/dist/project-delete-ui.js +4 -1
- package/dist/project-delete.d.ts +3 -1
- package/dist/project-delete.js +10 -4
- package/dist/project-file.d.ts +3 -0
- package/dist/project-file.js +16 -0
- package/dist/project-tools.d.ts +99 -25
- package/dist/project-tools.js +394 -98
- package/dist/provider-home.d.ts +22 -0
- package/dist/provider-home.js +99 -0
- package/dist/provider.d.ts +17 -3
- package/dist/provider.js +34 -10
- package/dist/remote-main.d.ts +61 -0
- package/dist/remote-main.js +179 -0
- package/dist/scope.d.ts +1 -1
- package/dist/serve.js +11 -3
- package/dist/server/flows.js +27 -5
- package/dist/server/render-tasks.d.ts +3 -0
- package/dist/server/render-tasks.js +3 -0
- package/dist/server/route-table.js +3 -0
- package/dist/server/runtime.d.ts +1 -1
- package/dist/server/settings.js +14 -5
- package/dist/server/task-views.js +2 -0
- package/dist/storage-sweep-child.js +3 -1
- package/dist/storage-sweep-process.d.ts +14 -0
- package/dist/storage-sweep-process.js +37 -3
- package/dist/storage-sweep-schedule.d.ts +30 -0
- package/dist/storage-sweep-schedule.js +164 -0
- package/dist/storage-sweep.d.ts +6 -5
- package/dist/storage-sweep.js +10 -70
- package/dist/storage-ui.d.ts +2 -0
- package/dist/storage-ui.js +7 -4
- package/dist/store.d.ts +75 -7
- package/dist/store.js +241 -17
- package/dist/subagent-tools.d.ts +1 -1
- package/dist/surface.js +10 -6
- package/dist/task-base.d.ts +35 -0
- package/dist/task-base.js +51 -0
- package/dist/teardown-git.d.ts +14 -0
- package/dist/teardown-git.js +119 -0
- package/dist/tool-broker.d.ts +37 -0
- package/dist/tool-broker.js +369 -0
- package/dist/tool-launcher.js +5 -4
- package/dist/tool-secret-store.d.ts +170 -0
- package/dist/tool-secret-store.js +834 -0
- package/dist/toolroll-update.js +4 -2
- package/dist/tools-ui.d.ts +7 -1
- package/dist/tools-ui.js +17 -8
- package/dist/work-index.js +5 -3
- package/dist/worktree.d.ts +64 -6
- package/dist/worktree.js +171 -35
- package/package.json +3 -1
- package/scripts/provider-canary.mjs +210 -31
package/dist/store.js
CHANGED
|
@@ -74,9 +74,10 @@ import { isProviderId } from "./provider.js";
|
|
|
74
74
|
import { authenticate as runnerAuthenticate } from "./runner.js";
|
|
75
75
|
import { stampNewProjectLevel } from "./check-levels.js";
|
|
76
76
|
import { parseCheckProgressSnapshot } from "./check-progress.js";
|
|
77
|
-
import { normalizeProjectAccess, projectAccessAllows, readProjectAccess } from "./project-access.js";
|
|
77
|
+
import { accountReachesSql, normalizeProjectAccess, projectAccessAllows, readProjectAccess } from "./project-access.js";
|
|
78
78
|
import { LEDGER_COLUMNS, LEDGER_SCHEMA, LEDGER_TABLE, LEDGER_V99_TABLE, installLedgerTriggers } from "./action-ledger.js";
|
|
79
79
|
import { PLAN_AUTO_SCHEMA } from "./plan-auto.js";
|
|
80
|
+
import { endUnattendedGrants, FLOW_UNATTENDED_SCHEMA, grantStandsSql } from "./flow-unattended.js";
|
|
80
81
|
import { RECIPE_SCHEMA } from "./recipes.js";
|
|
81
82
|
import { POLICY_SCHEMA, agentRefusal, approvalRefusal, policyParts, readPolicy, underCeiling, sessionCeilingRefusal } from "./policy.js";
|
|
82
83
|
import { PROVIDER_AUTH_SCHEMA } from "./provider-auth.js";
|
|
@@ -355,6 +356,22 @@ CREATE TABLE IF NOT EXISTS run_activity (
|
|
|
355
356
|
at TEXT NOT NULL
|
|
356
357
|
);
|
|
357
358
|
`;
|
|
359
|
+
/** A task branch moved to current main before an attempt (fresh base; v122, a new table: additive only): the branch
|
|
360
|
+
* had no commits of its own and no changes, so it started over from `to_rev`. `after_run` is the newest run id when it
|
|
361
|
+
* moved: the branch's whole-task diff runs from the first builder base recorded after it (firstBuilderBase), never from
|
|
362
|
+
* the older main it left. */
|
|
363
|
+
export const BRANCH_BASE_MOVE_SCHEMA = `
|
|
364
|
+
CREATE TABLE IF NOT EXISTS branch_base_move (
|
|
365
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
366
|
+
task_ref INTEGER NOT NULL REFERENCES task_ref(id) ON DELETE CASCADE,
|
|
367
|
+
branch TEXT NOT NULL,
|
|
368
|
+
from_rev TEXT NOT NULL,
|
|
369
|
+
to_rev TEXT NOT NULL,
|
|
370
|
+
after_run INTEGER NOT NULL,
|
|
371
|
+
moved_at TEXT NOT NULL
|
|
372
|
+
);
|
|
373
|
+
CREATE INDEX IF NOT EXISTS branch_base_move_branch ON branch_base_move (task_ref, branch, id);
|
|
374
|
+
`;
|
|
358
375
|
/** Pausing a running Claude build (live steering, v121: additive only). A person's Pause asks the build to
|
|
359
376
|
* stop its current turn and keep its session: one row per request, bound to the exact run it was asked of, under the
|
|
360
377
|
* request id the asker chose (a repeated request is the same request). `note` is the steering note a Pause and steer
|
|
@@ -459,6 +476,23 @@ CREATE TABLE IF NOT EXISTS integration_check (
|
|
|
459
476
|
error_at TEXT
|
|
460
477
|
);
|
|
461
478
|
`;
|
|
479
|
+
/** Tool secrets (no version bump): which sealing key Toolroll made for the secrets it keeps in the keychain, as a
|
|
480
|
+
* digest of the key, never the key. A key found beside them that isn't this one was put there by something else and is
|
|
481
|
+
* refused (tool-secret-store.ts). */
|
|
482
|
+
export const TOOL_SECRET_KEY_SCHEMA = `
|
|
483
|
+
CREATE TABLE IF NOT EXISTS tool_secret_key (
|
|
484
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
485
|
+
digest TEXT NOT NULL CHECK (length(digest) = 64),
|
|
486
|
+
made_at TEXT NOT NULL
|
|
487
|
+
);
|
|
488
|
+
-- Where secrets were kept before Toolroll's folder, decided once at this database's first start (null: none) and never
|
|
489
|
+
-- probed again, so a folder a build makes later under an old name is never migrated in.
|
|
490
|
+
CREATE TABLE IF NOT EXISTS tool_secret_legacy (
|
|
491
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
492
|
+
dir TEXT,
|
|
493
|
+
decided_at TEXT NOT NULL
|
|
494
|
+
);
|
|
495
|
+
`;
|
|
462
496
|
/** Sprint 8: database backups. One settings row (off, or every so many hours, keeping the newest N in a folder; no
|
|
463
497
|
* folder means `backups` beside the database), every backup's outcome, and a lease the console's backup loop holds
|
|
464
498
|
* and renews while it runs: one process backs up at a time, and a restore sees that the console is running. */
|
|
@@ -605,7 +639,13 @@ const REGISTERED_AT_FILING = "again, as it was filed";
|
|
|
605
639
|
// v121 adds, and changes no saved row: the Agents board (run.attempt_key; agent_turn, agent_now, agent_question,
|
|
606
640
|
// agent_seen, agent_view), live steering (a note's live identity and sends, a run's stdin session; run_pause and
|
|
607
641
|
// run_resume) and landed work's teardown journal (landed_cleanup, landed_cleanup_item).
|
|
608
|
-
|
|
642
|
+
// v122 adds, and changes no saved row: the remote URL an owner's project add recorded (project.remote_url), the only
|
|
643
|
+
// remote whose main automatic completion trusts (remote-main.ts; saved projects read none until it is recorded), and
|
|
644
|
+
// fresh base's record of a task branch moved to current main before an attempt (branch_base_move).
|
|
645
|
+
// v123 adds, and changes no saved row: flows that run on their own (flow_script_provenance, flow_unattended_grant,
|
|
646
|
+
// flow_unattended_run, plan_authorization.basis, flow_trigger_event.on_time and ran_script) and the tool-secret
|
|
647
|
+
// sealing key's record (tool_secret_key, tool_secret_legacy).
|
|
648
|
+
export const SCHEMA_VERSION = 123;
|
|
609
649
|
/** v118: the tables merged into `credential`, in the order they move (a person's API tokens keep their ids first). */
|
|
610
650
|
export const V118_MERGED_TABLES = Object.freeze({ api_token: "person", coordinator_credential: "coordinator", lead_credential: "lead" });
|
|
611
651
|
/** The foreign keys that named a merged table, exactly as their DDL wrote them; each names `credential(id)` instead. */
|
|
@@ -730,9 +770,12 @@ const V115_DROPPED_REFERENCES = /\s+REFERENCES (?:contestant|contest|tournament_
|
|
|
730
770
|
* (planning_generation on task_ref, run and plan_revision, defaulting to 0, and decision.superseded_reason, null);
|
|
731
771
|
* the rehearsal checks every saved row reads the declared default (HISTORY_RULES' added columns). v120 only adds
|
|
732
772
|
* three nullable task_steer columns (the running build's steering folder), checked the same way. v121 only adds
|
|
733
|
-
* new tables and the columns in V121_ADDED_COLUMNS (null, or no sends), checked the same way.
|
|
773
|
+
* new tables and the columns in V121_ADDED_COLUMNS (null, or no sends), checked the same way. v122 only adds a new
|
|
774
|
+
* table (branch_base_move) and the nullable column in V122_ADDED_COLUMNS (a project's recorded remote), checked the
|
|
775
|
+
* same way. v123 only adds new tables (unattended flows, tool secrets) and the columns in V123_ADDED_COLUMNS ('mode',
|
|
776
|
+
* 0 or null), checked the same way.
|
|
734
777
|
*/
|
|
735
|
-
export const UPDATE_SAFE_MIGRATIONS = Object.freeze([108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121]);
|
|
778
|
+
export const UPDATE_SAFE_MIGRATIONS = Object.freeze([108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123]);
|
|
736
779
|
/** v119: the columns it adds, by table, with the value every row saved before it reads (toolroll-update's HISTORY_RULES). */
|
|
737
780
|
export const V119_ADDED_COLUMNS = Object.freeze({
|
|
738
781
|
task_ref: { planning_generation: { definition: "INTEGER NOT NULL DEFAULT 0", saved: "0" } },
|
|
@@ -778,6 +821,21 @@ export const V121_ADDED_COLUMNS = Object.freeze({
|
|
|
778
821
|
task_steer: LIVE_STEER_COLUMNS.task_steer,
|
|
779
822
|
run: { ...LIVE_STEER_COLUMNS.run, attempt_key: { definition: "TEXT", saved: "NULL" } },
|
|
780
823
|
});
|
|
824
|
+
/** v122: the column it adds, with the value every row saved before it reads: the remote URL an owner's project add
|
|
825
|
+
* recorded (projects added before it read none, until `repos add <project> --remote <url>` records one). */
|
|
826
|
+
export const V122_ADDED_COLUMNS = Object.freeze({
|
|
827
|
+
project: { remote_url: { definition: "TEXT", saved: "NULL" } },
|
|
828
|
+
});
|
|
829
|
+
/** v123: the columns it adds, with the value every row saved before it reads: which authority armed a plan's
|
|
830
|
+
* unchanged-plan approval (saved rows read the operating mode), whether a trigger's firing was the clock's own (saved
|
|
831
|
+
* firings read not), and what code an on-time script firing a grant signed for ran (saved firings read none). */
|
|
832
|
+
export const V123_ADDED_COLUMNS = Object.freeze({
|
|
833
|
+
plan_authorization: { basis: { definition: "TEXT NOT NULL DEFAULT 'mode' CHECK (basis IN ('mode', 'grant'))", saved: "'mode'" } },
|
|
834
|
+
flow_trigger_event: {
|
|
835
|
+
on_time: { definition: "INTEGER NOT NULL DEFAULT 0", saved: "0" },
|
|
836
|
+
ran_script: { definition: "TEXT", saved: "NULL" },
|
|
837
|
+
},
|
|
838
|
+
});
|
|
781
839
|
/** Whether a database settled at `version` reaches this build's schema through update-safe migrations alone. */
|
|
782
840
|
export function updateSafeSchema(version) {
|
|
783
841
|
if (version === null || !Number.isSafeInteger(version) || version <= 0 || version > SCHEMA_VERSION)
|
|
@@ -3501,6 +3559,8 @@ function initializeStore(db, file) {
|
|
|
3501
3559
|
db.exec(LEDGER_CHAIN_SCHEMA);
|
|
3502
3560
|
db.exec(MONITORING_SCHEMA);
|
|
3503
3561
|
db.exec(INTEGRATIONS_SCHEMA);
|
|
3562
|
+
// v123: the tool-secret sealing key's record and where secrets were kept before (tool-secret-store.ts): new tables.
|
|
3563
|
+
db.exec(TOOL_SECRET_KEY_SCHEMA);
|
|
3504
3564
|
db.exec(LEAD_ASK_SCHEMA);
|
|
3505
3565
|
// Decisions in the chat app (no version bump: additive only): result, plan and merge buttons, and Request changes.
|
|
3506
3566
|
db.exec(CHAT_DECIDE_SCHEMA);
|
|
@@ -3508,6 +3568,8 @@ function initializeStore(db, file) {
|
|
|
3508
3568
|
db.exec(CHAT_APPROVAL_SCHEMA);
|
|
3509
3569
|
// What a running agent did last (no version bump: additive only; absent means no word yet).
|
|
3510
3570
|
db.exec(RUN_ACTIVITY_SCHEMA);
|
|
3571
|
+
// v122: a task branch moved to current main before an attempt (fresh base): a new table, nothing carried over.
|
|
3572
|
+
db.exec(BRANCH_BASE_MOVE_SCHEMA);
|
|
3511
3573
|
// v121: the Agents board: each run's attempt key (V121_ADDED_COLUMNS, below), its turn history and current snapshot,
|
|
3512
3574
|
// the agent's open questions, who has seen a finished run and who is watching one now. Older runs read null keys.
|
|
3513
3575
|
db.exec(AGENT_BOARD_SCHEMA);
|
|
@@ -3540,6 +3602,12 @@ function initializeStore(db, file) {
|
|
|
3540
3602
|
db.exec(STORAGE_SWEEP_SCHEMA);
|
|
3541
3603
|
// v121: landed work's teardown journal (landed-cleanup.ts): new tables.
|
|
3542
3604
|
db.exec(LANDED_CLEANUP_SCHEMA);
|
|
3605
|
+
// v122: the remote URL an owner's project add recorded (V122_ADDED_COLUMNS; additive only, saved projects read none):
|
|
3606
|
+
// automatic completion trusts only that remote's main (remote-main.ts), never the shared repository's config.
|
|
3607
|
+
for (const [table, columns] of Object.entries(V122_ADDED_COLUMNS)) {
|
|
3608
|
+
for (const [column, { definition }] of Object.entries(columns))
|
|
3609
|
+
addColumn(db, table, column, definition);
|
|
3610
|
+
}
|
|
3543
3611
|
db.exec(BACKUP_SCHEMA);
|
|
3544
3612
|
// Sprint 8: the organisation policy (one row, or none: nothing restricted).
|
|
3545
3613
|
db.exec(POLICY_SCHEMA);
|
|
@@ -3579,6 +3647,16 @@ function initializeStore(db, file) {
|
|
|
3579
3647
|
// v115: the task each standing-order firing filed, kept on the firing itself (a card's task changes as it moves).
|
|
3580
3648
|
addColumn(db, "flow_trigger_event", "task", "TEXT");
|
|
3581
3649
|
db.exec(PLAN_AUTO_SCHEMA);
|
|
3650
|
+
// v123: a flow that runs on its own (flow-unattended.ts): its grants and daily runs (new tables), and which authority
|
|
3651
|
+
// armed a plan's unchanged-plan approval (plan_authorization.basis, V123_ADDED_COLUMNS: saved rows read the mode).
|
|
3652
|
+
db.exec(FLOW_UNATTENDED_SCHEMA);
|
|
3653
|
+
// v123: whether a trigger's firing was the clock's own (flow_trigger_event.on_time, V123_ADDED_COLUMNS: a schedule
|
|
3654
|
+
// due, not Run now; only those can run on their own), and what code an on-time script firing a grant signed for ran
|
|
3655
|
+
// (flow_trigger_event.ran_script, V123_ADDED_COLUMNS: flow-unattended.ts scriptRanDigestOf; older firings read null).
|
|
3656
|
+
for (const [table, columns] of Object.entries(V123_ADDED_COLUMNS)) {
|
|
3657
|
+
for (const [column, { definition }] of Object.entries(columns))
|
|
3658
|
+
addColumn(db, table, column, definition);
|
|
3659
|
+
}
|
|
3582
3660
|
// v119: planning generations, and why a question closed unanswered (additive only; saved rows read the defaults).
|
|
3583
3661
|
for (const [table, columns] of Object.entries(V119_ADDED_COLUMNS)) {
|
|
3584
3662
|
for (const [column, { definition }] of Object.entries(columns))
|
|
@@ -3712,6 +3790,8 @@ function createBaseline(db, file) {
|
|
|
3712
3790
|
}
|
|
3713
3791
|
db.exec(BASELINE_SCHEMA);
|
|
3714
3792
|
db.prepare("INSERT INTO schema_version (version) VALUES (?)").run(BASELINE_SCHEMA_VERSION);
|
|
3793
|
+
const installedAt = new Date().toISOString();
|
|
3794
|
+
db.prepare("INSERT INTO installation_fact (key, value, created_at) VALUES ('installed-at', ?, ?)").run(installedAt, installedAt);
|
|
3715
3795
|
db.exec("COMMIT");
|
|
3716
3796
|
}
|
|
3717
3797
|
catch (error) {
|
|
@@ -4782,6 +4862,36 @@ export class Store {
|
|
|
4782
4862
|
close() {
|
|
4783
4863
|
this.db.close();
|
|
4784
4864
|
}
|
|
4865
|
+
/** The digest of the sealing key Toolroll made for its tool secrets; null before it made one (or in an older database). */
|
|
4866
|
+
toolSecretKeyDigest() {
|
|
4867
|
+
if (!tableExists(this.db, "tool_secret_key"))
|
|
4868
|
+
return null;
|
|
4869
|
+
const row = this.db.prepare("SELECT digest FROM tool_secret_key WHERE id = 1").get();
|
|
4870
|
+
return row === undefined ? null : String(row["digest"]);
|
|
4871
|
+
}
|
|
4872
|
+
/**
|
|
4873
|
+
* The folder secrets were kept in before Toolroll's own, as decided at this database's first start of a command that
|
|
4874
|
+
* can start builds (`record`): `probe` runs until then, and its answer (null: none) is kept for every later start.
|
|
4875
|
+
* Other commands never write, so a preview stays read-only and a busy database doesn't stop them.
|
|
4876
|
+
*/
|
|
4877
|
+
toolSecretLegacyDir(probe, now, options) {
|
|
4878
|
+
const read = () => tableExists(this.db, "tool_secret_legacy") ? this.db.prepare("SELECT dir FROM tool_secret_legacy WHERE id = 1").get() : undefined;
|
|
4879
|
+
let row = read();
|
|
4880
|
+
if (row === undefined) {
|
|
4881
|
+
if (!options.record)
|
|
4882
|
+
return probe();
|
|
4883
|
+
this.db.prepare("INSERT OR IGNORE INTO tool_secret_legacy (id, dir, decided_at) VALUES (1, ?, ?)").run(probe(), now.toISOString());
|
|
4884
|
+
row = read();
|
|
4885
|
+
}
|
|
4886
|
+
return row?.["dir"] === null || row?.["dir"] === undefined ? null : String(row["dir"]);
|
|
4887
|
+
}
|
|
4888
|
+
/** Record a new sealing key's digest only if the one recorded is still `expected` (null: none yet). */
|
|
4889
|
+
swapToolSecretKeyDigest(expected, next, now) {
|
|
4890
|
+
const changed = expected === null
|
|
4891
|
+
? this.db.prepare("INSERT OR IGNORE INTO tool_secret_key (id, digest, made_at) VALUES (1, ?, ?)").run(next, now.toISOString()).changes
|
|
4892
|
+
: this.db.prepare("UPDATE tool_secret_key SET digest = ?, made_at = ? WHERE id = 1 AND digest = ?").run(next, now.toISOString(), expected).changes;
|
|
4893
|
+
return Number(changed) === 1;
|
|
4894
|
+
}
|
|
4785
4895
|
// ---- the built-in task store -------------------------------------------
|
|
4786
4896
|
/**
|
|
4787
4897
|
* Creating a task also creates its overlay reference, because a task with no
|
|
@@ -6308,7 +6418,9 @@ export class Store {
|
|
|
6308
6418
|
? `Approved by ${currentActor()?.lead === true ? "the lead" : lifecycleWords(by, 40)}. A connected worker can take it next.`
|
|
6309
6419
|
: basis.kind === "chat"
|
|
6310
6420
|
? `Approved by ${lifecycleWords(by, 40)} in chat. A connected worker can take it next.`
|
|
6311
|
-
:
|
|
6421
|
+
: basis.kind === "grant"
|
|
6422
|
+
? "Approved automatically: its flow runs on its own. A connected worker can take it next."
|
|
6423
|
+
: "Approved automatically under the operating mode. A connected worker can take it next.",
|
|
6312
6424
|
link: chatControlHref("task", taskId),
|
|
6313
6425
|
}, now);
|
|
6314
6426
|
}
|
|
@@ -6339,16 +6451,19 @@ export class Store {
|
|
|
6339
6451
|
* scopes answer true: this belt only ever REMOVES mode authority.
|
|
6340
6452
|
*/
|
|
6341
6453
|
modeApprovalLive(taskRef, now) {
|
|
6454
|
+
// A flow's "runs on its own" grant (flow-unattended.ts) is held to the same belt.
|
|
6342
6455
|
const dead = this.db
|
|
6343
6456
|
.prepare(`SELECT 1 AS dead FROM task_scope
|
|
6344
6457
|
JOIN task_ref ON task_ref.id = ? AND task_ref.backend = 'built-in' AND task_scope.task_id = task_ref.external_id
|
|
6345
|
-
WHERE task_scope.
|
|
6458
|
+
WHERE task_scope.approved_at IS NOT NULL
|
|
6459
|
+
AND ((task_scope.approval_basis = 'mode'
|
|
6346
6460
|
AND NOT EXISTS (SELECT 1 FROM operating_mode om
|
|
6347
6461
|
JOIN approver signer ON signer.name = om.signed_by
|
|
6348
6462
|
WHERE om.repo = task_ref.repo AND om.revoked_at IS NULL
|
|
6349
6463
|
AND om.absolute_expiry > ? AND om.digest = task_scope.mode_digest
|
|
6350
|
-
AND signer.revoked_at IS NULL AND signer.role = 'approver')
|
|
6351
|
-
|
|
6464
|
+
AND signer.revoked_at IS NULL AND signer.role = 'approver' AND ${accountReachesSql("signer", "task_ref.repo")}))
|
|
6465
|
+
OR (task_scope.approval_basis = 'grant' AND NOT ${grantStandsSql("task_scope.mode_digest", "?", "task_ref.repo")}))`)
|
|
6466
|
+
.get(taskRef, now.toISOString(), now.toISOString());
|
|
6352
6467
|
return dead === undefined;
|
|
6353
6468
|
}
|
|
6354
6469
|
// ---- approvers ----------------------------------------------------------
|
|
@@ -6475,6 +6590,29 @@ export class Store {
|
|
|
6475
6590
|
return true;
|
|
6476
6591
|
});
|
|
6477
6592
|
}
|
|
6593
|
+
/** A flow's "runs on its own" grant ended (revoked, expired, renewed): the undispatched work it sealed falls back
|
|
6594
|
+
* to a person, as a mode's does, and plans it armed lose their pre-authorization. Running and finished work keeps
|
|
6595
|
+
* its history. */
|
|
6596
|
+
withdrawGrantApprovals(digest, now) {
|
|
6597
|
+
const demotable = `WHERE approval_basis = 'grant' AND approved_at IS NOT NULL AND mode_digest = ?
|
|
6598
|
+
AND EXISTS (SELECT 1 FROM task_ref JOIN task ON task.id = task_ref.external_id
|
|
6599
|
+
WHERE task_ref.backend = 'built-in' AND task_ref.external_id = task_scope.task_id AND task.state = 'queued')
|
|
6600
|
+
AND NOT EXISTS (SELECT 1 FROM claim JOIN task_ref tr ON tr.id = claim.task_ref
|
|
6601
|
+
WHERE tr.backend = 'built-in' AND tr.external_id = task_scope.task_id
|
|
6602
|
+
AND claim.released_at IS NULL AND claim.expires_at > ?)`;
|
|
6603
|
+
const demoted = this.db.prepare(`SELECT task_id FROM task_scope ${demotable}`).all(digest, now.toISOString()).map(row => String(row["task_id"]));
|
|
6604
|
+
this.db
|
|
6605
|
+
.prepare(`UPDATE task_scope SET approved_at = NULL, approved_by = NULL, approved_digest = NULL,
|
|
6606
|
+
approved_profile_json = NULL, approval_basis = 'password', mode_digest = NULL ${demotable}`)
|
|
6607
|
+
.run(digest, now.toISOString());
|
|
6608
|
+
this.db.prepare("DELETE FROM plan_authorization WHERE basis = 'grant' AND mode_digest = ?").run(digest);
|
|
6609
|
+
for (const taskId of demoted) {
|
|
6610
|
+
const ref = this.lookupRef(taskId);
|
|
6611
|
+
if (ref === null)
|
|
6612
|
+
continue;
|
|
6613
|
+
this.noteLifecycle({ taskRef: ref.id, kind: "approval-withdrawn", identity: `t${ref.id}`, subject: "Approval needs renewing", body: "Its flow no longer runs on its own. Approve it before it runs.", link: chatControlHref("approval", taskId) }, now);
|
|
6614
|
+
}
|
|
6615
|
+
}
|
|
6478
6616
|
/** F2/R-REVOKE: the ONE reconciliation road. newMode null = no live
|
|
6479
6617
|
* automerge authority (revocation, expiry, or a notify posture):
|
|
6480
6618
|
* mode-basis pending/claimed intents drop to waiting-human, leases
|
|
@@ -6699,6 +6837,7 @@ export class Store {
|
|
|
6699
6837
|
for (const row of this.db.prepare("SELECT repo FROM operating_mode WHERE signed_by = ? AND revoked_at IS NULL").all(name)) {
|
|
6700
6838
|
this.revokeMode(String(row["repo"]), by, "operator", now);
|
|
6701
6839
|
}
|
|
6840
|
+
endUnattendedGrants(this, { signer: name }, by, "access changed", now);
|
|
6702
6841
|
this.revokeDerivedAuthority(name, by, now);
|
|
6703
6842
|
for (const repo of [...new Set([...(account.projects ?? []), ...(access ?? [])])]) {
|
|
6704
6843
|
this.recordAction({ at: now.toISOString(), actor: by, repo, taskId: null, runId: null,
|
|
@@ -6726,6 +6865,10 @@ export class Store {
|
|
|
6726
6865
|
ON CONFLICT(repo) DO UPDATE SET not_requester = excluded.not_requester, protect_project = excluded.protect_project, protected_paths = excluded.protected_paths, updated_by = excluded.updated_by, updated_at = excluded.updated_at`)
|
|
6727
6866
|
.run(repo, next.notRequester ? 1 : 0, next.protectProject ? 1 : 0, JSON.stringify(next.protectedPaths), by, now.toISOString());
|
|
6728
6867
|
this.recordPolicy(by, repo, "approval rules changed", rulesWords(before), rulesWords(next), now);
|
|
6868
|
+
// Protected work is always a person's: a flow here (or building here) stops running on its own, and its queued
|
|
6869
|
+
// approvals are withdrawn (flow-unattended.ts). The dispatch belts check the rules again for any race.
|
|
6870
|
+
if (isProtectedWork(next, []))
|
|
6871
|
+
endUnattendedGrants(this, { repo }, by, "project protected", now);
|
|
6729
6872
|
});
|
|
6730
6873
|
}
|
|
6731
6874
|
/** Who filed a task, when anyone knows. */
|
|
@@ -12140,13 +12283,46 @@ export class Store {
|
|
|
12140
12283
|
* legacy first-attempt behavior, unchanged byte-for-byte.
|
|
12141
12284
|
*/
|
|
12142
12285
|
firstBuilderBase(taskRef, branch) {
|
|
12286
|
+
// A branch moved to current main (fresh base) had no commits of its own:
|
|
12287
|
+
// its origin is the first base recorded after the move, not the older main.
|
|
12143
12288
|
const row = this.db
|
|
12144
12289
|
.prepare(`SELECT base_revision FROM run
|
|
12145
12290
|
WHERE task_ref = ? AND branch = ? AND role = 'builder' AND base_revision IS NOT NULL
|
|
12291
|
+
AND id > COALESCE((SELECT after_run FROM branch_base_move WHERE task_ref = ? AND branch = ? ORDER BY id DESC LIMIT 1), 0)
|
|
12146
12292
|
ORDER BY id ASC LIMIT 1`)
|
|
12147
|
-
.get(taskRef, branch);
|
|
12293
|
+
.get(taskRef, branch, taskRef, branch);
|
|
12294
|
+
return row === undefined ? null : row.base_revision;
|
|
12295
|
+
}
|
|
12296
|
+
/** Where a task branch's attempts start now: a builder branch's pinned base, or the newest base any other attempt on
|
|
12297
|
+
* it recorded (a planner never commits). Null before any attempt on the branch recorded one, or after a move to
|
|
12298
|
+
* current main until the next attempt records its base. */
|
|
12299
|
+
recordedBranchBase(taskRef, branch) {
|
|
12300
|
+
const pinned = this.firstBuilderBase(taskRef, branch);
|
|
12301
|
+
if (pinned !== null)
|
|
12302
|
+
return pinned;
|
|
12303
|
+
const row = this.db
|
|
12304
|
+
.prepare(`SELECT base_revision FROM run
|
|
12305
|
+
WHERE task_ref = ? AND branch = ? AND role <> 'builder' AND base_revision IS NOT NULL
|
|
12306
|
+
AND id > COALESCE((SELECT after_run FROM branch_base_move WHERE task_ref = ? AND branch = ? ORDER BY id DESC LIMIT 1), 0)
|
|
12307
|
+
ORDER BY id DESC LIMIT 1`)
|
|
12308
|
+
.get(taskRef, branch, taskRef, branch);
|
|
12148
12309
|
return row === undefined ? null : row.base_revision;
|
|
12149
12310
|
}
|
|
12311
|
+
/** Record a task branch's move to current main, before git moves it: a move that then fails leaves the next
|
|
12312
|
+
* attempt's own base (still the old one) as the branch's origin, which is exactly right. */
|
|
12313
|
+
recordBranchBaseMove(taskRef, branch, from, to, now) {
|
|
12314
|
+
this.db
|
|
12315
|
+
.prepare(`INSERT INTO branch_base_move (task_ref, branch, from_rev, to_rev, after_run, moved_at)
|
|
12316
|
+
VALUES (?, ?, ?, ?, (SELECT COALESCE(MAX(id), 0) FROM run), ?)`)
|
|
12317
|
+
.run(taskRef, branch, from, to, now.toISOString());
|
|
12318
|
+
}
|
|
12319
|
+
/** The newest move of a task branch to current main, or null. */
|
|
12320
|
+
latestBranchBaseMove(taskRef, branch) {
|
|
12321
|
+
const row = this.db
|
|
12322
|
+
.prepare("SELECT from_rev, to_rev, after_run, moved_at FROM branch_base_move WHERE task_ref = ? AND branch = ? ORDER BY id DESC LIMIT 1")
|
|
12323
|
+
.get(taskRef, branch);
|
|
12324
|
+
return row === undefined ? null : { from: String(row["from_rev"]), to: String(row["to_rev"]), afterRun: Number(row["after_run"]), movedAt: String(row["moved_at"]) };
|
|
12325
|
+
}
|
|
12150
12326
|
finishRun(id, result) {
|
|
12151
12327
|
this.transact(() => {
|
|
12152
12328
|
// Read before the write (Telegram task updates): only the OPEN → closed
|
|
@@ -12256,6 +12432,12 @@ export class Store {
|
|
|
12256
12432
|
.prepare("UPDATE run SET auth_mode = COALESCE(auth_mode, ?), terminal_class = ? WHERE id = ? AND terminal_class IS NULL")
|
|
12257
12433
|
.run(authMode, terminalClass, id);
|
|
12258
12434
|
}
|
|
12435
|
+
/** A launch refused before any model turn (an installed CLI too old for a
|
|
12436
|
+
* build's flags): the start stamp goes, its version and evidence stay. Only
|
|
12437
|
+
* while the run is open, so a settled run's account never changes. */
|
|
12438
|
+
withdrawProviderStart(id) {
|
|
12439
|
+
this.db.prepare("UPDATE run SET provider_started_at = NULL WHERE id = ? AND outcome IS NULL").run(id);
|
|
12440
|
+
}
|
|
12259
12441
|
/** The race-refusal stamp (Phase 3 C5): the version that was REFUSED,
|
|
12260
12442
|
* recorded without provider_started_at — no provider process started. */
|
|
12261
12443
|
stampProviderVersion(id, providerVersion) {
|
|
@@ -14859,6 +15041,8 @@ export class Store {
|
|
|
14859
15041
|
const version = (current?.version ?? Number(this.db.prepare("SELECT COALESCE(MAX(version), 0) AS v FROM flow_script WHERE repo = ? AND name = ?").get(script.repo, script.name)?.["v"] ?? 0)) + 1;
|
|
14860
15042
|
if (current !== null)
|
|
14861
15043
|
this.db.prepare("UPDATE flow_script SET state = 'removed' WHERE id = ?").run(current.id);
|
|
15044
|
+
// A "runs on its own" grant signed this script's code: a new version ends it (flow-unattended.ts).
|
|
15045
|
+
endUnattendedGrants(this, { script: { repo: script.repo, name: script.name } }, script.by, "script changed", now);
|
|
14862
15046
|
this.db.prepare(`INSERT INTO flow_script (repo, name, about, body, timeout_minutes, version, digest, saved_by, saved_at, state, language, file, held)
|
|
14863
15047
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', ?, ?, ?)`).run(script.repo, script.name, script.about, script.body, script.timeoutMinutes, version, script.digest, script.by, now.toISOString(), script.language ?? "shell", script.file ?? null, script.held ?? null);
|
|
14864
15048
|
return version;
|
|
@@ -14869,9 +15053,14 @@ export class Store {
|
|
|
14869
15053
|
const { changes } = this.db.prepare("UPDATE flow_script SET held = NULL WHERE repo = ? AND name = ? AND state = 'active' AND held IS NOT NULL").run(repo, name);
|
|
14870
15054
|
return Number(changes) === 1;
|
|
14871
15055
|
}
|
|
14872
|
-
removeFlowScript(repo, name) {
|
|
14873
|
-
|
|
14874
|
-
|
|
15056
|
+
removeFlowScript(repo, name, by = "flow", now = new Date()) {
|
|
15057
|
+
return this.transact(() => {
|
|
15058
|
+
const { changes } = this.db.prepare("UPDATE flow_script SET state = 'removed' WHERE repo = ? AND name = ? AND state = 'active'").run(repo, name);
|
|
15059
|
+
// A grant that signed this script's code covers nothing without it: it ends (flow-unattended.ts).
|
|
15060
|
+
if (Number(changes) === 1)
|
|
15061
|
+
endUnattendedGrants(this, { script: { repo, name } }, by, "script changed", now);
|
|
15062
|
+
return Number(changes) === 1;
|
|
15063
|
+
});
|
|
14875
15064
|
}
|
|
14876
15065
|
/** Claim a card's visit to a step zone. False when another pass holds it or it already finished. */
|
|
14877
15066
|
claimFlowStep(step, now) {
|
|
@@ -14960,6 +15149,13 @@ export class Store {
|
|
|
14960
15149
|
// ---- flow triggers (v82) -----------------------------------------------------
|
|
14961
15150
|
addFlowTrigger(trigger, now) {
|
|
14962
15151
|
const stamp = now.toISOString();
|
|
15152
|
+
// A new schedule is one the flow's "runs on its own" grant never read: it ends (flow-unattended.ts).
|
|
15153
|
+
if (trigger.kind === "schedule")
|
|
15154
|
+
return this.transact(() => {
|
|
15155
|
+
endUnattendedGrants(this, { flow: trigger.flow }, trigger.by, "schedule changed", now);
|
|
15156
|
+
return Number(this.db.prepare("INSERT INTO flow_trigger (flow, kind, config_json, state, hook_hash, cursor, next_at, created_by, created_at, updated_at) VALUES (?, ?, ?, 'active', ?, ?, ?, ?, ?, ?)")
|
|
15157
|
+
.run(trigger.flow, trigger.kind, trigger.configJson, trigger.hookHash, trigger.cursor, trigger.nextAt, trigger.by, stamp, stamp).lastInsertRowid);
|
|
15158
|
+
});
|
|
14963
15159
|
return Number(this.db.prepare("INSERT INTO flow_trigger (flow, kind, config_json, state, hook_hash, cursor, next_at, created_by, created_at, updated_at) VALUES (?, ?, ?, 'active', ?, ?, ?, ?, ?, ?)")
|
|
14964
15160
|
.run(trigger.flow, trigger.kind, trigger.configJson, trigger.hookHash, trigger.cursor, trigger.nextAt, trigger.by, stamp, stamp).lastInsertRowid);
|
|
14965
15161
|
}
|
|
@@ -14980,11 +15176,17 @@ export class Store {
|
|
|
14980
15176
|
const row = this.db.prepare("SELECT t.* FROM flow_trigger t JOIN flow f ON f.id = t.flow WHERE t.hook_hash = ? AND t.state = 'active' AND f.state = 'active'").get(hash);
|
|
14981
15177
|
return row === undefined ? null : readFlowTriggerRow(row);
|
|
14982
15178
|
}
|
|
14983
|
-
updateFlowTrigger(id, change, now) {
|
|
15179
|
+
updateFlowTrigger(id, change, now, by = "a person") {
|
|
14984
15180
|
const trigger = this.getFlowTrigger(id);
|
|
14985
15181
|
if (trigger === null)
|
|
14986
15182
|
return;
|
|
14987
|
-
|
|
15183
|
+
// Pausing, resuming or removing a schedule ends the flow's "runs on its own" grant (flow-unattended.ts).
|
|
15184
|
+
const write = () => this.db.prepare("UPDATE flow_trigger SET state = ?, cursor = ?, next_at = ?, last_at = ?, last_outcome = ?, failures = ?, hook_hash = ?, updated_at = ? WHERE id = ?").run(change.state ?? trigger.state, change.cursor === undefined ? trigger.cursor : change.cursor, change.nextAt === undefined ? trigger.nextAt : change.nextAt, change.lastAt ?? trigger.lastAt, change.lastOutcome ?? trigger.lastOutcome, change.failures ?? trigger.failures, change.hookHash === undefined ? trigger.hookHash : change.hookHash, now.toISOString(), id);
|
|
15185
|
+
if (trigger.kind !== "schedule" || change.state === undefined || change.state === trigger.state) {
|
|
15186
|
+
write();
|
|
15187
|
+
return;
|
|
15188
|
+
}
|
|
15189
|
+
this.transact(() => { endUnattendedGrants(this, { flow: trigger.flow }, by, "schedule changed", now); write(); });
|
|
14988
15190
|
}
|
|
14989
15191
|
/** Whether a trigger already handled this outside thing (an issue, a run, a schedule slot). */
|
|
14990
15192
|
flowTriggerSaw(trigger, key) {
|
|
@@ -15005,8 +15207,8 @@ export class Store {
|
|
|
15005
15207
|
* Record what a trigger did with one outside thing: the card it made, or why it made none, and (a standing order's
|
|
15006
15208
|
* firing) the task it filed. False when it was already recorded.
|
|
15007
15209
|
*/
|
|
15008
|
-
recordFlowTriggerEvent(trigger, key, card, note, now, task = null) {
|
|
15009
|
-
const { changes } = this.db.prepare("INSERT OR IGNORE INTO flow_trigger_event (trigger, key, card, note, at, task) VALUES (?, ?, ?, ?, ?, ?)").run(trigger, key, card, note, now.toISOString(), task);
|
|
15210
|
+
recordFlowTriggerEvent(trigger, key, card, note, now, task = null, onTime = false, ranScript = null) {
|
|
15211
|
+
const { changes } = this.db.prepare("INSERT OR IGNORE INTO flow_trigger_event (trigger, key, card, note, at, task, on_time, ran_script) VALUES (?, ?, ?, ?, ?, ?, ?, ?)").run(trigger, key, card, note, now.toISOString(), task, onTime ? 1 : 0, ranScript);
|
|
15010
15212
|
return Number(changes) === 1;
|
|
15011
15213
|
}
|
|
15012
15214
|
/** The last card a trigger made, if any. */
|
|
@@ -15594,17 +15796,38 @@ export class Store {
|
|
|
15594
15796
|
// ---- projects ------------------------------------------------------------
|
|
15595
15797
|
/** Remember a project was opened. Upsert keeps added_at; recency always moves.
|
|
15596
15798
|
* A project added for the first time starts on Quick checks. */
|
|
15597
|
-
upsertProject(path, name, now) {
|
|
15799
|
+
upsertProject(path, name, now, admission) {
|
|
15598
15800
|
this.transact(() => {
|
|
15599
15801
|
const added = this.db.prepare("SELECT 1 FROM project WHERE path = ?").get(path) === undefined;
|
|
15600
15802
|
this.db
|
|
15601
15803
|
.prepare(`INSERT INTO project (path, name, added_at, last_opened_at) VALUES (?, ?, ?, ?)
|
|
15602
15804
|
ON CONFLICT (path) DO UPDATE SET name = excluded.name, last_opened_at = excluded.last_opened_at`)
|
|
15603
15805
|
.run(path, name, now.toISOString(), now.toISOString());
|
|
15806
|
+
// Admission owns remote changes; ordinary openings preserve the saved value.
|
|
15807
|
+
if (admission !== undefined)
|
|
15808
|
+
this.db.prepare("UPDATE project SET remote_url = ? WHERE path = ?").run(admission.remoteUrl, path);
|
|
15604
15809
|
if (added)
|
|
15605
15810
|
stampNewProjectLevel(this, path, now);
|
|
15606
15811
|
});
|
|
15607
15812
|
}
|
|
15813
|
+
/** Includes removed/legacy projects, so deleting a row never makes build-writable config authoritative again. */
|
|
15814
|
+
projectAdmissionKnown(path) {
|
|
15815
|
+
return this.db.prepare(`SELECT 1 FROM project WHERE path = ?
|
|
15816
|
+
UNION ALL SELECT 1 FROM action_ledger WHERE repo = ?
|
|
15817
|
+
UNION ALL SELECT 1 FROM task_ref WHERE repo = ?
|
|
15818
|
+
UNION ALL SELECT 1 FROM worktree WHERE repo = ? LIMIT 1`).get(path, path, path, path) !== undefined;
|
|
15819
|
+
}
|
|
15820
|
+
/** Full structured URL audit, without the generic ledger's display-text truncation. */
|
|
15821
|
+
recordProjectRemote(actor, repo, previous, remoteUrl, now) {
|
|
15822
|
+
this.db.prepare(`INSERT INTO action_ledger(at,actor,repo,task_id,run_id,action,outcome,source,detail)
|
|
15823
|
+
VALUES (?,?,?,NULL,NULL,'project remote recorded','recorded','access',?)`)
|
|
15824
|
+
.run(now.toISOString(), actor, repo, JSON.stringify({ previousRemote: previous, remoteUrl }));
|
|
15825
|
+
}
|
|
15826
|
+
/** The remote URL captured on admission or explicitly supplied by the owner; null when none was (or the project was removed). */
|
|
15827
|
+
projectRemote(path) {
|
|
15828
|
+
const url = this.db.prepare("SELECT remote_url FROM project WHERE path = ?").get(path)?.["remote_url"];
|
|
15829
|
+
return typeof url === "string" && url !== "" ? url : null;
|
|
15830
|
+
}
|
|
15608
15831
|
/** A project removed from the lists (project-admission.ts): only its row goes; its tasks, results and settings stay. */
|
|
15609
15832
|
forgetProject(path) {
|
|
15610
15833
|
this.db.prepare("DELETE FROM project WHERE path = ?").run(path);
|
|
@@ -15619,6 +15842,7 @@ export class Store {
|
|
|
15619
15842
|
name: String(row["name"]),
|
|
15620
15843
|
addedAt: String(row["added_at"]),
|
|
15621
15844
|
lastOpenedAt: String(row["last_opened_at"]),
|
|
15845
|
+
remoteUrl: typeof row["remote_url"] === "string" ? row["remote_url"] : null,
|
|
15622
15846
|
}));
|
|
15623
15847
|
}
|
|
15624
15848
|
/**
|
package/dist/subagent-tools.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export type ToolLister = (spec: ToolSpec, values: Record<string, string>) => Pro
|
|
|
12
12
|
export type ToolIo = {
|
|
13
13
|
callTool?: ToolCaller;
|
|
14
14
|
listTools?: ToolLister;
|
|
15
|
-
toolHome?: string;
|
|
15
|
+
toolHome?: string | undefined;
|
|
16
16
|
};
|
|
17
17
|
/** Actions whose names say they only read start as "do it"; everything else asks first until its manager says otherwise. */
|
|
18
18
|
export declare const READ_VERB: RegExp;
|
package/dist/surface.js
CHANGED
|
@@ -67,9 +67,11 @@ const GUIDE_ROWS = [
|
|
|
67
67
|
// D5: the central team service (connect, the central lead verbs, conversation …) and native coding sessions are
|
|
68
68
|
// deprecated: they still run this release, with a warning, but are no longer declared here.
|
|
69
69
|
operator("chat", "talk to your lead: one conversation across every project; --say sends one message; --follow enables crew updates and --no-follow pauses them; proposals keep their approval requirements"),
|
|
70
|
-
|
|
70
|
+
// Unkeyed: --remote with a different URL replaces the project's trusted remote (recorded in the action ledger).
|
|
71
|
+
{ invocation: "repos add", synopsis: "add a project: a Git repository, in the console at once; the lead adds with its token; --remote records or replaces its trusted remote", audience: "agent", agentMayInvoke: true, mutation: "unkeyed",
|
|
71
72
|
positionals: [{ name: "path", required: false, meaning: "the repository (default: the one you are in)" }],
|
|
72
|
-
flags: [jsonFlag, { name: "token", takesValue: true, meaning: "the lead token (or TOOLROLL_LEAD_TOKEN)" }
|
|
73
|
+
flags: [jsonFlag, { name: "token", takesValue: true, meaning: "the lead token (or TOOLROLL_LEAD_TOKEN)" },
|
|
74
|
+
{ name: "remote", takesValue: true, meaning: "an HTTPS or SSH URL without credentials whose main may complete landed work; replaces a different recorded one (owner or an instance operator's lead only)" }],
|
|
73
75
|
notableReasons: ["not-git", "unavailable", "refused", "locked"] },
|
|
74
76
|
{ invocation: "repos remove", synopsis: "remove a project from the console and the builder; its tasks and results stay saved", audience: "agent", agentMayInvoke: true, mutation: "identity-idempotent",
|
|
75
77
|
positionals: [{ name: "path", required: true, meaning: "the repository" }],
|
|
@@ -114,8 +116,9 @@ const GUIDE_ROWS = [
|
|
|
114
116
|
...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" }] } : {}) })),
|
|
115
117
|
...MEMORY_DESCRIPTORS.map(spec => ({ invocation: `memory ${spec.action}`, synopsis: spec.synopsis, audience: "agent", agentMayInvoke: true, mutation: spec.mutation, flags: spec.flags, ...(spec.takesQuery ? { positionals: [{ name: "query", required: true, meaning: "search text, a decision id, or the decision sentence" }] } : {}) })),
|
|
116
118
|
// Flow writes carry an approver's credential: an agent runs them only as the person asked, after showing the preview.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
+
// Letting a flow run on its own raises authority: the owner's own password ceremony, never an agent's act.
|
|
120
|
+
...FLOWS_DESCRIPTORS.map(spec => ({ invocation: `flows ${spec.action}`, synopsis: spec.synopsis, audience: "audience" in spec ? spec.audience : "agent", agentMayInvoke: !("audience" in spec), mutation: spec.mutation, flags: spec.flags,
|
|
121
|
+
...("positionals" in spec ? { positionals: spec.positionals.map(name => ({ name, required: true, meaning: name === "flow" ? "a flow id from flows list" : name === "card" ? "a card id from flows show" : name === "on|off" ? "on signs the grant, off revokes it" : "a trigger id from flows show, or for trigger add its settings as JSON, a JSON file, or -" })) } : {}) })),
|
|
119
122
|
...MODELS_DESCRIPTORS.map(spec => ({ invocation: `models ${spec.action}`, synopsis: spec.synopsis, audience: "agent", agentMayInvoke: true, mutation: spec.mutation, flags: spec.flags, ...(spec.takesQuery ? { positionals: [{ name: "target", required: true, meaning: "the CLI to update, or on/off" }] } : {}) })),
|
|
120
123
|
// ---- the queue (agent surface) ----
|
|
121
124
|
{ invocation: "status", synopsis: "running work, queued reasons, results to review, the latest release check and plan windows", audience: "agent", agentMayInvoke: true, mutation: "none", flags: [jsonFlag, dbFlag] },
|
|
@@ -409,6 +412,7 @@ const GUIDE_ROWS = [
|
|
|
409
412
|
operator("up", "console + worker + browser, one command"),
|
|
410
413
|
operator("onboard", "add this repository, report signed-in agents, install or --remove the operator skill (--yes), offer the starter flows (--starter <ids>), and print the handoff"),
|
|
411
414
|
operator("daemon", "install the loop under the OS service manager"),
|
|
415
|
+
{ ...operatorRead("doctor", "check that Toolroll's service holds no key, token or password a build could read; names what it finds and the steps to fix it, never a value, and changes nothing"), flags: [jsonFlag] },
|
|
412
416
|
];
|
|
413
417
|
/**
|
|
414
418
|
* Whether each row may run on a central server for a person signed in with their own API token (`runOperateAs` in
|
|
@@ -427,7 +431,7 @@ const REMOTE_NO = [
|
|
|
427
431
|
// Workers, the loop and the services that run here.
|
|
428
432
|
"claim", "heartbeat", "worker release", "reap", "tick", "build", "reconcile",
|
|
429
433
|
"runner register", "runner retire", "runner bind", "runner capacity", "coordinator mint", "coordinator revoke", "mcp", "outbox deliver", "peek",
|
|
430
|
-
"serve", "serve check-public", "watch", "up", "onboard", "daemon", "bridge", "setup show", "setup clear",
|
|
434
|
+
"serve", "serve check-public", "watch", "up", "onboard", "daemon", "doctor", "bridge", "setup show", "setup clear",
|
|
431
435
|
// Provider keys and model tools on this machine.
|
|
432
436
|
"providers", "keys status", "keys set", "keys clear", "keys verify", "keys auth", "models check", "models update", "models watch",
|
|
433
437
|
// Publication and release.
|
|
@@ -441,7 +445,7 @@ const REMOTE_NO = [
|
|
|
441
445
|
];
|
|
442
446
|
const REMOTE_STEP_UP = [
|
|
443
447
|
"proposals", "decide", "task regate", "task approve", "task accept", "task merge", "run settle", "knowledge apply", "memory apply",
|
|
444
|
-
"flows card approve", "flows card send-back", "flows script approve",
|
|
448
|
+
"flows card approve", "flows card send-back", "flows script approve", "flows unattended",
|
|
445
449
|
"approver list", "approver add", "people list", "people invite", "people projects", "people revoke",
|
|
446
450
|
// A person's own API tokens: a password step-up on the server's machine, or the console. Never with a token.
|
|
447
451
|
"tokens create", "tokens list", "tokens revoke", "tokens rotate",
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a task's work starts, against current main (fresh base): one view for
|
|
3
|
+
* the task page and `task show`. The base is saved run lineage — a builder
|
|
4
|
+
* branch's pinned origin, or the newest plan's base — and the behind count is
|
|
5
|
+
* read from Git now. Reads only.
|
|
6
|
+
*/
|
|
7
|
+
import type { Store } from "./store.js";
|
|
8
|
+
export type TaskBaseView = {
|
|
9
|
+
/** The base commit, in full. */
|
|
10
|
+
base: string;
|
|
11
|
+
branch: string;
|
|
12
|
+
/** The project's main: its checked-out branch, as a dispatch pass resolves it. Null when Git could not say. */
|
|
13
|
+
main: string | null;
|
|
14
|
+
/** Commits on main the base does not have; null when Git could not say. */
|
|
15
|
+
behind: number | null;
|
|
16
|
+
/** One short line: `Base 9d087efe · Behind main by 2 commits`. */
|
|
17
|
+
words: string;
|
|
18
|
+
};
|
|
19
|
+
/** A synchronous, read-only Git call in `cwd`: its trimmed output, or null on any failure. */
|
|
20
|
+
export type GitRead = (cwd: string, args: readonly string[]) => string | null;
|
|
21
|
+
export declare const readGit: GitRead;
|
|
22
|
+
/** The base line on its own, for surfaces that label it already: `9d087efe · Behind main by 2 commits`. */
|
|
23
|
+
export declare function baseStandingWords(view: Pick<TaskBaseView, "base" | "main" | "behind">): string;
|
|
24
|
+
/**
|
|
25
|
+
* The lease's fresh-base request for one attempt on `branch`, or nothing when the task's start is pinned elsewhere: a
|
|
26
|
+
* revision keeps the head of the attempt it revises, and a prepared candidate or coding review keeps the base its
|
|
27
|
+
* candidate was proved against.
|
|
28
|
+
*/
|
|
29
|
+
export declare function freshBaseFor(store: Store, taskId: string, branch: string, main: string): {
|
|
30
|
+
freshBase: {
|
|
31
|
+
main: string;
|
|
32
|
+
recordedBase: string | null;
|
|
33
|
+
};
|
|
34
|
+
} | Record<string, never>;
|
|
35
|
+
export declare function taskBaseOf(store: Store, taskId: string, git?: GitRead): TaskBaseView | null;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a task's work starts, against current main (fresh base): one view for
|
|
3
|
+
* the task page and `task show`. The base is saved run lineage — a builder
|
|
4
|
+
* branch's pinned origin, or the newest plan's base — and the behind count is
|
|
5
|
+
* read from Git now. Reads only.
|
|
6
|
+
*/
|
|
7
|
+
import { execFileSync } from "node:child_process";
|
|
8
|
+
export const readGit = (cwd, args) => {
|
|
9
|
+
try {
|
|
10
|
+
return execFileSync("git", ["--no-optional-locks", ...args], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5_000, maxBuffer: 65_536 }).trim();
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
const commits = (count) => `${count} commit${count === 1 ? "" : "s"}`;
|
|
17
|
+
/** The base line on its own, for surfaces that label it already: `9d087efe · Behind main by 2 commits`. */
|
|
18
|
+
export function baseStandingWords(view) {
|
|
19
|
+
const short = view.base.slice(0, 8);
|
|
20
|
+
if (view.main === null || view.behind === null)
|
|
21
|
+
return short;
|
|
22
|
+
return view.behind === 0 ? `${short} · Up to date with ${view.main}` : `${short} · Behind ${view.main} by ${commits(view.behind)}`;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The lease's fresh-base request for one attempt on `branch`, or nothing when the task's start is pinned elsewhere: a
|
|
26
|
+
* revision keeps the head of the attempt it revises, and a prepared candidate or coding review keeps the base its
|
|
27
|
+
* candidate was proved against.
|
|
28
|
+
*/
|
|
29
|
+
export function freshBaseFor(store, taskId, branch, main) {
|
|
30
|
+
const ref = store.lookupRef(taskId);
|
|
31
|
+
if (ref === null || ref.revisionOf !== null || store.getScope(taskId)?.candidate != null || store.filedViaOf(taskId)?.startsWith("coding:") === true)
|
|
32
|
+
return {};
|
|
33
|
+
return { freshBase: { main, recordedBase: store.recordedBranchBase(ref.id, branch) } };
|
|
34
|
+
}
|
|
35
|
+
export function taskBaseOf(store, taskId, git = readGit) {
|
|
36
|
+
const ref = store.lookupRef(taskId);
|
|
37
|
+
if (ref === null || ref.repo === null)
|
|
38
|
+
return null;
|
|
39
|
+
const latest = store.runsFor(ref.id).find(one => one.role !== "reviewer" && one.branch !== null && one.baseRevision !== null);
|
|
40
|
+
if (latest === undefined || latest.branch === null || latest.baseRevision === null)
|
|
41
|
+
return null;
|
|
42
|
+
const branch = latest.branch;
|
|
43
|
+
// A move to current main newer than the latest attempt is where the next one starts.
|
|
44
|
+
const move = store.latestBranchBaseMove(ref.id, branch);
|
|
45
|
+
const base = move !== null && move.afterRun >= latest.id ? move.to : store.recordedBranchBase(ref.id, branch) ?? latest.baseRevision;
|
|
46
|
+
const main = git(ref.repo, ["symbolic-ref", "--short", "-q", "HEAD"]) || null;
|
|
47
|
+
const counted = main === null ? null : git(ref.repo, ["rev-list", "--count", `${base}..refs/heads/${main}`]);
|
|
48
|
+
const behind = counted === null || !/^\d+$/.test(counted) ? null : Number(counted);
|
|
49
|
+
const view = { base, branch, main: behind === null ? null : main, behind };
|
|
50
|
+
return { ...view, words: `Base ${baseStandingWords(view)}` };
|
|
51
|
+
}
|