toolroll 0.9.31 → 0.9.33
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 +30 -0
- package/dist/assignment.d.ts +3 -0
- package/dist/assignment.js +8 -2
- package/dist/batch-checks.d.ts +15 -0
- package/dist/batch-checks.js +244 -0
- package/dist/batch-policy.d.ts +23 -0
- package/dist/batch-policy.js +25 -0
- package/dist/browser/workspace.js +13 -13
- package/dist/builder.js +14 -3
- package/dist/chat-actions.js +2 -2
- package/dist/check-levels-ui.d.ts +4 -0
- package/dist/check-levels-ui.js +21 -2
- package/dist/checkout-cleanup.d.ts +2 -1
- package/dist/checkout-cleanup.js +24 -8
- package/dist/coding-context.js +2 -2
- package/dist/coding-handoff.js +2 -2
- package/dist/desktop-update.d.ts +1 -1
- package/dist/exec.d.ts +2 -0
- package/dist/exec.js +11 -0
- package/dist/guides.js +3 -0
- package/dist/knowledge-cli.d.ts +31 -0
- package/dist/knowledge-cli.js +35 -2
- package/dist/knowledge-ui.d.ts +1 -1
- package/dist/knowledge-ui.js +7 -0
- package/dist/lead-capabilities.d.ts +0 -7
- package/dist/lead-capabilities.js +11 -1
- package/dist/mate-doors.js +2 -2
- package/dist/mate.d.ts +6 -2
- package/dist/mate.js +63 -38
- package/dist/never-index.d.ts +3 -0
- package/dist/never-index.js +21 -0
- package/dist/operate.js +99 -5
- package/dist/orphan-sweep.d.ts +63 -0
- package/dist/orphan-sweep.js +235 -0
- package/dist/project-delete.js +1 -1
- package/dist/project-knowledge.d.ts +28 -3
- package/dist/project-knowledge.js +64 -25
- package/dist/project-learning.d.ts +33 -0
- package/dist/project-learning.js +105 -15
- package/dist/project-skills.js +27 -21
- package/dist/result-follow-ups.d.ts +22 -2
- package/dist/result-follow-ups.js +44 -10
- package/dist/serve.js +33 -11
- package/dist/shared-deps.d.ts +18 -2
- package/dist/shared-deps.js +129 -15
- package/dist/staged-runtimes.d.ts +42 -0
- package/dist/staged-runtimes.js +191 -0
- package/dist/storage-sweep.d.ts +50 -0
- package/dist/storage-sweep.js +203 -0
- package/dist/storage-ui.d.ts +5 -0
- package/dist/storage-ui.js +30 -4
- package/dist/storage.d.ts +2 -0
- package/dist/storage.js +15 -3
- package/dist/store.d.ts +17 -2
- package/dist/store.js +37 -5
- package/dist/subscription-chat.d.ts +3 -1
- package/dist/subscription-chat.js +1 -0
- package/dist/surface.js +2 -1
- package/dist/task-status.d.ts +13 -0
- package/dist/task-status.js +37 -3
- package/dist/team-leads.js +6 -2
- package/dist/team-runtime.js +2 -1
- package/dist/test-temp.d.ts +20 -2
- package/dist/test-temp.js +53 -13
- package/dist/toolroll-update.js +2 -0
- package/dist/workspace-ui.js +3 -1
- package/dist/worktree.js +9 -0
- package/package.json +1 -1
package/dist/project-learning.js
CHANGED
|
@@ -54,13 +54,100 @@ const git = (repo, args, seen) => {
|
|
|
54
54
|
seen?.set(key, out);
|
|
55
55
|
return out;
|
|
56
56
|
};
|
|
57
|
+
const sameValue = (n) => typeof n === 'bigint' ? n.toString() : n;
|
|
57
58
|
/** Path plus the physical shared Git database: replacing a repository does not inherit its lessons. */
|
|
58
|
-
export
|
|
59
|
+
export const identityOf = (repo, common, st) => learningSha(JSON.stringify([repo, common, sameValue(st.ino), st.birthtimeMs]));
|
|
60
|
+
/** The formula before 0.9.33, which also hashed st.dev; macOS may renumber a volume at restart. */
|
|
61
|
+
export const legacyIdentityOf = (repo, common, st) => learningSha(JSON.stringify([repo, common, sameValue(st.dev), sameValue(st.ino), st.birthtimeMs]));
|
|
62
|
+
function identityFacts(repo, seen) {
|
|
59
63
|
if (canonicalProject(repo) !== repo)
|
|
60
64
|
throw new Error('Project identity changed.');
|
|
61
65
|
const common = realpathSync(resolve(repo, git(repo, ['rev-parse', '--git-common-dir'], seen)));
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
return { common, st: statSync(common) };
|
|
67
|
+
}
|
|
68
|
+
export function learningIdentity(repo, seen) {
|
|
69
|
+
const { common, st } = identityFacts(repo, seen);
|
|
70
|
+
return identityOf(repo, common, st);
|
|
71
|
+
}
|
|
72
|
+
/** Projects whose rows have already been checked for a same-repository carry, per store. */
|
|
73
|
+
const reconciled = new WeakMap();
|
|
74
|
+
// Every table that saves a project identity. Rows are never rewritten: a carry aliases them.
|
|
75
|
+
const STORED_IDENTITY = `SELECT 1 FROM project_knowledge WHERE repo=?1 AND identity=?2
|
|
76
|
+
UNION ALL SELECT 1 FROM knowledge_change WHERE repo=?1 AND identity=?2
|
|
77
|
+
UNION ALL SELECT 1 FROM knowledge_snapshot WHERE repo=?1 AND identity=?2
|
|
78
|
+
UNION ALL SELECT 1 FROM project_skill_change WHERE repo=?1 AND identity=?2
|
|
79
|
+
UNION ALL SELECT 1 FROM skill_test JOIN task_ref ON task_ref.id=skill_test.task_ref WHERE task_ref.repo=?1 AND skill_test.identity=?2
|
|
80
|
+
UNION ALL SELECT 1 FROM learning_policy WHERE repo=?1 AND identity=?2
|
|
81
|
+
UNION ALL SELECT 1 FROM project_lesson WHERE repo=?1 AND identity=?2
|
|
82
|
+
UNION ALL SELECT 1 FROM learning_capture WHERE repo=?1 AND identity=?2
|
|
83
|
+
UNION ALL SELECT 1 FROM learning_snapshot WHERE repo=?1 AND identity=?2
|
|
84
|
+
UNION ALL SELECT 1 FROM project_decision WHERE repo=?1 AND identity=?2 LIMIT 1`;
|
|
85
|
+
/**
|
|
86
|
+
* Record that saved project rows under `from` belong to the project now known as `to`.
|
|
87
|
+
* Knowledge saved under `from` also gets a new revision under `to`; older revisions stay as they are.
|
|
88
|
+
* Returns the new knowledge revision, or null when knowledge needed none.
|
|
89
|
+
*/
|
|
90
|
+
export function carryProject(store, repo, args, now = new Date()) {
|
|
91
|
+
return store.transact(() => {
|
|
92
|
+
store.handle.prepare('INSERT OR IGNORE INTO project_identity_carry(repo,from_identity,to_identity,actor,at,how) VALUES (?,?,?,?,?,?)').run(repo, args.from, args.to, args.actor, now.toISOString(), args.how);
|
|
93
|
+
const row = store.handle.prepare('SELECT * FROM project_knowledge WHERE repo=?').get(repo);
|
|
94
|
+
if (!row || row['identity'] !== args.from)
|
|
95
|
+
return null;
|
|
96
|
+
const history = store.handle.prepare('SELECT sha FROM knowledge_change WHERE repo=? AND identity=? AND revision=?').get(repo, args.from, row['revision']);
|
|
97
|
+
if (history?.['sha'] !== row['sha'] || learningSha(String(row['payload'])) !== row['sha'])
|
|
98
|
+
throw new Error('Project knowledge history could not be verified.');
|
|
99
|
+
const revision = Number(row['revision']) + 1;
|
|
100
|
+
store.handle.prepare('INSERT INTO knowledge_change(repo,identity,revision,actor,at,payload,sha) VALUES (?,?,?,?,?,?,?)').run(repo, args.to, revision, args.actor, now.toISOString(), String(row['payload']), String(row['sha']));
|
|
101
|
+
store.handle.prepare('UPDATE project_knowledge SET identity=?,revision=? WHERE repo=?').run(args.to, revision, repo);
|
|
102
|
+
return revision;
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
export const SAME_REPOSITORY = 'toolroll (same repository)';
|
|
106
|
+
/**
|
|
107
|
+
* Carry silently only what is provably the same repository: rows saved under the
|
|
108
|
+
* previous formula with this volume's current device number (no restart since).
|
|
109
|
+
*/
|
|
110
|
+
export function reconcileIdentity(store, repo, seen, now = new Date()) {
|
|
111
|
+
const { common, st } = identityFacts(repo, seen);
|
|
112
|
+
const current = identityOf(repo, common, st), legacy = legacyIdentityOf(repo, common, st);
|
|
113
|
+
let done = reconciled.get(store);
|
|
114
|
+
if (!done)
|
|
115
|
+
reconciled.set(store, done = new Set());
|
|
116
|
+
const key = `${repo}\n${legacy}`;
|
|
117
|
+
if (legacy !== current && !done.has(key)) {
|
|
118
|
+
if (!store.handle.prepare('SELECT 1 FROM project_identity_carry WHERE repo=? AND from_identity=? AND to_identity=?').get(repo, legacy, current)
|
|
119
|
+
&& store.handle.prepare(STORED_IDENTITY).get(repo, legacy)) {
|
|
120
|
+
// Damaged knowledge history keeps its own error; retry on the next read.
|
|
121
|
+
try {
|
|
122
|
+
carryProject(store, repo, { from: legacy, to: current, actor: SAME_REPOSITORY, how: 'automatic' }, now);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return { current, legacy };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
done.add(key);
|
|
129
|
+
}
|
|
130
|
+
return { current, legacy };
|
|
131
|
+
}
|
|
132
|
+
/** The current identity and every identity carried into it. Checks accept these; writes use the current one. */
|
|
133
|
+
export function acceptedIdentities(store, repo, seen) {
|
|
134
|
+
const { current, legacy } = reconcileIdentity(store, repo, seen);
|
|
135
|
+
const accepted = new Set([current, legacy]);
|
|
136
|
+
const carries = store.handle.prepare('SELECT from_identity,to_identity FROM project_identity_carry WHERE repo=?').all(repo).map(r => [String(r['from_identity']), String(r['to_identity'])]);
|
|
137
|
+
for (let grew = true; grew;) {
|
|
138
|
+
grew = false;
|
|
139
|
+
for (const [from, to] of carries)
|
|
140
|
+
if (accepted.has(to) && !accepted.has(from)) {
|
|
141
|
+
accepted.add(from);
|
|
142
|
+
grew = true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return accepted;
|
|
146
|
+
}
|
|
147
|
+
/** Without a database: the current identity, or the previous formula on this same volume. */
|
|
148
|
+
export function identityMatches(repo, identity) {
|
|
149
|
+
const { common, st } = identityFacts(repo);
|
|
150
|
+
return identity === identityOf(repo, common, st) || identity === legacyIdentityOf(repo, common, st);
|
|
64
151
|
}
|
|
65
152
|
function environmentFingerprint(store, repo) {
|
|
66
153
|
return learningSha(JSON.stringify({ platform: process.platform, arch: process.arch, node: process.version,
|
|
@@ -158,7 +245,7 @@ export function queueLearning(store, source, reviewer, value, catalog, now, asse
|
|
|
158
245
|
// Legacy reviews without a learning snapshot need no optional failure.
|
|
159
246
|
if (empty && assessment === undefined && !snapshot)
|
|
160
247
|
return;
|
|
161
|
-
if (!snapshot || snapshot['repo'] !== repo || snapshot['identity']
|
|
248
|
+
if (!snapshot || snapshot['repo'] !== repo || !acceptedIdentities(store, repo).has(String(snapshot['identity'])) || learningSha(String(snapshot['payload'])) !== snapshot['sha'])
|
|
162
249
|
throw new Error('Learning review identity is unavailable.');
|
|
163
250
|
const result = assessmentOf(assessment, value);
|
|
164
251
|
// A contradictory decision cannot publish suggestions. Keep its sealed
|
|
@@ -216,6 +303,8 @@ export function recoverLearning(store, root, repo, now = new Date(), seen = new
|
|
|
216
303
|
projects.set(repo, cursor = { reviewer: 0, reviewerEnd: 0, source: 0, sourceEnd: 0 });
|
|
217
304
|
// Freeze each sweep's end so a stream of new arrivals cannot postpone retry
|
|
218
305
|
// of an earlier failed row indefinitely. Cursors never confer eligibility.
|
|
306
|
+
let accepted;
|
|
307
|
+
const carried = (identity) => (accepted ??= acceptedIdentities(store, repo, seen)).has(String(identity));
|
|
219
308
|
if (!cursor.reviewerEnd)
|
|
220
309
|
cursor.reviewerEnd = Number(store.handle.prepare('SELECT max(id) AS id FROM run').get()?.['id'] ?? 0);
|
|
221
310
|
// If the optional outbox write failed, recover from the already sealed accepted response.
|
|
@@ -234,7 +323,7 @@ export function recoverLearning(store, root, repo, now = new Date(), seen = new
|
|
|
234
323
|
if (!source || !reviewer || source.taskRef !== reviewer.taskRef)
|
|
235
324
|
continue;
|
|
236
325
|
const snapshot = reviewSnapshot(store, reviewer.id);
|
|
237
|
-
if (!snapshot || snapshot['identity']
|
|
326
|
+
if (!snapshot || !carried(snapshot['identity']))
|
|
238
327
|
continue;
|
|
239
328
|
const response = store.artifactsFor(reviewer.id).find(a => a.kind === 'structured-output' && a.captureStatus === 'ok');
|
|
240
329
|
const read = response && readVerifiedArtifact(root, response);
|
|
@@ -270,7 +359,7 @@ export function recoverLearning(store, root, repo, now = new Date(), seen = new
|
|
|
270
359
|
throw new Error('Learning review provenance is unavailable.');
|
|
271
360
|
const candidates = parseLearning(JSON.parse(String(row['payload'])));
|
|
272
361
|
const identity = learningIdentity(repo, seen);
|
|
273
|
-
if (
|
|
362
|
+
if (!carried(row['identity']))
|
|
274
363
|
throw new Error('Learning project identity changed.');
|
|
275
364
|
const head = run.headRevision ?? run.baseRevision ?? '';
|
|
276
365
|
const diff = store.artifactsFor(source).find(a => a.kind === 'terminal-diff');
|
|
@@ -312,7 +401,7 @@ function lessonOf(row) {
|
|
|
312
401
|
return { id: Number(row['id']), repo: String(row['repo']), source: Number(row['source']), reviewer: Number(row['reviewer']), status: String(row['status']), version: Number(row['version']), payload, sha: String(row['sha']), identity: String(row['identity']), adoptedBy: row['adopted_by'] == null ? null : String(row['adopted_by']) };
|
|
313
402
|
}
|
|
314
403
|
function supported(store, root, lesson, seen) {
|
|
315
|
-
if (
|
|
404
|
+
if (!acceptedIdentities(store, lesson.repo, seen).has(lesson.identity))
|
|
316
405
|
throw new Error('Project identity changed.');
|
|
317
406
|
const capture = store.handle.prepare('SELECT catalog FROM learning_capture WHERE source=? AND reviewer=? AND repo=?').get(lesson.source, lesson.reviewer, lesson.repo);
|
|
318
407
|
if (!capture || reviewedSource(store, lesson.reviewer) !== lesson.source)
|
|
@@ -325,9 +414,9 @@ function supported(store, root, lesson, seen) {
|
|
|
325
414
|
export function learningView(store, root, repo, actor, before = 0) {
|
|
326
415
|
admission(store, repo, actor);
|
|
327
416
|
recoverLearning(store, root, repo);
|
|
328
|
-
const identity = learningIdentity(repo);
|
|
417
|
+
const identity = learningIdentity(repo), accepted = acceptedIdentities(store, repo);
|
|
329
418
|
const p = store.handle.prepare('SELECT * FROM learning_policy WHERE repo=?').get(repo);
|
|
330
|
-
const revision = Number(p?.['revision'] ?? 0), enabled = p?.['identity']
|
|
419
|
+
const revision = Number(p?.['revision'] ?? 0), enabled = accepted.has(String(p?.['identity'])) && p?.['enabled'] === 1;
|
|
331
420
|
let damaged = false;
|
|
332
421
|
const lessons = store.handle.prepare('SELECT * FROM project_lesson WHERE repo=? ORDER BY id DESC LIMIT 50').all(repo).flatMap(row => { try {
|
|
333
422
|
return [lessonOf(row)];
|
|
@@ -344,8 +433,9 @@ export function changeLearning(store, root, args, now = new Date()) {
|
|
|
344
433
|
admission(store, args.repo, args.actor, true);
|
|
345
434
|
if (learningIdentity(args.repo) !== args.identity)
|
|
346
435
|
throw new Error('Project identity changed. Reload Learning.');
|
|
436
|
+
const accepted = acceptedIdentities(store, args.repo);
|
|
347
437
|
const p = store.handle.prepare('SELECT * FROM learning_policy WHERE repo=?').get(args.repo);
|
|
348
|
-
if (Number(p?.['revision'] ?? 0) !== args.revision || (p && p['identity']
|
|
438
|
+
if (Number(p?.['revision'] ?? 0) !== args.revision || (p && !accepted.has(String(p['identity'])) && args.action !== 'reset'))
|
|
349
439
|
throw new Error('Learning changed. Reload before trying again.');
|
|
350
440
|
let before = p?.['enabled'] === 1 ? 'enabled' : 'paused', after = before, ev = [], source = null;
|
|
351
441
|
if (args.action === 'adopt' || args.action === 'disable') {
|
|
@@ -353,7 +443,7 @@ export function changeLearning(store, root, args, now = new Date()) {
|
|
|
353
443
|
if (!row)
|
|
354
444
|
throw new Error('Learning lesson is unavailable.');
|
|
355
445
|
const l = lessonOf(row);
|
|
356
|
-
if (l.version !== args.version || l.sha !== args.sha || l.identity
|
|
446
|
+
if (l.version !== args.version || l.sha !== args.sha || !accepted.has(l.identity))
|
|
357
447
|
throw new Error('Learning changed. Reload before trying again.');
|
|
358
448
|
if (l.payload.kind !== 'project' || (args.action === 'adopt' ? l.status !== 'proposed' : l.status !== 'adopted'))
|
|
359
449
|
throw new Error('This suggestion cannot take that action.');
|
|
@@ -396,14 +486,14 @@ export function learningContext(store, root, runId, phase, now = new Date(), val
|
|
|
396
486
|
const runner = store.getRunner(run.runner)?.runner;
|
|
397
487
|
if (!runner || runner.retiredAt !== null || !runner.repos.includes(repo) || (phase === 'review') !== (run.role === 'reviewer') || (phase === 'plan') !== (run.role === 'planner'))
|
|
398
488
|
return '';
|
|
399
|
-
const identity = learningIdentity(repo, seen);
|
|
489
|
+
const identity = learningIdentity(repo, seen), accepted = acceptedIdentities(store, repo, seen), carried = JSON.stringify([...accepted]);
|
|
400
490
|
const existing = store.handle.prepare('SELECT * FROM learning_snapshot WHERE run=?').get(runId);
|
|
401
491
|
if (existing) {
|
|
402
|
-
if (existing['repo'] !== repo || existing['identity']
|
|
492
|
+
if (existing['repo'] !== repo || !accepted.has(String(existing['identity'])) || learningSha(String(existing['payload'])) !== existing['sha'])
|
|
403
493
|
throw new Error('Learning snapshot no longer verifies.');
|
|
404
494
|
return String(existing['payload']);
|
|
405
495
|
}
|
|
406
|
-
const policy = store.handle.prepare('SELECT * FROM learning_policy WHERE repo=? AND identity
|
|
496
|
+
const policy = store.handle.prepare('SELECT * FROM learning_policy WHERE repo=? AND identity IN (SELECT value FROM json_each(?)) AND enabled=1').get(repo, carried);
|
|
407
497
|
const scope = ref && store.getScope(ref.externalId);
|
|
408
498
|
const source = run.role === 'reviewer' && run.parentRun !== null ? store.getRun(run.parentRun) : run;
|
|
409
499
|
// Prepared coding handoffs defer recording their validated base until
|
|
@@ -415,7 +505,7 @@ export function learningContext(store, root, runId, phase, now = new Date(), val
|
|
|
415
505
|
const unscopedPlan = phase === 'plan' && scope === null && !run.scopeDigest;
|
|
416
506
|
const eligible = [];
|
|
417
507
|
if (policy && (paths.length > 0 || unscopedPlan)) {
|
|
418
|
-
for (const row of store.handle.prepare("SELECT * FROM project_lesson WHERE repo=? AND identity
|
|
508
|
+
for (const row of store.handle.prepare("SELECT * FROM project_lesson WHERE repo=? AND identity IN (SELECT value FROM json_each(?)) AND status='adopted' ORDER BY id DESC LIMIT 100").all(repo, carried)) {
|
|
419
509
|
try {
|
|
420
510
|
const l = lessonOf(row);
|
|
421
511
|
if (!store.handle.prepare("SELECT 1 FROM learning_event WHERE lesson=? AND action='adopt' AND actor=?").get(l.id, l.adoptedBy ?? '') || l.source === source?.id || l.payload.kind !== 'project' || !l.adoptedBy || !store.accountCanAccess(l.adoptedBy, repo) || store.accountOf(l.adoptedBy)?.role !== 'approver' || !l.payload.phases.includes(phase) || l.payload.platform !== process.platform || (!unscopedPlan && !l.payload.paths.some(p => paths.some(t => p === t || p.startsWith(t.replace(/\/$/, '') + '/')))))
|
package/dist/project-skills.js
CHANGED
|
@@ -3,7 +3,7 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import { mkdirSync, mkdtempSync, writeFileSync, chmodSync } from "node:fs";
|
|
4
4
|
import { join, dirname } from "node:path";
|
|
5
5
|
import { parseDocument } from "yaml";
|
|
6
|
-
import { learningIdentity, learningSha } from "./project-learning.js";
|
|
6
|
+
import { acceptedIdentities, learningIdentity, learningSha } from "./project-learning.js";
|
|
7
7
|
import { scanForSecrets } from "./evidence.js";
|
|
8
8
|
import { fileTaskProposal } from "./proposal.js";
|
|
9
9
|
export const SKILLS_SCHEMA = `
|
|
@@ -120,8 +120,11 @@ function admit(store, repo, actor, write = false) {
|
|
|
120
120
|
!store.accountCanAccess(actor, repo) ||
|
|
121
121
|
(write && store.accountOf(actor)?.role !== "approver"))
|
|
122
122
|
throw Error("Skills are outside your project access.");
|
|
123
|
-
return learningIdentity(repo);
|
|
123
|
+
return { identity: learningIdentity(repo), accepted: acceptedIdentities(store, repo) };
|
|
124
124
|
}
|
|
125
|
+
/** Revisions under any identity this project accepts, as a bound JSON list. */
|
|
126
|
+
const IN_ACCEPTED = "identity IN (SELECT value FROM json_each(?))";
|
|
127
|
+
const listOf = (accepted) => JSON.stringify([...accepted]);
|
|
125
128
|
function unpack(payload, sha) {
|
|
126
129
|
if (typeof payload !== "string" || learningSha(payload) !== sha)
|
|
127
130
|
throw Error("Saved skills could not be verified.");
|
|
@@ -135,13 +138,13 @@ function packageOf(store, sha) {
|
|
|
135
138
|
throw Error("That skill version is unavailable.");
|
|
136
139
|
return { ...unpack(row["payload"], sha), sha };
|
|
137
140
|
}
|
|
138
|
-
function current(store, repo,
|
|
141
|
+
function current(store, repo, accepted) {
|
|
139
142
|
const row = store.handle
|
|
140
143
|
.prepare("SELECT * FROM project_skill_change WHERE repo=? ORDER BY revision DESC LIMIT 1")
|
|
141
144
|
.get(repo);
|
|
142
145
|
if (!row)
|
|
143
146
|
return { revision: 0, selection: {} };
|
|
144
|
-
if (row["identity"]
|
|
147
|
+
if (!accepted.has(String(row["identity"])))
|
|
145
148
|
throw Error("The project changed. Its previous skills have not been applied.");
|
|
146
149
|
return {
|
|
147
150
|
revision: Number(row["revision"]),
|
|
@@ -149,7 +152,7 @@ function current(store, repo, identity) {
|
|
|
149
152
|
};
|
|
150
153
|
}
|
|
151
154
|
export function skillsView(store, repo, actor) {
|
|
152
|
-
const identity = admit(store, repo, actor), value = current(store, repo,
|
|
155
|
+
const { identity, accepted } = admit(store, repo, actor), value = current(store, repo, accepted);
|
|
153
156
|
const visible = new Set(store.handle
|
|
154
157
|
.prepare("SELECT sha FROM skill_owner WHERE actor=? ORDER BY at,sha")
|
|
155
158
|
.all(actor)
|
|
@@ -162,7 +165,7 @@ export function skillsView(store, repo, actor) {
|
|
|
162
165
|
.get(project);
|
|
163
166
|
if (row) {
|
|
164
167
|
try {
|
|
165
|
-
if (row["identity"]
|
|
168
|
+
if (!acceptedIdentities(store, project).has(String(row["identity"])))
|
|
166
169
|
continue;
|
|
167
170
|
}
|
|
168
171
|
catch {
|
|
@@ -183,8 +186,8 @@ export function skillsView(store, repo, actor) {
|
|
|
183
186
|
.map((sha) => packageOf(store, sha))
|
|
184
187
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
185
188
|
const history = store.handle
|
|
186
|
-
.prepare(
|
|
187
|
-
.all(repo,
|
|
189
|
+
.prepare(`SELECT revision,actor,at,payload,sha FROM project_skill_change WHERE repo=? AND ${IN_ACCEPTED} ORDER BY revision DESC LIMIT 20`)
|
|
190
|
+
.all(repo, listOf(accepted))
|
|
188
191
|
.map((r) => ({
|
|
189
192
|
revision: Number(r["revision"]),
|
|
190
193
|
actor: String(r["actor"]),
|
|
@@ -197,8 +200,8 @@ export function skillsView(store, repo, actor) {
|
|
|
197
200
|
}
|
|
198
201
|
/** Current enabled versions, admitted and hash-verified, without any worker-run writes. */
|
|
199
202
|
export function selectProjectSkills(store, repo, actor) {
|
|
200
|
-
const identity = admit(store, repo, actor);
|
|
201
|
-
const value = current(store, repo,
|
|
203
|
+
const { identity, accepted } = admit(store, repo, actor);
|
|
204
|
+
const value = current(store, repo, accepted);
|
|
202
205
|
const packages = Object.values(value.selection).filter(choice => choice.enabled).map(choice => packageOf(store, choice.sha));
|
|
203
206
|
if (packages.length > SKILL_LIMITS.enabled || Buffer.byteLength(JSON.stringify(packages)) > SKILL_LIMITS.selectionBytes)
|
|
204
207
|
throw Error('Enable up to 8 skills, totaling at most 2 MB.');
|
|
@@ -206,10 +209,10 @@ export function selectProjectSkills(store, repo, actor) {
|
|
|
206
209
|
}
|
|
207
210
|
/** The same verified selection used by Restore, including disabled versions. */
|
|
208
211
|
export function skillsVersion(store, repo, actor, revision) {
|
|
209
|
-
const
|
|
212
|
+
const { accepted } = admit(store, repo, actor);
|
|
210
213
|
if (!Number.isSafeInteger(revision) || revision < 1)
|
|
211
214
|
throw Error("Choose a saved skills version.");
|
|
212
|
-
const row = store.handle.prepare(
|
|
215
|
+
const row = store.handle.prepare(`SELECT payload,sha FROM project_skill_change WHERE repo=? AND ${IN_ACCEPTED} AND revision=?`).get(repo, listOf(accepted), revision);
|
|
213
216
|
if (!row)
|
|
214
217
|
throw Error("That skills version is unavailable.");
|
|
215
218
|
const selection = unpack(row["payload"], row["sha"]);
|
|
@@ -238,7 +241,7 @@ export function importSkill(store, repo, actor, files, source, now = new Date())
|
|
|
238
241
|
return { ...skill, sha };
|
|
239
242
|
}
|
|
240
243
|
export function changeSkills(store, args, now = new Date()) {
|
|
241
|
-
const identity = admit(store, args.repo, args.actor, true);
|
|
244
|
+
const { identity, accepted } = admit(store, args.repo, args.actor, true);
|
|
242
245
|
if (identity !== args.identity || !Number.isSafeInteger(args.revision))
|
|
243
246
|
throw Error("The project changed. Reload Skills.");
|
|
244
247
|
store.transact(() => {
|
|
@@ -248,8 +251,8 @@ export function changeSkills(store, args, now = new Date()) {
|
|
|
248
251
|
let selection = view.selection;
|
|
249
252
|
if (args.action === "restore") {
|
|
250
253
|
const row = store.handle
|
|
251
|
-
.prepare(
|
|
252
|
-
.get(args.repo,
|
|
254
|
+
.prepare(`SELECT * FROM project_skill_change WHERE repo=? AND ${IN_ACCEPTED} AND revision=?`)
|
|
255
|
+
.get(args.repo, listOf(accepted), args.restore ?? -1);
|
|
253
256
|
if (!row)
|
|
254
257
|
throw Error("That version is unavailable.");
|
|
255
258
|
selection = unpack(row["payload"], row["sha"]);
|
|
@@ -316,7 +319,7 @@ export function readSkillsSnapshot(store, runId) {
|
|
|
316
319
|
const saved = unpack(row["payload"], row["sha"]);
|
|
317
320
|
const { packageShas, ...facts } = saved;
|
|
318
321
|
if (packageShas.length &&
|
|
319
|
-
|
|
322
|
+
!acceptedIdentities(store, String(row["repo"])).has(facts.identity))
|
|
320
323
|
throw Error("The project identity saved for this run changed.");
|
|
321
324
|
return {
|
|
322
325
|
...facts,
|
|
@@ -370,7 +373,11 @@ export function freezeSkills(store, runId) {
|
|
|
370
373
|
if (inherited)
|
|
371
374
|
snapshot = { ...inherited, inheritedFrom: parent.id };
|
|
372
375
|
else if (!parent || (run.role !== "reviewer" && test)) {
|
|
373
|
-
|
|
376
|
+
const saved = store.handle
|
|
377
|
+
.prepare("SELECT identity FROM project_skill_change WHERE repo=? ORDER BY revision DESC LIMIT 1")
|
|
378
|
+
.get(ref.repo);
|
|
379
|
+
const accepted = test || saved ? acceptedIdentities(store, ref.repo) : new Set();
|
|
380
|
+
if (test && !accepted.has(String(test["identity"])))
|
|
374
381
|
throw Error("The project for this test changed. Create a new test.");
|
|
375
382
|
if (test)
|
|
376
383
|
snapshot = {
|
|
@@ -380,10 +387,9 @@ export function freezeSkills(store, runId) {
|
|
|
380
387
|
inheritedFrom: null,
|
|
381
388
|
packages: [packageOf(store, String(test["package"]))],
|
|
382
389
|
};
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
const value = current(store, ref.repo, learningIdentity(ref.repo));
|
|
390
|
+
// Skills saved under another identity wait for an approver; the run proceeds without them.
|
|
391
|
+
else if (saved && accepted.has(String(saved["identity"]))) {
|
|
392
|
+
const value = current(store, ref.repo, accepted);
|
|
387
393
|
snapshot = {
|
|
388
394
|
...snapshot,
|
|
389
395
|
revision: value.revision,
|
|
@@ -8,6 +8,19 @@ export declare const FOLLOW_UP_LOG = "follow-up check log v1";
|
|
|
8
8
|
export declare const FOLLOW_UP_RECEIPT = "follow-up check receipt v1";
|
|
9
9
|
export declare const ADD_TESTS_ACTION = "tests task filed";
|
|
10
10
|
export type RunLevel = Exclude<CheckLevel, "off">;
|
|
11
|
+
/** How a batch check (batch-checks.ts) reached this result's outcome. `together`: one check on a temporary
|
|
12
|
+
* merge of `members`; `split`: its batch failed, so it was checked on its own; `conflict`: it couldn't be merged
|
|
13
|
+
* with the others, so it was checked on its own; `alone`: nothing joined it within the window. */
|
|
14
|
+
export type BatchFacts = {
|
|
15
|
+
mode: "together" | "split" | "conflict" | "alone";
|
|
16
|
+
tested: string;
|
|
17
|
+
base: string | null;
|
|
18
|
+
members: {
|
|
19
|
+
task: string;
|
|
20
|
+
run: number;
|
|
21
|
+
head: string;
|
|
22
|
+
}[];
|
|
23
|
+
};
|
|
11
24
|
export type FollowUpCheck = {
|
|
12
25
|
request: number;
|
|
13
26
|
runId: number;
|
|
@@ -15,13 +28,19 @@ export type FollowUpCheck = {
|
|
|
15
28
|
head: string;
|
|
16
29
|
actor: string;
|
|
17
30
|
at: string;
|
|
18
|
-
why: "person" | "pull-request";
|
|
31
|
+
why: "person" | "pull-request" | "batch";
|
|
19
32
|
/** The approved command's digest when checks were asked for: only that exact command runs. */
|
|
20
33
|
digest: string;
|
|
21
34
|
state: "waiting" | "running" | "passed" | "failed" | "not-run";
|
|
22
35
|
exitCode: number | null;
|
|
23
36
|
logArtifactId: number | null;
|
|
24
37
|
note: string | null;
|
|
38
|
+
/** A batch request: the project base it is merged onto. */
|
|
39
|
+
base?: string | null;
|
|
40
|
+
/** The commit the check actually ran on: the result's own, or a temporary batch commit. Null until it ran. */
|
|
41
|
+
tested?: string | null;
|
|
42
|
+
/** From the sealed receipt of a batch check. */
|
|
43
|
+
batch?: BatchFacts | null;
|
|
25
44
|
};
|
|
26
45
|
type Refusal = {
|
|
27
46
|
ok: false;
|
|
@@ -43,8 +62,9 @@ export declare function requestFollowUpChecks(store: Store, input: {
|
|
|
43
62
|
level: RunLevel;
|
|
44
63
|
existing: boolean;
|
|
45
64
|
} | Refusal;
|
|
46
|
-
/** Requests nobody has started yet, oldest first. */
|
|
65
|
+
/** Requests nobody has started yet, oldest first. Batch requests wait for the batch pass (batch-checks.ts). */
|
|
47
66
|
export declare function waitingCheckRequests(store: Store, now: Date): FollowUpCheck[];
|
|
67
|
+
export declare function waitingRequestsOfAnyKind(store: Store, now: Date): FollowUpCheck[];
|
|
48
68
|
export type CheckRunDeps = {
|
|
49
69
|
runner?: Runner;
|
|
50
70
|
git?: Runner;
|
|
@@ -47,28 +47,34 @@ export function followUpChecksOf(store, runId, now = new Date(), root) {
|
|
|
47
47
|
const body = JSON.parse(read.content.toString("utf8"));
|
|
48
48
|
if (typeof body.request !== "number")
|
|
49
49
|
continue;
|
|
50
|
+
const head = typeof body.head === "string" ? body.head : null;
|
|
50
51
|
receipts.set(body.request, { ran: body.result?.ran === true, exitCode: typeof body.result?.exitCode === "number" ? body.result.exitCode : null,
|
|
51
|
-
log: typeof body.log?.artifactId === "number" ? body.log.artifactId : null
|
|
52
|
+
log: typeof body.log?.artifactId === "number" ? body.log.artifactId : null, head,
|
|
53
|
+
tested: typeof body.tested === "string" ? body.tested : head, batch: batchFactsOf(body.batch, runId, head) });
|
|
52
54
|
}
|
|
53
55
|
catch { /* an unreadable receipt upgrades nothing */ }
|
|
54
56
|
}
|
|
55
57
|
}
|
|
56
58
|
return rows.filter(row => row.action === CHECKS_REQUESTED && (row.outcome === "quick" || row.outcome === "full")).map(request => {
|
|
57
|
-
const [head = "", why = "person", digest = ""] = (request.detail ?? "").split(" · ");
|
|
59
|
+
const [head = "", why = "person", digest = "", recordedBase = ""] = (request.detail ?? "").split(" · ");
|
|
58
60
|
const started = rows.find(row => row.action === CHECKS_STARTED && row.outcome === String(request.id)) ?? null;
|
|
59
61
|
const finished = rows.find(row => row.action === CHECKS_FINISHED && row.outcome.startsWith(`${request.id}:`)) ?? null;
|
|
60
62
|
const base = { request: request.id, runId, level: request.outcome, head, digest, actor: request.actor, at: request.at,
|
|
61
|
-
why: why === "pull-request" ? "pull-request" : "person", exitCode: null, logArtifactId: null, note: null
|
|
63
|
+
why: why === "pull-request" ? "pull-request" : why === "batch" ? "batch" : "person", exitCode: null, logArtifactId: null, note: null,
|
|
64
|
+
...(why === "batch" ? { base: /^[a-f0-9]{40}$/.test(recordedBase) ? recordedBase : null } : {}) };
|
|
62
65
|
if (finished !== null) {
|
|
63
66
|
const status = finished.outcome.slice(String(request.id).length + 1);
|
|
64
67
|
const sealed = receipts.get(request.id) ?? null;
|
|
65
|
-
// With the evidence at hand, only a sealed receipt can say passed
|
|
66
|
-
|
|
68
|
+
// With the evidence at hand, only a sealed receipt can say passed: for this result's own commit, and,
|
|
69
|
+
// for a batch, a batch that names this result.
|
|
70
|
+
if (status === "passed" && root !== undefined && (sealed === null || !sealed.ran || sealed.exitCode !== 0 || (sealed.head !== null && sealed.head !== head)
|
|
71
|
+
|| (base.why === "batch" && sealed.batch === null))) {
|
|
67
72
|
return { ...base, state: "not-run", note: "The saved check result could not be verified." };
|
|
68
73
|
}
|
|
69
74
|
const exit = /exit (\d+)/.exec(finished.detail ?? "");
|
|
70
75
|
return { ...base, state: status === "passed" ? "passed" : status === "failed" ? "failed" : "not-run",
|
|
71
|
-
exitCode: exit === null ? sealed?.exitCode ?? null : Number(exit[1]), logArtifactId: sealed?.log ?? null, note: status === "not-run" ? finished.detail : null
|
|
76
|
+
exitCode: exit === null ? sealed?.exitCode ?? null : Number(exit[1]), logArtifactId: sealed?.log ?? null, note: status === "not-run" ? finished.detail : null,
|
|
77
|
+
...(sealed === null ? {} : { tested: sealed.ran ? sealed.tested : null, batch: sealed.batch }) };
|
|
72
78
|
}
|
|
73
79
|
if (started !== null) {
|
|
74
80
|
const limit = Number(/limit (\d+)/.exec(started.detail ?? "")?.[1] ?? 3_600_000);
|
|
@@ -79,6 +85,26 @@ export function followUpChecksOf(store, runId, now = new Date(), root) {
|
|
|
79
85
|
return { ...base, state: "waiting" };
|
|
80
86
|
});
|
|
81
87
|
}
|
|
88
|
+
/** A receipt's batch facts, only when they are whole and name this result at its own commit. */
|
|
89
|
+
function batchFactsOf(value, runId, head) {
|
|
90
|
+
if (value === null || typeof value !== "object")
|
|
91
|
+
return null;
|
|
92
|
+
const raw = value;
|
|
93
|
+
const modes = ["together", "split", "conflict", "alone"];
|
|
94
|
+
const mode = modes.find(one => one === raw.mode);
|
|
95
|
+
if (mode === undefined || typeof raw.tested !== "string" || !/^[a-f0-9]{40}$/.test(raw.tested) || !Array.isArray(raw.members))
|
|
96
|
+
return null;
|
|
97
|
+
const members = raw.members.flatMap(one => {
|
|
98
|
+
const m = one;
|
|
99
|
+
return typeof m?.task === "string" && typeof m.run === "number" && typeof m.head === "string" ? [{ task: m.task, run: m.run, head: m.head }] : [];
|
|
100
|
+
});
|
|
101
|
+
if (members.length !== raw.members.length || !members.some(one => one.run === runId && one.head === head))
|
|
102
|
+
return null;
|
|
103
|
+
// Checked on its own: the commit tested is the result's own.
|
|
104
|
+
if (mode !== "together" && (members.length !== 1 || raw.tested !== head))
|
|
105
|
+
return null;
|
|
106
|
+
return { mode, tested: raw.tested, base: typeof raw.base === "string" ? raw.base : null, members };
|
|
107
|
+
}
|
|
82
108
|
/** Whether a result can have checks run on it, and with which command. */
|
|
83
109
|
function checkTarget(store, runId, level) {
|
|
84
110
|
const run = store.getRun(runId);
|
|
@@ -110,8 +136,11 @@ export function requestFollowUpChecks(store, input, now) {
|
|
|
110
136
|
return { ok: true, request, level: target.level, existing: false };
|
|
111
137
|
});
|
|
112
138
|
}
|
|
113
|
-
/** Requests nobody has started yet, oldest first. */
|
|
139
|
+
/** Requests nobody has started yet, oldest first. Batch requests wait for the batch pass (batch-checks.ts). */
|
|
114
140
|
export function waitingCheckRequests(store, now) {
|
|
141
|
+
return waitingRequestsOfAnyKind(store, now).filter(one => one.why !== "batch");
|
|
142
|
+
}
|
|
143
|
+
export function waitingRequestsOfAnyKind(store, now) {
|
|
115
144
|
const runs = store.handle.prepare(`SELECT DISTINCT r.run_id AS run FROM action_ledger r WHERE r.action = ? AND r.run_id IS NOT NULL
|
|
116
145
|
AND NOT EXISTS (SELECT 1 FROM action_ledger s WHERE s.run_id = r.run_id AND s.action = ? AND s.outcome = CAST(r.id AS TEXT)) ORDER BY r.id`)
|
|
117
146
|
.all(CHECKS_REQUESTED, CHECKS_STARTED).map(row => Number(row["run"]));
|
|
@@ -204,7 +233,7 @@ export async function runFollowUpCheck(store, root, request, deps = {}) {
|
|
|
204
233
|
const text = log.length === 0 ? "No command started." : log.join("\n\n");
|
|
205
234
|
const logId = storeEvidence(store, root, runId, "structured-output", `follow-up-check-${request}.log`, Buffer.from(text, "utf8"), FOLLOW_UP_LOG, at, { captureStatus: "ok" });
|
|
206
235
|
const logArtifact = store.getArtifact(logId);
|
|
207
|
-
const receipt = JSON.stringify({ version: 1, request, run: runId, head: target.head, level: target.level,
|
|
236
|
+
const receipt = JSON.stringify({ version: 1, request, run: runId, head: target.head, tested: target.head, level: target.level,
|
|
208
237
|
command: { repo: target.command.repo, command: target.command.command, digest: target.command.digest, approvedBy: target.command.approvedBy },
|
|
209
238
|
result, log: { artifactId: logArtifact.id, sha256: logArtifact.sha256, bytesStored: logArtifact.bytesStored, truncated: logArtifact.truncated } }, null, 1);
|
|
210
239
|
storeEvidence(store, root, runId, "structured-output", `follow-up-check-${request}.json`, Buffer.from(receipt, "utf8"), FOLLOW_UP_RECEIPT, at, { captureStatus: "ok" });
|
|
@@ -262,9 +291,14 @@ export function fullCheckGate(store, runId, now) {
|
|
|
262
291
|
const level = runCheckLevel(store, runId);
|
|
263
292
|
const repo = repoOfRun(store, runId);
|
|
264
293
|
const run = store.getRun(runId);
|
|
265
|
-
if (level === null ||
|
|
294
|
+
if (level === null || repo === null || run === null)
|
|
295
|
+
return { state: "clear" };
|
|
296
|
+
const fulls = followUpChecksOf(store, runId, now).filter(one => one.level === "full" && one.head === run.headRevision);
|
|
297
|
+
// A Full build that waited for a batch check has had no check of its own: Merge waits for the batch, like Quick does.
|
|
298
|
+
const batched = level === "full" && fulls.some(one => one.why === "batch");
|
|
299
|
+
if ((level === "full" && !batched) || (!batched && store.liveVerifyCommand(repo) === null))
|
|
266
300
|
return { state: "clear" };
|
|
267
|
-
const last =
|
|
301
|
+
const last = fulls.at(-1);
|
|
268
302
|
if (last === undefined)
|
|
269
303
|
return { state: "not-requested", message: "The full check hasn't run on this commit yet. Merge once it passes, or merge anyway." };
|
|
270
304
|
if (last.state === "passed")
|