toolroll 0.9.58 → 0.9.59
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 +7 -0
- package/dist/action-ledger.js +10 -1
- package/dist/browser/workspace.js +1 -1
- package/dist/builder.js +4 -2
- package/dist/chat-channel.d.ts +1 -0
- package/dist/chat-channel.js +3 -0
- package/dist/chat-delivery.js +2 -0
- package/dist/contract-changes.d.ts +52 -0
- package/dist/contract-changes.js +61 -0
- package/dist/decision.d.ts +4 -0
- package/dist/decision.js +8 -0
- package/dist/deploy-lock.d.ts +8 -0
- package/dist/deploy-lock.js +45 -0
- package/dist/dispatch.js +1 -1
- package/dist/lead-cli.js +3 -0
- package/dist/lead-doors.js +3 -0
- package/dist/lead-tools.js +2 -1
- package/dist/needs-you.js +3 -1
- package/dist/operate.js +21 -10
- package/dist/password-bearer.d.ts +4 -4
- package/dist/password-bearer.js +2 -2
- package/dist/planner-source.d.ts +23 -53
- package/dist/planner-source.js +43 -68
- package/dist/release-gate.d.ts +2 -3
- package/dist/release-gate.js +44 -16
- package/dist/scope.js +14 -0
- package/dist/server/render-chat.js +10 -0
- package/dist/server/render-tasks.js +19 -9
- package/dist/server/tasks.js +3 -1
- package/dist/store.d.ts +46 -8
- package/dist/store.js +111 -15
- package/dist/task-status.js +3 -1
- package/dist/telegram-lead.js +6 -0
- package/dist/telegram.js +9 -7
- package/dist/toolroll-update.js +8 -1
- package/package.json +1 -1
package/dist/store.js
CHANGED
|
@@ -559,7 +559,10 @@ const REGISTERED_AT_FILING = "again, as it was filed";
|
|
|
559
559
|
// v118 (D7) keeps every machine credential in one table, `credential`: a person's API tokens (so_…), the lead's tokens
|
|
560
560
|
// (lt_…) and coordinators, each with its kind, read/act access, project limit, expiry, revocation and generation. Every
|
|
561
561
|
// row moves with its id, hash and dates; the references to coordinators and API tokens point at it.
|
|
562
|
-
|
|
562
|
+
// v119 keeps planning generations: a rewritten scope starts a clean planning context (task_ref, run and plan_revision
|
|
563
|
+
// carry planning_generation, 0 for all earlier history), and a question closed without an answer says why
|
|
564
|
+
// (decision.superseded_reason: its task's scope changed, or the task was cancelled).
|
|
565
|
+
export const SCHEMA_VERSION = 119;
|
|
563
566
|
/** v118: the tables merged into `credential`, in the order they move (a person's API tokens keep their ids first). */
|
|
564
567
|
export const V118_MERGED_TABLES = Object.freeze({ api_token: "person", coordinator_credential: "coordinator", lead_credential: "lead" });
|
|
565
568
|
/** The foreign keys that named a merged table, exactly as their DDL wrote them; each names `credential(id)` instead. */
|
|
@@ -680,9 +683,18 @@ const V115_DROPPED_REFERENCES = /\s+REFERENCES (?:contestant|contest|tournament_
|
|
|
680
683
|
* teammates' to its subagents' (SQLite's RENAME, every row, id and link carried), renames their teammate column, and rewrites four internal kinds from 'teammate' to 'subagent' in place; the rehearsal checks
|
|
681
684
|
* each renamed table's rows arrived under its new name. v118 (D7) is checked the same way: every row of api_token,
|
|
682
685
|
* coordinator_credential and lead_credential moves into credential under its kind in one transaction that counts them
|
|
683
|
-
* before the old tables drop, and the references that named them name credential in place, their rows untouched.
|
|
686
|
+
* before the old tables drop, and the references that named them name credential in place, their rows untouched. v119 only adds four columns
|
|
687
|
+
* (planning_generation on task_ref, run and plan_revision, defaulting to 0, and decision.superseded_reason, null);
|
|
688
|
+
* the rehearsal checks every saved row reads the declared default (HISTORY_RULES' added columns).
|
|
684
689
|
*/
|
|
685
|
-
export const UPDATE_SAFE_MIGRATIONS = Object.freeze([108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118]);
|
|
690
|
+
export const UPDATE_SAFE_MIGRATIONS = Object.freeze([108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119]);
|
|
691
|
+
/** v119: the columns it adds, by table, with the value every row saved before it reads (toolroll-update's HISTORY_RULES). */
|
|
692
|
+
export const V119_ADDED_COLUMNS = Object.freeze({
|
|
693
|
+
task_ref: { planning_generation: { definition: "INTEGER NOT NULL DEFAULT 0", saved: "0" } },
|
|
694
|
+
run: { planning_generation: { definition: "INTEGER NOT NULL DEFAULT 0", saved: "0" } },
|
|
695
|
+
plan_revision: { planning_generation: { definition: "INTEGER NOT NULL DEFAULT 0", saved: "0" } },
|
|
696
|
+
decision: { superseded_reason: { definition: "TEXT CHECK (superseded_reason IN ('scope-changed','cancelled'))", saved: "NULL" } },
|
|
697
|
+
});
|
|
686
698
|
/** Whether a database settled at `version` reaches this build's schema through update-safe migrations alone. */
|
|
687
699
|
export function updateSafeSchema(version) {
|
|
688
700
|
if (version === null || !Number.isSafeInteger(version) || version <= 0 || version > SCHEMA_VERSION)
|
|
@@ -3474,6 +3486,11 @@ function initializeStore(db, file) {
|
|
|
3474
3486
|
// v115: the task each standing-order firing filed, kept on the firing itself (a card's task changes as it moves).
|
|
3475
3487
|
addColumn(db, "flow_trigger_event", "task", "TEXT");
|
|
3476
3488
|
db.exec(PLAN_AUTO_SCHEMA);
|
|
3489
|
+
// v119: planning generations, and why a question closed unanswered (additive only; saved rows read the defaults).
|
|
3490
|
+
for (const [table, columns] of Object.entries(V119_ADDED_COLUMNS)) {
|
|
3491
|
+
for (const [column, { definition }] of Object.entries(columns))
|
|
3492
|
+
addColumn(db, table, column, definition);
|
|
3493
|
+
}
|
|
3477
3494
|
installLedgerTriggers(db);
|
|
3478
3495
|
// Attention/history indexes come AFTER migration: on a database whose
|
|
3479
3496
|
// constrained tables still carry a pre-rebuild shape, creating a partial
|
|
@@ -4796,6 +4813,10 @@ export class Store {
|
|
|
4796
4813
|
// just the state verb's.
|
|
4797
4814
|
if (ref !== undefined)
|
|
4798
4815
|
this.supersedeSteerNotes(Number(ref["id"]), now);
|
|
4816
|
+
// Its open questions close the same way (v119), typed 'cancelled', with their holds and pages: a question about
|
|
4817
|
+
// work that will never run must leave every decision queue. Answered ones stay exactly as they were.
|
|
4818
|
+
if (ref !== undefined)
|
|
4819
|
+
this.closeQuestionsLocked(Number(ref["id"]), "cancelled", now);
|
|
4799
4820
|
// The reason is part of the transition, not decoration: a
|
|
4800
4821
|
// coordinator-filed task's dismissal writes its durable event IN THIS
|
|
4801
4822
|
// transaction — operator text and typed machine codes alike (MCP spec
|
|
@@ -9942,6 +9963,66 @@ export class Store {
|
|
|
9942
9963
|
return { ok: true };
|
|
9943
9964
|
});
|
|
9944
9965
|
}
|
|
9966
|
+
/**
|
|
9967
|
+
* A rewritten scope starts planning again (v119), in one transaction: the task's planning generation advances, a
|
|
9968
|
+
* requested or drafted plan becomes requested with no strikes, and what the old goal produced leaves the active
|
|
9969
|
+
* planning context — its open questions close (typed 'scope-changed', never a fake answer, their holds and pages
|
|
9970
|
+
* with them) and a revision still waiting for a person is rejected with its hold. The old runs, plans, revisions,
|
|
9971
|
+
* answers and ledger rows all stay: generation-bound reads simply stop offering them to the next planner. Only a
|
|
9972
|
+
* task already planning restarts; false (nothing changed) for any other.
|
|
9973
|
+
*/
|
|
9974
|
+
restartPlanning(taskRef, now) {
|
|
9975
|
+
return this.transact(() => {
|
|
9976
|
+
const moved = this.db
|
|
9977
|
+
.prepare("UPDATE task_ref SET planning_generation = planning_generation + 1, plan = 'requested', plan_strikes = 0 WHERE id = ? AND plan IN ('requested','drafted')")
|
|
9978
|
+
.run(taskRef);
|
|
9979
|
+
if (Number(moved.changes) === 0)
|
|
9980
|
+
return false;
|
|
9981
|
+
const ref = this.db.prepare("SELECT external_id, repo, planning_generation FROM task_ref WHERE id = ?").get(taskRef);
|
|
9982
|
+
const generation = Number(ref["planning_generation"]);
|
|
9983
|
+
this.closeQuestionsLocked(taskRef, "scope-changed", now, generation);
|
|
9984
|
+
const blocked = this.db
|
|
9985
|
+
.prepare("SELECT id FROM plan_revision WHERE task_ref = ? AND status = 'blocked' AND planning_generation < ?")
|
|
9986
|
+
.all(taskRef, generation)
|
|
9987
|
+
.map(row => Number(row["id"]));
|
|
9988
|
+
for (const id of blocked) {
|
|
9989
|
+
this.resolvePlanRevision(id, "rejected", "toolroll", now);
|
|
9990
|
+
this.releaseOwnedHold("revision", String(id));
|
|
9991
|
+
}
|
|
9992
|
+
this.recordAction({
|
|
9993
|
+
at: now.toISOString(), actor: currentActor() === null ? "system" : actorLabel(currentActor()), repo: ref["repo"] === null ? null : String(ref["repo"]),
|
|
9994
|
+
taskId: String(ref["external_id"]), runId: null, action: "planning restarted", outcome: "scope changed", source: "work",
|
|
9995
|
+
});
|
|
9996
|
+
this.bumpWake();
|
|
9997
|
+
return true;
|
|
9998
|
+
});
|
|
9999
|
+
}
|
|
10000
|
+
/**
|
|
10001
|
+
* Close a task's open and expired questions without answering them (v119): typed with why, no choice, their
|
|
10002
|
+
* holds lifted and their pages resolved, so they leave every decision queue. Answered questions are never touched.
|
|
10003
|
+
* `belowGeneration` limits it to questions asked in earlier planning generations. Caller holds the transaction.
|
|
10004
|
+
*/
|
|
10005
|
+
closeQuestionsLocked(taskRef, reason, now, belowGeneration) {
|
|
10006
|
+
const ids = this.db
|
|
10007
|
+
.prepare(`SELECT decision.id FROM decision JOIN run ON run.id = decision.run
|
|
10008
|
+
WHERE run.task_ref = ? AND decision.state IN ('open','expired') AND (? IS NULL OR run.planning_generation < ?)
|
|
10009
|
+
ORDER BY decision.id`)
|
|
10010
|
+
.all(taskRef, belowGeneration ?? null, belowGeneration ?? null)
|
|
10011
|
+
.map(row => Number(row["id"]));
|
|
10012
|
+
const close = this.db.prepare("UPDATE decision SET state = 'answered', answered_at = ?, answered_by = 'toolroll', superseded_reason = ? WHERE id = ? AND state IN ('open','expired')");
|
|
10013
|
+
for (const id of ids) {
|
|
10014
|
+
close.run(now.toISOString(), reason, id);
|
|
10015
|
+
this.releaseOwnedHold("decision", String(id));
|
|
10016
|
+
this.resolveEpisode(`decision:${id}`, now);
|
|
10017
|
+
}
|
|
10018
|
+
return ids;
|
|
10019
|
+
}
|
|
10020
|
+
/** The number the next plan revision of a task takes: one past every revision it ever had, in any planning
|
|
10021
|
+
* generation, so a restarted plan's revisions never collide with the history they follow. */
|
|
10022
|
+
nextPlanRevisionNumber(taskRef) {
|
|
10023
|
+
const row = this.db.prepare("SELECT coalesce(max(revision), 0) AS n FROM plan_revision WHERE task_ref = ?").get(taskRef);
|
|
10024
|
+
return Number(row?.["n"] ?? 0) + 1;
|
|
10025
|
+
}
|
|
9945
10026
|
/** Move the planning state; null clears it. Caller owns the transaction story. */
|
|
9946
10027
|
setPlanState(taskRef, state) {
|
|
9947
10028
|
this.db.prepare("UPDATE task_ref SET plan = ? WHERE id = ?").run(state, taskRef);
|
|
@@ -11653,8 +11734,8 @@ export class Store {
|
|
|
11653
11734
|
})();
|
|
11654
11735
|
const inserted = this.db
|
|
11655
11736
|
.prepare(`INSERT INTO run (task_ref, lease_id, runner, branch, worktree, model, role, provider, parent_run, session_id, quality_mode,
|
|
11656
|
-
started_at, review_attempt)
|
|
11657
|
-
VALUES (
|
|
11737
|
+
started_at, review_attempt, planning_generation)
|
|
11738
|
+
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, (SELECT planning_generation FROM task_ref WHERE id = ?1))`)
|
|
11658
11739
|
.run(run.taskRef, run.leaseId, run.runner, run.branch ?? null, run.worktree ?? null, model, role, provider ?? "claude", run.parentRun ?? null, run.sessionId ?? null, qualityMode, run.now.toISOString(),
|
|
11659
11740
|
// The root reviewer's ordinal (v50) rides the insert: a root and
|
|
11660
11741
|
// its attempt number are one fact, and the ordinal index refuses a
|
|
@@ -12599,12 +12680,14 @@ export class Store {
|
|
|
12599
12680
|
* reads in the order the questions arrived. "unanswered" is open + expired:
|
|
12600
12681
|
* expiry makes a decision louder, never gone.
|
|
12601
12682
|
*/
|
|
12602
|
-
/** The newest planner run's plan document for a task, when one exists
|
|
12683
|
+
/** The newest planner run's plan document for a task, when one exists in its current planning generation (v119:
|
|
12684
|
+
* a plan drafted for a scope since rewritten is history, read by its artifact id). */
|
|
12603
12685
|
latestPlanArtifact(taskRef) {
|
|
12604
12686
|
const row = this.db
|
|
12605
12687
|
.prepare(`SELECT artifact.* FROM artifact
|
|
12606
12688
|
JOIN run ON run.id = artifact.run
|
|
12607
12689
|
WHERE run.task_ref = ? AND run.role = 'planner' AND artifact.kind = 'plan'
|
|
12690
|
+
AND run.planning_generation = (SELECT planning_generation FROM task_ref WHERE id = run.task_ref)
|
|
12608
12691
|
ORDER BY artifact.id DESC LIMIT 1`)
|
|
12609
12692
|
.get(taskRef);
|
|
12610
12693
|
return row === undefined ? null : readArtifact(row);
|
|
@@ -12619,6 +12702,7 @@ export class Store {
|
|
|
12619
12702
|
JOIN run ON run.id = artifact.run
|
|
12620
12703
|
WHERE run.task_ref = ? AND run.role = 'planner' AND artifact.kind = 'plan-contract'
|
|
12621
12704
|
AND artifact.key LIKE '%/plan-contract.json'
|
|
12705
|
+
AND run.planning_generation = (SELECT planning_generation FROM task_ref WHERE id = run.task_ref)
|
|
12622
12706
|
ORDER BY artifact.id DESC LIMIT 1`)
|
|
12623
12707
|
.get(taskRef);
|
|
12624
12708
|
return row === undefined ? null : readArtifact(row);
|
|
@@ -12646,16 +12730,17 @@ export class Store {
|
|
|
12646
12730
|
const inserted = this.db
|
|
12647
12731
|
.prepare(`INSERT INTO plan_revision
|
|
12648
12732
|
(task_ref, revision, artifact, parent_hash, reason, evidence_link, author, origin_run,
|
|
12649
|
-
kind, authority_kind, authority_digest, changed_fields, status, created_at)
|
|
12650
|
-
VALUES (
|
|
12733
|
+
kind, authority_kind, authority_digest, changed_fields, status, created_at, planning_generation)
|
|
12734
|
+
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, (SELECT planning_generation FROM task_ref WHERE id = ?1))`)
|
|
12651
12735
|
.run(revision.taskRef, revision.revision, revision.artifact, revision.parentHash, revision.reason, revision.evidenceLink, revision.author, revision.originRun, revision.kind, revision.authorityKind, revision.authorityDigest, JSON.stringify(revision.changedFields), revision.status, now.toISOString());
|
|
12652
12736
|
return Number(inserted.lastInsertRowid);
|
|
12653
12737
|
}
|
|
12654
|
-
/** Every revision for a task
|
|
12655
|
-
* reader can show without a second table
|
|
12738
|
+
/** Every revision for a task in its current planning generation, newest
|
|
12739
|
+
* first — the immutable history a reader can show without a second table
|
|
12740
|
+
* (v119: an earlier generation's rows stay, read by id). */
|
|
12656
12741
|
listPlanRevisions(taskRef) {
|
|
12657
12742
|
return this.db
|
|
12658
|
-
.prepare("SELECT * FROM plan_revision WHERE task_ref = ? ORDER BY revision DESC")
|
|
12743
|
+
.prepare("SELECT * FROM plan_revision WHERE task_ref = ? AND planning_generation = (SELECT planning_generation FROM task_ref WHERE id = plan_revision.task_ref) ORDER BY revision DESC")
|
|
12659
12744
|
.all(taskRef)
|
|
12660
12745
|
.map(readPlanRevision);
|
|
12661
12746
|
}
|
|
@@ -12666,7 +12751,7 @@ export class Store {
|
|
|
12666
12751
|
* `latestPlanArtifact`'s synthetic revision 1 in both cases. */
|
|
12667
12752
|
currentPlanRevision(taskRef) {
|
|
12668
12753
|
const row = this.db
|
|
12669
|
-
.prepare("SELECT * FROM plan_revision WHERE task_ref = ? AND status = 'applied' ORDER BY revision DESC LIMIT 1")
|
|
12754
|
+
.prepare("SELECT * FROM plan_revision WHERE task_ref = ? AND planning_generation = (SELECT planning_generation FROM task_ref WHERE id = plan_revision.task_ref) AND status = 'applied' ORDER BY revision DESC LIMIT 1")
|
|
12670
12755
|
.get(taskRef);
|
|
12671
12756
|
return row === undefined ? null : readPlanRevision(row);
|
|
12672
12757
|
}
|
|
@@ -12674,7 +12759,7 @@ export class Store {
|
|
|
12674
12759
|
* 'blocked' awaiting an operator's decision — what a reader shows
|
|
12675
12760
|
* alongside the current one to render a pending proposal at all. */
|
|
12676
12761
|
latestPlanRevision(taskRef) {
|
|
12677
|
-
const row = this.db.prepare("SELECT * FROM plan_revision WHERE task_ref = ? ORDER BY revision DESC LIMIT 1").get(taskRef);
|
|
12762
|
+
const row = this.db.prepare("SELECT * FROM plan_revision WHERE task_ref = ? AND planning_generation = (SELECT planning_generation FROM task_ref WHERE id = plan_revision.task_ref) ORDER BY revision DESC LIMIT 1").get(taskRef);
|
|
12678
12763
|
return row === undefined ? null : readPlanRevision(row);
|
|
12679
12764
|
}
|
|
12680
12765
|
getPlanRevision(id) {
|
|
@@ -12762,7 +12847,8 @@ export class Store {
|
|
|
12762
12847
|
.get(taskRef);
|
|
12763
12848
|
return row === undefined ? null : readArtifact(row);
|
|
12764
12849
|
}
|
|
12765
|
-
/** Answered questions for one task, newest first — the planner's memory.
|
|
12850
|
+
/** Answered questions for one task, newest first — the planner's memory. Only real answers from its current
|
|
12851
|
+
* planning generation (v119): a question closed unanswered, or asked about a scope since rewritten, is history. */
|
|
12766
12852
|
answeredDecisionsFor(taskId, limit = 5) {
|
|
12767
12853
|
return this.db
|
|
12768
12854
|
.prepare(`SELECT decision.question, decision.choice, decision.note
|
|
@@ -12770,7 +12856,8 @@ export class Store {
|
|
|
12770
12856
|
JOIN run ON run.id = decision.run
|
|
12771
12857
|
JOIN task_ref ON task_ref.id = run.task_ref
|
|
12772
12858
|
WHERE task_ref.backend = ? AND task_ref.external_id = ?
|
|
12773
|
-
AND decision.answered_at IS NOT NULL
|
|
12859
|
+
AND decision.answered_at IS NOT NULL AND decision.superseded_reason IS NULL
|
|
12860
|
+
AND run.planning_generation = task_ref.planning_generation
|
|
12774
12861
|
ORDER BY decision.answered_at DESC, decision.id DESC LIMIT ?`)
|
|
12775
12862
|
.all(BUILT_IN, taskId, Math.max(1, Math.min(Math.floor(limit), 20)))
|
|
12776
12863
|
.map(row => ({
|
|
@@ -12869,6 +12956,9 @@ export class Store {
|
|
|
12869
12956
|
JOIN task_ref ON task_ref.id = run.task_ref
|
|
12870
12957
|
WHERE run.task_ref = ? AND decision.state = 'answered'
|
|
12871
12958
|
AND decision.contestant IS NULL
|
|
12959
|
+
-- v119: a question closed unanswered carries no answer, and one asked about a rewritten scope is history.
|
|
12960
|
+
AND decision.superseded_reason IS NULL
|
|
12961
|
+
AND run.planning_generation = task_ref.planning_generation
|
|
12872
12962
|
AND NOT EXISTS (
|
|
12873
12963
|
SELECT 1 FROM run_decision
|
|
12874
12964
|
JOIN run AS delivered ON delivered.id = run_decision.run
|
|
@@ -13022,6 +13112,9 @@ export class Store {
|
|
|
13022
13112
|
const existing = this.getDecision(answer.id);
|
|
13023
13113
|
if (existing === null)
|
|
13024
13114
|
return { ok: false, reason: "unknown-decision" };
|
|
13115
|
+
// A question closed without an answer (v119) stays closed: its task's scope changed or the task was cancelled.
|
|
13116
|
+
if (existing.supersededReason !== null)
|
|
13117
|
+
return { ok: false, reason: "closed" };
|
|
13025
13118
|
if (!existing.options.some(option => option.id === answer.choice)) {
|
|
13026
13119
|
return { ok: false, reason: "bad-option" };
|
|
13027
13120
|
}
|
|
@@ -17443,6 +17536,7 @@ function readRun(row) {
|
|
|
17443
17536
|
taskRef: Number(row["task_ref"]),
|
|
17444
17537
|
leaseId: String(row["lease_id"]),
|
|
17445
17538
|
runner: String(row["runner"]),
|
|
17539
|
+
planningGeneration: Number(row["planning_generation"] ?? 0),
|
|
17446
17540
|
watchIncarnation: row["watch_incarnation"] == null ? null : String(row["watch_incarnation"]),
|
|
17447
17541
|
reviewAttempt: wholeNumber(row["review_attempt"]),
|
|
17448
17542
|
role: String(row["role"] ?? "builder"),
|
|
@@ -17575,6 +17669,7 @@ function readDecision(row) {
|
|
|
17575
17669
|
answeredVia: row["answered_via"] === null ? null : String(row["answered_via"]),
|
|
17576
17670
|
choice: row["choice"] === null ? null : String(row["choice"]),
|
|
17577
17671
|
note: row["note"] === null ? null : String(row["note"]),
|
|
17672
|
+
supersededReason: row["superseded_reason"] === "scope-changed" || row["superseded_reason"] === "cancelled" ? row["superseded_reason"] : null,
|
|
17578
17673
|
};
|
|
17579
17674
|
}
|
|
17580
17675
|
function readArtifact(row) {
|
|
@@ -17762,6 +17857,7 @@ function readTaskRef(row) {
|
|
|
17762
17857
|
? null
|
|
17763
17858
|
: String(row["plan"]),
|
|
17764
17859
|
planStrikes: Number(row["plan_strikes"] ?? 0),
|
|
17860
|
+
planningGeneration: Number(row["planning_generation"] ?? 0),
|
|
17765
17861
|
deliverable: row["deliverable"] === "report" ? "report" : "branch",
|
|
17766
17862
|
strikes: Number(row["strikes"] ?? 0),
|
|
17767
17863
|
agentProvider: row["agent_provider"] === null || row["agent_provider"] === undefined
|
package/dist/task-status.js
CHANGED
|
@@ -261,7 +261,9 @@ export const taskStatusOf = statusOf;
|
|
|
261
261
|
* wait on a person stay Needs you; everything else waits in the queue. */
|
|
262
262
|
const CODE_STAGE = {
|
|
263
263
|
queued: ["queued"], ready: ["queued"], "worker-at-capacity": ["queued"], "retry-scheduled": ["queued"], updating: ["queued"],
|
|
264
|
-
"waiting-dependency": ["queued"], "scouting-ready": ["queued"], "planning-ready": ["queued"], "provider-quota": ["queued"],
|
|
264
|
+
"waiting-dependency": ["queued"], "scouting-ready": ["queued"], "planning-ready": ["queued"], "provider-quota": ["queued"],
|
|
265
|
+
// A request the planner cannot be given (too long to hand back, say) waits on a person to shorten it.
|
|
266
|
+
"planner-source": ["needs-you", "fix-request"],
|
|
265
267
|
running: ["building"], reviewing: ["reviewing"], "review-pending": ["building"],
|
|
266
268
|
"signed-out": ["needs-you", "sign-in"],
|
|
267
269
|
"needs-approval": ["needs-you", "approval"],
|
package/dist/telegram-lead.js
CHANGED
|
@@ -204,6 +204,12 @@ export function applyProposalTap(store, binding, token, message, repos, options,
|
|
|
204
204
|
const who = verified.who;
|
|
205
205
|
const preview = proposalPreview(store, proposal, actionRepos, "telegram");
|
|
206
206
|
const origin = options.phoneOrigin?.() ?? null;
|
|
207
|
+
if (preview.closed) {
|
|
208
|
+
store.consumeTelegramProposalActions(proposal.id, now);
|
|
209
|
+
ack("This question is closed.");
|
|
210
|
+
edit(preview.text, []);
|
|
211
|
+
return { effects, confirmed: false, ignored: true };
|
|
212
|
+
}
|
|
207
213
|
if (action.phase === "dismiss") {
|
|
208
214
|
if (!store.consumeTelegramProposalAction(token, now)) {
|
|
209
215
|
ack("that button was already used");
|
package/dist/telegram.js
CHANGED
|
@@ -25,7 +25,7 @@ import { resultImageFileName, resultTaskLabel, verifyResultImage } from "./chat-
|
|
|
25
25
|
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
26
26
|
import { chmodSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
27
27
|
import { join } from "node:path";
|
|
28
|
-
import { validateNote } from "./decision.js";
|
|
28
|
+
import { closedQuestionWords, validateNote } from "./decision.js";
|
|
29
29
|
import { isShotsKind, resultShotsFor } from "./result-shots.js";
|
|
30
30
|
import { isLifecycleNotification, isTelegramProgressNotification, RESULT_SHOTS_KIND, TELEGRAM_HOLD_REASONS } from "./store.js";
|
|
31
31
|
import { telegramProgressCard } from "./telegram-progress.js";
|
|
@@ -1998,7 +1998,7 @@ function applyNote(context, update, effects) {
|
|
|
1998
1998
|
return;
|
|
1999
1999
|
}
|
|
2000
2000
|
if (decision.state === "answered") {
|
|
2001
|
-
say(`already answered: ${decision.choice ?? "?"} — this note did not travel`);
|
|
2001
|
+
say(`${decision.supersededReason !== null ? closedQuestionWords(decision.supersededReason) : `already answered: ${decision.choice ?? "?"}`} — this note did not travel`);
|
|
2002
2002
|
report.ignored++;
|
|
2003
2003
|
return;
|
|
2004
2004
|
}
|
|
@@ -2344,8 +2344,8 @@ function applyCallback(context, update, effects) {
|
|
|
2344
2344
|
}
|
|
2345
2345
|
if (decision.state === "answered") {
|
|
2346
2346
|
store.consumeTelegramAction(token, clock());
|
|
2347
|
-
ack(`already answered: ${decision.choice ?? "?"}`);
|
|
2348
|
-
editText(answeredText(store, decision));
|
|
2347
|
+
ack(decision.supersededReason !== null ? closedQuestionWords(decision.supersededReason) : `already answered: ${decision.choice ?? "?"}`);
|
|
2348
|
+
editText(answeredText(store, decision), []);
|
|
2349
2349
|
return;
|
|
2350
2350
|
}
|
|
2351
2351
|
if (action.phase === "choose") {
|
|
@@ -2451,13 +2451,13 @@ function answerNow(context, decision, choice, binding, ack, editText) {
|
|
|
2451
2451
|
editText(answeredText(store, answered.decision));
|
|
2452
2452
|
return;
|
|
2453
2453
|
}
|
|
2454
|
-
if (answered.reason === "already-answered") {
|
|
2454
|
+
if (answered.reason === "already-answered" || answered.reason === "closed") {
|
|
2455
2455
|
if (draft !== null)
|
|
2456
2456
|
store.setNoteDraftState(draft.id, "discarded");
|
|
2457
2457
|
const settled = store.getDecision(decision.id);
|
|
2458
|
-
ack(`already answered: ${settled?.choice ?? "?"}${draft === null ? "" : " — your note did NOT travel"}`);
|
|
2458
|
+
ack(`${settled?.supersededReason != null ? closedQuestionWords(settled.supersededReason) : `already answered: ${settled?.choice ?? "?"}`}${draft === null ? "" : " — your note did NOT travel"}`);
|
|
2459
2459
|
if (settled !== null)
|
|
2460
|
-
editText(answeredText(store, settled));
|
|
2460
|
+
editText(answeredText(store, settled), []);
|
|
2461
2461
|
return;
|
|
2462
2462
|
}
|
|
2463
2463
|
ack(`could not answer: ${answered.reason}`);
|
|
@@ -2479,6 +2479,8 @@ function armedDecisionText(option, note) {
|
|
|
2479
2479
|
].join("\n");
|
|
2480
2480
|
}
|
|
2481
2481
|
function answeredText(store, decision) {
|
|
2482
|
+
if (decision.supersededReason !== null)
|
|
2483
|
+
return [closedQuestionWords(decision.supersededReason), taskOf(store, decision)].join("\n");
|
|
2482
2484
|
const chosen = decision.options.find(one => one.id === decision.choice)?.label ?? decision.choice ?? "?";
|
|
2483
2485
|
return [
|
|
2484
2486
|
`✓ Answered: ${chosen}`,
|
package/dist/toolroll-update.js
CHANGED
|
@@ -35,7 +35,7 @@ import { processMayBeAlive } from "./process-liveness.js";
|
|
|
35
35
|
import { assertCodingUpdateStopped, backupCodingCatalog, codingCatalogExists, releaseStaleCodingOwner, removeCodingUpdateGate } from "./coding-update.js";
|
|
36
36
|
import { installLaunchdService, launchdPlist, stopLaunchdService, writeFileDurably } from "./daemon.js";
|
|
37
37
|
import { NAME } from "./names.js";
|
|
38
|
-
import { orphanKey, updateSafeSchema, V117_RENAMED_COLUMNS, V117_RENAMED_TABLES, V118_MERGED_TABLES } from "./store.js";
|
|
38
|
+
import { orphanKey, updateSafeSchema, V117_RENAMED_COLUMNS, V117_RENAMED_TABLES, V118_MERGED_TABLES, V119_ADDED_COLUMNS } from "./store.js";
|
|
39
39
|
import { isNewer, REGISTRY } from "./releases.js";
|
|
40
40
|
import { markNeverIndex } from "./never-index.js";
|
|
41
41
|
import { readRuntimeUpdateJournal, RUNTIME_PHASES, RUNTIME_UPDATE_STEPS, stagedStartedAt, updaterStartingOf } from "./contracts/update-journal.js";
|
|
@@ -384,6 +384,9 @@ const HISTORY_RULES = {
|
|
|
384
384
|
...Object.fromEntries(V117_RENAMED_COLUMNS.filter(([table]) => !Object.values(V117_RENAMED_TABLES).includes(table)).map(([table, from, to]) => [table, { renamed: { [from]: to } }])),
|
|
385
385
|
// v118 (D7): every API token, coordinator and lead token arrives in the one credential table under its kind.
|
|
386
386
|
...Object.fromEntries(Object.entries(V118_MERGED_TABLES).map(([table, kind]) => [table, { moved: { into: "credential", where: `kind = '${kind}'` } }])),
|
|
387
|
+
// v119: planning generations and a question's closure reason are new columns; every row saved before them reads
|
|
388
|
+
// the declared value (generation 0, no closure reason).
|
|
389
|
+
...Object.fromEntries(Object.entries(V119_ADDED_COLUMNS).map(([table, columns]) => [table, { added: Object.fromEntries(Object.entries(columns).map(([column, { saved }]) => [column, saved])) }])),
|
|
387
390
|
};
|
|
388
391
|
/** The tables a migration may remove whole (v115's removed features). */
|
|
389
392
|
export const RETIRED_TABLES = Object.freeze(Object.keys(HISTORY_RULES).filter(name => HISTORY_RULES[name].retired));
|
|
@@ -537,6 +540,10 @@ export function changedHistory(db, before) {
|
|
|
537
540
|
if (t.columns.some(c => !kept(c) && (!rule?.dropped?.includes(c) || (t.nonNull?.[c] !== 0 && !accounted(rule, t)))))
|
|
538
541
|
return true;
|
|
539
542
|
const columns = t.columns.filter(c => present.has(c));
|
|
543
|
+
// A declared new column: every row saved before it reads its declared value.
|
|
544
|
+
const added = Object.entries(rule?.added ?? {}).filter(([c]) => !t.columns.includes(c) && present.has(c));
|
|
545
|
+
if (t.rowid && added.some(([c, saved]) => Number(db.prepare("SELECT count(*) AS n FROM " + quote(t.name) + " WHERE rowid <= ? AND " + quote(c) + " IS NOT " + saved).get(t.last)["n"]) > 0))
|
|
546
|
+
return true;
|
|
540
547
|
if (!t.rowid) {
|
|
541
548
|
const after = rowsHash(db, t.name, columns);
|
|
542
549
|
return after.count !== t.count || after.hash !== t.hash;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolroll",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.59",
|
|
4
4
|
"description": "A control plane for unattended coding agents — queue tasks, walk away, come back to pull requests. Agents build in leased worktrees, never touch your default branch, and interrupt you only for decisions that need a human.",
|
|
5
5
|
"directories": {
|
|
6
6
|
"doc": "docs"
|