toolroll 0.9.91 → 0.9.93
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 +41 -0
- package/dist/approval-authority.d.ts +9 -1
- package/dist/approval-authority.js +15 -2
- package/dist/assignment-status.d.ts +1 -1
- package/dist/assignment-ui.js +2 -2
- package/dist/assignment.js +11 -3
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +30 -30
- package/dist/browser-crew.js +2 -2
- package/dist/browser-workspace.d.ts +3 -5
- package/dist/build-review.js +2 -0
- package/dist/builder.js +21 -2
- package/dist/chat-actions.d.ts +11 -3
- package/dist/chat-actions.js +47 -2
- package/dist/chat-decide.js +2 -0
- package/dist/chat-review.d.ts +2 -1
- package/dist/chat-review.js +2 -2
- package/dist/check-facts.d.ts +124 -0
- package/dist/check-facts.js +272 -0
- package/dist/check-report.d.ts +5 -0
- package/dist/check-report.js +4 -3
- package/dist/cli.js +5 -0
- package/dist/contracts/chat-actions.d.ts +10 -0
- package/dist/contracts/chat-actions.js +4 -0
- package/dist/contracts/console-api.d.ts +3 -2
- package/dist/contracts/console-api.js +5 -2
- package/dist/contracts/release-check.d.ts +25 -1
- package/dist/contracts/release-check.js +19 -0
- package/dist/contracts/route-results.d.ts +121 -0
- package/dist/contracts/route-results.js +71 -0
- package/dist/contracts/route.d.ts +343 -5
- package/dist/contracts/route.js +36 -7
- package/dist/contracts/store-json.d.ts +2 -0
- package/dist/desktop-update.d.ts +2 -2
- package/dist/desktop-update.js +13 -9
- package/dist/dispose.js +6 -0
- package/dist/eval-hooks.d.ts +55 -0
- package/dist/eval-hooks.js +227 -4
- package/dist/flaky.d.ts +17 -0
- package/dist/flaky.js +9 -0
- package/dist/flow-engine.js +1 -1
- package/dist/flow-pull-request.js +1 -1
- package/dist/lead-doors.js +4 -2
- package/dist/lead-tools.js +10 -2
- package/dist/local-owner.d.ts +1 -1
- package/dist/local-owner.js +11 -0
- package/dist/memory-cli.d.ts +3 -0
- package/dist/memory-cli.js +3 -3
- package/dist/memory-pass.js +5 -1
- package/dist/model-choices.d.ts +7 -3
- package/dist/model-choices.js +26 -11
- package/dist/models-cli.d.ts +205 -0
- package/dist/models-cli.js +143 -4
- package/dist/models-ui.d.ts +57 -0
- package/dist/models-ui.js +44 -1
- package/dist/needs-you.d.ts +7 -7
- package/dist/needs-you.js +12 -10
- package/dist/operate-remote-arguments.js +4 -4
- package/dist/operate-remote.js +1 -1
- package/dist/operate.js +191 -23
- package/dist/phase-routing.d.ts +82 -2
- package/dist/phase-routing.js +132 -18
- package/dist/project-delete.d.ts +4 -0
- package/dist/project-delete.js +16 -4
- package/dist/project-memory.d.ts +8 -0
- package/dist/project-memory.js +20 -1
- package/dist/pull-request-flow.js +1 -1
- package/dist/push.d.ts +10 -3
- package/dist/push.js +22 -6
- package/dist/release-adapters.js +11 -1
- package/dist/release-approval.d.ts +191 -0
- package/dist/release-approval.js +382 -0
- package/dist/release-auto.js +22 -4
- package/dist/release-card.d.ts +73 -4
- package/dist/release-card.js +168 -23
- package/dist/release-cli.js +9 -1
- package/dist/release.d.ts +30 -0
- package/dist/release.js +42 -3
- package/dist/result-actions.d.ts +4 -0
- package/dist/result-actions.js +3 -0
- package/dist/result-acts.d.ts +3 -3
- package/dist/result-acts.js +1 -1
- package/dist/route-hooks.d.ts +54 -0
- package/dist/route-hooks.js +179 -0
- package/dist/route-results.d.ts +70 -0
- package/dist/route-results.js +340 -0
- package/dist/route-rules.d.ts +324 -0
- package/dist/route-rules.js +781 -0
- package/dist/route-savings.d.ts +136 -0
- package/dist/route-savings.js +193 -0
- package/dist/route-store.d.ts +300 -0
- package/dist/route-store.js +598 -0
- package/dist/scope.d.ts +3 -1
- package/dist/scope.js +4 -1
- package/dist/server/guards.js +1 -0
- package/dist/server/pages-assets.js +1 -1
- package/dist/server/render-chat.js +2 -2
- package/dist/server/render-release.d.ts +17 -0
- package/dist/server/render-release.js +84 -0
- package/dist/server/render-results.d.ts +0 -2
- package/dist/server/render-results.js +4 -6
- package/dist/server/render-tasks.d.ts +7 -0
- package/dist/server/render-tasks.js +16 -3
- package/dist/server/route-table.d.ts +1 -1
- package/dist/server/route-table.js +19 -1
- package/dist/server/settings.d.ts +2 -1
- package/dist/server/settings.js +87 -1
- package/dist/server/task-views.js +6 -0
- package/dist/server/tasks.d.ts +15 -0
- package/dist/server/tasks.js +105 -20
- package/dist/settings-proofs.js +2 -0
- package/dist/spend-ui.d.ts +11 -0
- package/dist/spend-ui.js +49 -2
- package/dist/stop-proof.d.ts +67 -0
- package/dist/stop-proof.js +293 -0
- package/dist/store.d.ts +106 -31
- package/dist/store.js +653 -128
- package/dist/surface.js +17 -5
- package/dist/task-outcome-cli.js +24 -2
- package/dist/toolroll-update-ui.js +2 -2
- package/dist/toolroll-update.js +4 -2
- package/dist/work-index.js +7 -9
- package/dist/work-summary.d.ts +2 -2
- package/package.json +1 -1
package/dist/eval-hooks.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type ts from "typescript-parser";
|
|
1
2
|
/** One required call: in this file, inside this top-level function, a call to this name, the name bound as `from`
|
|
2
3
|
* says (a module specifier its un-renamed named import comes from; `self`: the file's own top-level function; `local`:
|
|
3
4
|
* declared inside `inside`). `args`: the call's arguments, as text, must stay as the base has them. */
|
|
@@ -9,12 +10,17 @@ export type EvalHook = {
|
|
|
9
10
|
args?: true;
|
|
10
11
|
why: string;
|
|
11
12
|
};
|
|
13
|
+
/** `fn` may also be `outer>inner` (docs/plans/learning-loops.md, Protection 3): a `const inner = (…) => …` (or function
|
|
14
|
+
* expression) or `function inner`, bound once in the file and never reassigned, inside the top-level function `outer`
|
|
15
|
+
* and in no other function between them. `strict`: a base that holds the file but can't resolve the pin is a problem,
|
|
16
|
+
* never skipped (a base without the file at all is a project that isn't Toolroll, and holds none). */
|
|
12
17
|
/** One function whose body (its text, as the candidate has it) must stay as the base has it: a top-level function, or
|
|
13
18
|
* `Class#method`, a method of a top-level class. */
|
|
14
19
|
export type EvalPinned = {
|
|
15
20
|
file: string;
|
|
16
21
|
fn: string;
|
|
17
22
|
why: string;
|
|
23
|
+
strict?: true;
|
|
18
24
|
};
|
|
19
25
|
export declare const EVAL_HOOKS: readonly EvalHook[];
|
|
20
26
|
export declare const EVAL_PINNED: readonly EvalPinned[];
|
|
@@ -37,3 +43,52 @@ export declare const EVAL_HOOK_FILES: readonly string[];
|
|
|
37
43
|
* files the verified diff says exist at the candidate, so no bytes is a failed read, never a deletion.
|
|
38
44
|
*/
|
|
39
45
|
export declare function storeRewriteProblem(paths: readonly string[], candidate: ((file: string) => Buffer | null) | null, base: ((file: string) => Buffer | null) | null, pinned?: readonly string[]): string | null;
|
|
46
|
+
/** The calls that record a check's facts: at disposition, and as the check's command starts. */
|
|
47
|
+
export declare const LEARNING_HOOKS: readonly EvalHook[];
|
|
48
|
+
/** The bodies a release can't change by itself, every one strict: what a run is told from project memory, how a memory
|
|
49
|
+
* proposal and a decision are written, and where a check's conditions are captured. */
|
|
50
|
+
export declare const LEARNING_PINNED: readonly EvalPinned[];
|
|
51
|
+
/** Every file the learning manifest reads. */
|
|
52
|
+
export declare const LEARNING_HOOK_FILES: readonly string[];
|
|
53
|
+
/** Each protected table and its only writer modules (the v149 migration in store.ts aside). The decision tables'
|
|
54
|
+
* existing writers stay theirs: project-memory.ts records and retires, project-delete.ts deletes a whole project. */
|
|
55
|
+
export declare const LEARNING_WRITERS: Readonly<Record<string, readonly string[]>>;
|
|
56
|
+
/** One SQL-shaped construct in a file: its static text (a placeholder `${}` where an expression sits), the protected
|
|
57
|
+
* tables it writes, and the top-level declaration it's in ("" for none). `unreadable`: a write whose table name is
|
|
58
|
+
* split around an expression; `bare`: a string that is only a protected table's name (SQL assembled elsewhere). */
|
|
59
|
+
type LearningConstruct = {
|
|
60
|
+
text: string;
|
|
61
|
+
tables: string[];
|
|
62
|
+
inside: string;
|
|
63
|
+
unreadable: boolean;
|
|
64
|
+
bare: boolean;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Every construct in the file that could write a protected table: each string literal, template (its static parts)
|
|
68
|
+
* and `+` concatenation (its literal operands), whatever it's passed to — `prepare`, `exec`, a renamed `prepare`, a
|
|
69
|
+
* helper. A write verb next to a protected table's name, a table name split around an expression next to a write
|
|
70
|
+
* word, and a string that is only a protected table's name are each found.
|
|
71
|
+
*/
|
|
72
|
+
export declare function learningConstructs(source: ts.SourceFile): LearningConstruct[];
|
|
73
|
+
/**
|
|
74
|
+
* Why a changed file writes a learning-loops table (or a decision merge or restore) from a place that may not, or null
|
|
75
|
+
* (docs/plans/learning-loops.md, Protection 4). Every changed JavaScript or TypeScript file but tests is parsed; a
|
|
76
|
+
* construct its writer module doesn't own is new unless the base's copy of that file already had exactly that text
|
|
77
|
+
* (counted by its sha256, as storeRewriteProblem does). A file that can't be read or parsed needs a person. It reads
|
|
78
|
+
* text the candidate wrote, never runs it: it raises the bar for review; a person reviewing these files is the backstop.
|
|
79
|
+
*/
|
|
80
|
+
export declare function learningWriteProblem(paths: readonly string[], candidate: ((file: string) => Buffer | null) | null, base: ((file: string) => Buffer | null) | null): string | null;
|
|
81
|
+
/**
|
|
82
|
+
* Why a candidate needs a person for learning loops, or null: a required call dropped, a pinned body changed (strict
|
|
83
|
+
* pins included), or a changed file writing a protected table from outside its writers. With `manifest.base`, only what
|
|
84
|
+
* the base holds is required, as evalHookProblem does — except a strict pin whose file the base holds.
|
|
85
|
+
*/
|
|
86
|
+
export declare function learningHookProblem(manifest: {
|
|
87
|
+
candidate: (file: string) => Buffer | null;
|
|
88
|
+
base?: (file: string) => Buffer | null;
|
|
89
|
+
}, changed: {
|
|
90
|
+
paths: readonly string[];
|
|
91
|
+
candidate: ((file: string) => Buffer | null) | null;
|
|
92
|
+
base: ((file: string) => Buffer | null) | null;
|
|
93
|
+
}): string | null;
|
|
94
|
+
export {};
|
package/dist/eval-hooks.js
CHANGED
|
@@ -36,6 +36,7 @@ export const EVAL_HOOKS = Object.freeze([
|
|
|
36
36
|
{ file: "src/release-auto.ts", inside: "changeProblem", calls: "evalHookProblem", from: "./eval-hooks.js", why: "this check itself" },
|
|
37
37
|
{ file: "src/release-auto.ts", inside: "changeProblem", calls: "autoPathProblem", from: "self", why: "policy paths need a person" },
|
|
38
38
|
{ file: "src/release-auto.ts", inside: "changeProblem", calls: "storeRewriteProblem", from: "./eval-hooks.js", why: "no changed file rewrites a pinned Store method" },
|
|
39
|
+
{ file: "src/release-auto.ts", inside: "changeProblem", calls: "learningHookProblem", from: "./eval-hooks.js", why: "learning loops' hooks, pins and writers (docs/plans/learning-loops.md)" },
|
|
39
40
|
// The invocation gateway (invoke.ts): every agent run an eval world spawns takes a slot.
|
|
40
41
|
{ file: "src/invoke.ts", inside: "invokeAgent", calls: "takeEvalDispatch", from: "./eval-gate.js", why: "every agent dispatch in an eval world takes a slot" },
|
|
41
42
|
// PR #263 review: what the run's billing basis and its unknown cost are computed from is held too, not only the call.
|
|
@@ -227,7 +228,7 @@ function boundAs(source, name, from, inside) {
|
|
|
227
228
|
if (from === "self")
|
|
228
229
|
return compiler.isFunctionDeclaration(only) && only.parent === source ? null : `${name} isn't its own top-level function`;
|
|
229
230
|
if (from === "local")
|
|
230
|
-
return only.pos >= inside.pos && only.end <= inside.end && !compiler.isParameter(only) ? null : `${name} isn't declared inside
|
|
231
|
+
return only.pos >= inside.pos && only.end <= inside.end && !compiler.isParameter(only) ? null : `${name} isn't declared inside its function`;
|
|
231
232
|
if (!compiler.isImportSpecifier(only) || only.propertyName !== undefined || only.isTypeOnly)
|
|
232
233
|
return `${name} isn't imported by its own name`;
|
|
233
234
|
const clause = only.parent.parent;
|
|
@@ -254,6 +255,46 @@ function heldFunction(source, name) {
|
|
|
254
255
|
const fn = topFunction(source, name);
|
|
255
256
|
return fn === null || boundAs(source, name, "self", fn) !== null ? null : fn;
|
|
256
257
|
}
|
|
258
|
+
/** `outer>inner`: the one function-like `inner` declared inside the held top-level `outer` (no other function between
|
|
259
|
+
* them), bound once in the file and never reassigned — a `function inner` or a `const inner =` arrow or function
|
|
260
|
+
* expression. Null otherwise. */
|
|
261
|
+
function nestedFunction(source, spec) {
|
|
262
|
+
const compiler = typescript();
|
|
263
|
+
const [outerName, innerName, ...rest] = spec.split(">");
|
|
264
|
+
if (outerName === undefined || innerName === undefined || rest.length > 0 || outerName === "" || innerName === "")
|
|
265
|
+
return null;
|
|
266
|
+
const outer = heldFunction(source, outerName);
|
|
267
|
+
if (outer === null || outer.body === undefined)
|
|
268
|
+
return null;
|
|
269
|
+
const all = bindingsOf(source, innerName);
|
|
270
|
+
if (all.length !== 1)
|
|
271
|
+
return null;
|
|
272
|
+
const only = all[0];
|
|
273
|
+
let fn = null;
|
|
274
|
+
if (compiler.isFunctionDeclaration(only) && only.body !== undefined)
|
|
275
|
+
fn = only;
|
|
276
|
+
else if (compiler.isVariableDeclaration(only) && only.initializer !== undefined && (compiler.isArrowFunction(only.initializer) || compiler.isFunctionExpression(only.initializer))
|
|
277
|
+
&& compiler.isVariableDeclarationList(only.parent) && (only.parent.flags & compiler.NodeFlags.Const) !== 0)
|
|
278
|
+
fn = only.initializer;
|
|
279
|
+
if (fn === null)
|
|
280
|
+
return null;
|
|
281
|
+
// Directly inside `outer`: the first function-like ancestor of the declaration is `outer` itself.
|
|
282
|
+
for (let at = only.parent; at !== undefined; at = at.parent) {
|
|
283
|
+
if (at === outer)
|
|
284
|
+
return fn;
|
|
285
|
+
if (compiler.isFunctionLike(at) || compiler.isClassLike(at))
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
/** A held function by its manifest name: top-level, `Class#method` or `outer>inner`. */
|
|
291
|
+
function heldNode(source, spec) {
|
|
292
|
+
if (spec.includes(">"))
|
|
293
|
+
return nestedFunction(source, spec);
|
|
294
|
+
if (spec.includes("#"))
|
|
295
|
+
return classMethod(source, spec);
|
|
296
|
+
return heldFunction(source, spec);
|
|
297
|
+
}
|
|
257
298
|
/** A reader that parses each file once. */
|
|
258
299
|
function parser(read) {
|
|
259
300
|
const seen = new Map();
|
|
@@ -265,7 +306,7 @@ function hookIn(tree, hook) {
|
|
|
265
306
|
const parsed = tree(hook.file);
|
|
266
307
|
if ("problem" in parsed)
|
|
267
308
|
return { missing: `${parsed.problem}, and it holds a required call (${hook.why})` };
|
|
268
|
-
const fn = heldFunction(parsed.source, hook.inside);
|
|
309
|
+
const fn = (hook.inside.includes(">") ? nestedFunction(parsed.source, hook.inside) : heldFunction(parsed.source, hook.inside));
|
|
269
310
|
if (fn === null || fn.body === undefined)
|
|
270
311
|
return { missing: `${hook.file} no longer has ${hook.inside} as one function, which must call ${hook.calls} (${hook.why})` };
|
|
271
312
|
const found = callsOf(fn.body, hook.calls);
|
|
@@ -281,7 +322,7 @@ function pinnedText(tree, pin) {
|
|
|
281
322
|
const parsed = tree(pin.file);
|
|
282
323
|
if ("problem" in parsed)
|
|
283
324
|
return null;
|
|
284
|
-
const fn =
|
|
325
|
+
const fn = heldNode(parsed.source, pin.fn);
|
|
285
326
|
if (fn === null)
|
|
286
327
|
return null;
|
|
287
328
|
// The static block's pin holds the class header too: its name, modifiers and heritage, as the base has them.
|
|
@@ -308,8 +349,12 @@ export function evalHookProblem(candidate, base, hooks = EVAL_HOOKS, pinned = EV
|
|
|
308
349
|
}
|
|
309
350
|
for (const pin of pinned) {
|
|
310
351
|
const before = was === null ? null : pinnedText(was, pin);
|
|
311
|
-
if (was !== null && before === null)
|
|
352
|
+
if (was !== null && before === null) {
|
|
353
|
+
// A strict pin the base holds the file of but can't resolve is a problem (learning loops, Protection 2), never a skip.
|
|
354
|
+
if (pin.strict === true && base(pin.file) !== null)
|
|
355
|
+
return `${pin.file} at the base has no ${pin.fn} as one function, which is pinned strictly (${pin.why})`;
|
|
312
356
|
continue;
|
|
357
|
+
}
|
|
313
358
|
const after = pinnedText(now, pin);
|
|
314
359
|
if (after === null)
|
|
315
360
|
return `${pin.file} no longer has ${pin.fn} as one function (${pin.why})`;
|
|
@@ -475,3 +520,181 @@ export function storeRewriteProblem(paths, candidate, base, pinned = PINNED_STOR
|
|
|
475
520
|
}
|
|
476
521
|
return null;
|
|
477
522
|
}
|
|
523
|
+
// ---- learning loops (docs/plans/learning-loops.md, "Safety", Protection 2-4) ----------------------------------------
|
|
524
|
+
/** The calls that record a check's facts: at disposition, and as the check's command starts. */
|
|
525
|
+
export const LEARNING_HOOKS = Object.freeze([
|
|
526
|
+
{ file: "src/builder.ts", inside: "settleProof", calls: "recordCheckFacts", from: "./check-facts.js", why: "every settled check records its facts" },
|
|
527
|
+
{ file: "src/builder.ts", inside: "settleProof>runVerification", calls: "captureCheckConditions", from: "./check-facts.js", why: "a check's conditions are captured as its command starts" },
|
|
528
|
+
]);
|
|
529
|
+
/** The bodies a release can't change by itself, every one strict: what a run is told from project memory, how a memory
|
|
530
|
+
* proposal and a decision are written, and where a check's conditions are captured. */
|
|
531
|
+
export const LEARNING_PINNED = Object.freeze([
|
|
532
|
+
{ file: "src/project-knowledge.ts", fn: "knowledgeContext", strict: true, why: "the decisions and references a run is given, and their snapshot" },
|
|
533
|
+
{ file: "src/project-learning.ts", fn: "learningContext", strict: true, why: "the lessons a run is given, and their snapshot" },
|
|
534
|
+
{ file: "src/memory-pass.ts", fn: "decideProposal", strict: true, why: "a memory proposal's one writer" },
|
|
535
|
+
{ file: "src/project-memory.ts", fn: "recordDecision", strict: true, why: "how a decision is recorded and what it supersedes" },
|
|
536
|
+
{ file: "src/project-memory.ts", fn: "retireDecision", strict: true, why: "how a decision is retired" },
|
|
537
|
+
{ file: "src/builder.ts", fn: "settleProof>runVerification", strict: true, why: "the check's command and the conditions it starts under" },
|
|
538
|
+
]);
|
|
539
|
+
/** Every file the learning manifest reads. */
|
|
540
|
+
export const LEARNING_HOOK_FILES = Object.freeze([...new Set([...LEARNING_HOOKS.map(hook => hook.file), ...LEARNING_PINNED.map(pin => pin.file)])]);
|
|
541
|
+
/** Each protected table and its only writer modules (the v149 migration in store.ts aside). The decision tables'
|
|
542
|
+
* existing writers stay theirs: project-memory.ts records and retires, project-delete.ts deletes a whole project. */
|
|
543
|
+
export const LEARNING_WRITERS = Object.freeze({
|
|
544
|
+
check_fact: ["src/check-facts.ts"], check_result: ["src/check-facts.ts"],
|
|
545
|
+
local_observation: ["src/flaky.ts"], ci_flaky_observation: ["src/flaky-ci.ts"], ci_pairing_rejected: ["src/flaky-ci.ts"],
|
|
546
|
+
learning_watch: ["src/flaky.ts", "src/lesson-notes.ts"],
|
|
547
|
+
lesson_note: ["src/lesson-notes.ts"], lesson_note_tally: ["src/lesson-notes.ts"],
|
|
548
|
+
memory_use: ["src/memory-use.ts"],
|
|
549
|
+
memory_tidy_proposal: ["src/memory-tidy.ts"], decision_merge: ["src/memory-tidy.ts"],
|
|
550
|
+
project_decision: ["src/memory-tidy.ts", "src/project-memory.ts", "src/project-delete.ts"],
|
|
551
|
+
decision_change: ["src/memory-tidy.ts", "src/project-memory.ts", "src/project-delete.ts"],
|
|
552
|
+
});
|
|
553
|
+
/** store.ts's v149 migration: the only place outside the writers that may name these tables in SQL. */
|
|
554
|
+
const LEARNING_MIGRATION = Object.freeze({ "src/store.ts": ["V149_SCHEMA", "V149_TABLES", "V149_REPO_TABLES"] });
|
|
555
|
+
const PROTECTED = Object.keys(LEARNING_WRITERS);
|
|
556
|
+
const tableWrite = (table) => new RegExp(String.raw `(?:\b(?:(?:INSERT(?:\s+OR\s+\w+)?|REPLACE)\s+INTO|UPDATE(?:\s+OR\s+\w+)?|DELETE\s+FROM)\s+(?:main\s*\.\s*)?["\`\[]?${table}["\`\]]?(?!\w)`
|
|
557
|
+
+ String.raw `|\b(?:DROP\s+(?:TABLE|TRIGGER|INDEX|VIEW)(?:\s+IF\s+EXISTS)?|ALTER\s+TABLE)\s+(?:main\s*\.\s*)?["\`\[]?${table})`, "i");
|
|
558
|
+
const TABLE_WRITES = PROTECTED.map(table => ({ table, write: tableWrite(table) }));
|
|
559
|
+
const WRITE_WORD = /\b(?:INSERT|UPDATE|DELETE|REPLACE|DROP|ALTER)\b/i;
|
|
560
|
+
const BARE_NAME = new RegExp(String.raw `^\s*(?:main\s*\.\s*)?["\`\[]?(?:${PROTECTED.join("|")})["\`\]]?\s*$`, "i");
|
|
561
|
+
/**
|
|
562
|
+
* Every construct in the file that could write a protected table: each string literal, template (its static parts)
|
|
563
|
+
* and `+` concatenation (its literal operands), whatever it's passed to — `prepare`, `exec`, a renamed `prepare`, a
|
|
564
|
+
* helper. A write verb next to a protected table's name, a table name split around an expression next to a write
|
|
565
|
+
* word, and a string that is only a protected table's name are each found.
|
|
566
|
+
*/
|
|
567
|
+
export function learningConstructs(source) {
|
|
568
|
+
const compiler = typescript();
|
|
569
|
+
const found = [];
|
|
570
|
+
const topOf = (node) => {
|
|
571
|
+
let at = node;
|
|
572
|
+
while (at.parent !== undefined && !compiler.isSourceFile(at.parent))
|
|
573
|
+
at = at.parent;
|
|
574
|
+
if (compiler.isFunctionDeclaration(at) || compiler.isClassDeclaration(at))
|
|
575
|
+
return at.name?.text ?? "";
|
|
576
|
+
if (compiler.isVariableStatement(at))
|
|
577
|
+
return at.declarationList.declarations.map(one => compiler.isIdentifier(one.name) ? one.name.text : "").join(",");
|
|
578
|
+
return "";
|
|
579
|
+
};
|
|
580
|
+
const fragmentRisk = (parts) => {
|
|
581
|
+
// Around each placeholder: the identifier characters just before and just after it.
|
|
582
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
583
|
+
const before = /[A-Za-z0-9_]*$/.exec(parts[i])[0].toLowerCase(), after = /^[A-Za-z0-9_]*/.exec(parts[i + 1])[0].toLowerCase();
|
|
584
|
+
if (before === "" && after === "")
|
|
585
|
+
continue;
|
|
586
|
+
if (PROTECTED.some(table => (before !== "" && table.startsWith(before) && table !== before) || (after !== "" && table.endsWith(after) && table !== after)
|
|
587
|
+
|| (before !== "" && after !== "" && table.startsWith(before) && table.endsWith(after))))
|
|
588
|
+
return true;
|
|
589
|
+
}
|
|
590
|
+
return false;
|
|
591
|
+
};
|
|
592
|
+
const consider = (node, parts) => {
|
|
593
|
+
const text = parts.join("${}");
|
|
594
|
+
const tables = TABLE_WRITES.filter(one => one.write.test(text)).map(one => one.table);
|
|
595
|
+
const unreadable = parts.length > 1 && WRITE_WORD.test(text) && fragmentRisk(parts);
|
|
596
|
+
const bare = parts.length === 1 && BARE_NAME.test(text);
|
|
597
|
+
if (tables.length > 0 || unreadable || bare)
|
|
598
|
+
found.push({ text: text.replace(/\s+/g, " ").trim(), tables, inside: topOf(node), unreadable, bare });
|
|
599
|
+
};
|
|
600
|
+
const isPlus = (node) => compiler.isBinaryExpression(node) && node.operatorToken.kind === compiler.SyntaxKind.PlusToken;
|
|
601
|
+
const flatten = (node, into) => {
|
|
602
|
+
let at = node;
|
|
603
|
+
while (compiler.isParenthesizedExpression(at))
|
|
604
|
+
at = at.expression;
|
|
605
|
+
if (isPlus(at)) {
|
|
606
|
+
flatten(at.left, into);
|
|
607
|
+
flatten(at.right, into);
|
|
608
|
+
}
|
|
609
|
+
else
|
|
610
|
+
into.push(at);
|
|
611
|
+
};
|
|
612
|
+
const visit = (node) => {
|
|
613
|
+
if (isPlus(node) && !(node.parent !== undefined && (isPlus(node.parent) || (compiler.isParenthesizedExpression(node.parent) && node.parent.parent !== undefined && isPlus(node.parent.parent))))) {
|
|
614
|
+
const operands = [];
|
|
615
|
+
flatten(node, operands);
|
|
616
|
+
if (operands.some(one => compiler.isStringLiteralLike(one) || compiler.isTemplateExpression(one))) {
|
|
617
|
+
const parts = [""];
|
|
618
|
+
for (const one of operands) {
|
|
619
|
+
if (compiler.isStringLiteralLike(one))
|
|
620
|
+
parts[parts.length - 1] += one.text;
|
|
621
|
+
else if (compiler.isTemplateExpression(one)) {
|
|
622
|
+
parts[parts.length - 1] += one.head.text;
|
|
623
|
+
for (const span of one.templateSpans)
|
|
624
|
+
parts.push(span.literal.text);
|
|
625
|
+
}
|
|
626
|
+
else
|
|
627
|
+
parts.push("");
|
|
628
|
+
}
|
|
629
|
+
if (parts.length > 1 || operands.length > 1)
|
|
630
|
+
consider(node, parts);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
if (compiler.isStringLiteralLike(node))
|
|
634
|
+
consider(node, [node.text]);
|
|
635
|
+
else if (compiler.isTemplateExpression(node))
|
|
636
|
+
consider(node, [node.head.text, ...node.templateSpans.map(span => span.literal.text)]);
|
|
637
|
+
compiler.forEachChild(node, visit);
|
|
638
|
+
};
|
|
639
|
+
visit(source);
|
|
640
|
+
return found;
|
|
641
|
+
}
|
|
642
|
+
/** Whether this construct may stand in this file: its writer module for every table it writes, or the migration's own
|
|
643
|
+
* declarations in store.ts. An unreadable or bare construct stands only in a writer of some protected table. */
|
|
644
|
+
function learningAllowed(file, one) {
|
|
645
|
+
if ((LEARNING_MIGRATION[file] ?? []).some(name => one.inside.split(",").includes(name)))
|
|
646
|
+
return true;
|
|
647
|
+
if (one.tables.length > 0)
|
|
648
|
+
return one.tables.every(table => LEARNING_WRITERS[table].includes(file));
|
|
649
|
+
return Object.values(LEARNING_WRITERS).some(writers => writers.includes(file));
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Why a changed file writes a learning-loops table (or a decision merge or restore) from a place that may not, or null
|
|
653
|
+
* (docs/plans/learning-loops.md, Protection 4). Every changed JavaScript or TypeScript file but tests is parsed; a
|
|
654
|
+
* construct its writer module doesn't own is new unless the base's copy of that file already had exactly that text
|
|
655
|
+
* (counted by its sha256, as storeRewriteProblem does). A file that can't be read or parsed needs a person. It reads
|
|
656
|
+
* text the candidate wrote, never runs it: it raises the bar for review; a person reviewing these files is the backstop.
|
|
657
|
+
*/
|
|
658
|
+
export function learningWriteProblem(paths, candidate, base) {
|
|
659
|
+
const scanned = paths.filter(path => /\.(?:[cm]?[jt]sx?)$/.test(path) && !/\.test\.[cm]?[jt]sx?$/.test(path) && !path.startsWith("test/")
|
|
660
|
+
&& !path.startsWith("node_modules/") && !path.includes("/node_modules/"));
|
|
661
|
+
if (scanned.length === 0)
|
|
662
|
+
return null;
|
|
663
|
+
if (candidate === null || base === null)
|
|
664
|
+
return "the changed files can't be read";
|
|
665
|
+
const key = (text) => createHash("sha256").update(text).digest("hex");
|
|
666
|
+
for (const path of scanned) {
|
|
667
|
+
const bytes = candidate(path);
|
|
668
|
+
if (bytes === null)
|
|
669
|
+
return `${path} can't be read`;
|
|
670
|
+
const now = parse(path, bytes);
|
|
671
|
+
if ("problem" in now)
|
|
672
|
+
return now.problem;
|
|
673
|
+
const before = base(path);
|
|
674
|
+
const was = before === null ? null : parse(path, before);
|
|
675
|
+
const kept = new Map();
|
|
676
|
+
if (was !== null && !("problem" in was))
|
|
677
|
+
for (const one of learningConstructs(was.source))
|
|
678
|
+
kept.set(key(one.text), (kept.get(key(one.text)) ?? 0) + 1);
|
|
679
|
+
for (const one of learningConstructs(now.source)) {
|
|
680
|
+
if (learningAllowed(path, one))
|
|
681
|
+
continue;
|
|
682
|
+
const left = kept.get(key(one.text)) ?? 0;
|
|
683
|
+
if (left > 0) {
|
|
684
|
+
kept.set(key(one.text), left - 1);
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
const what = one.unreadable ? "builds a learning-loops table's name around an expression in SQL" : one.bare ? "names a learning-loops table outside its writers" : "writes learning-loops rows outside their writers";
|
|
688
|
+
return `${path} ${what} (\`${one.text.slice(0, 80)}\`)`;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
return null;
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Why a candidate needs a person for learning loops, or null: a required call dropped, a pinned body changed (strict
|
|
695
|
+
* pins included), or a changed file writing a protected table from outside its writers. With `manifest.base`, only what
|
|
696
|
+
* the base holds is required, as evalHookProblem does — except a strict pin whose file the base holds.
|
|
697
|
+
*/
|
|
698
|
+
export function learningHookProblem(manifest, changed) {
|
|
699
|
+
return evalHookProblem(manifest.candidate, manifest.base, LEARNING_HOOKS, LEARNING_PINNED) ?? learningWriteProblem(changed.paths, changed.candidate, changed.base);
|
|
700
|
+
}
|
package/dist/flaky.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mixed outcomes under recorded conditions (docs/plans/learning-loops.md, "Flaky tests"): advisory only. Nothing here
|
|
3
|
+
* changes a check's outcome, a verdict, an approval or a rerun; rerunning stays a person's `task regate`.
|
|
4
|
+
*
|
|
5
|
+
* Slice 1 holds the tracked identities an observation adds (`learning_watch`, reason `observation`): every Full check
|
|
6
|
+
* with a handshake asks about them for 90 days (check-facts.ts learnRequestFor). Lesson notes (lesson-notes.ts) add
|
|
7
|
+
* their own. This module and lesson-notes.ts are the only writers of `learning_watch`; release-auto's learning scan
|
|
8
|
+
* (eval-hooks.ts learningWriteProblem) needs a person for any other. A policy path (release-auto.ts EVAL_POLICY).
|
|
9
|
+
*/
|
|
10
|
+
import type { Store } from "./store.js";
|
|
11
|
+
/** Track an identity a local observation named (one row per observation; first write wins, never rewritten). */
|
|
12
|
+
export declare function watchObservation(store: Store, input: {
|
|
13
|
+
repo: string;
|
|
14
|
+
identity: string;
|
|
15
|
+
observation: number;
|
|
16
|
+
now: Date;
|
|
17
|
+
}): boolean;
|
package/dist/flaky.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Track an identity a local observation named (one row per observation; first write wins, never rewritten). */
|
|
2
|
+
export function watchObservation(store, input) {
|
|
3
|
+
if (input.repo.trim() === "" || input.identity.length < 1 || input.identity.length > 2600 || !Number.isSafeInteger(input.observation) || input.observation < 1) {
|
|
4
|
+
throw new Error("a tracked identity needs its project, a bounded identity and the observation that named it");
|
|
5
|
+
}
|
|
6
|
+
const added = store.handle.prepare(`INSERT INTO learning_watch (repo, identity, reason, source_ref, since, until) VALUES (?, ?, 'observation', ?, ?, NULL)
|
|
7
|
+
ON CONFLICT (repo, identity, reason, source_ref) DO NOTHING`).run(input.repo, input.identity, `observation:${input.observation}`, input.now.toISOString());
|
|
8
|
+
return Number(added.changes) === 1;
|
|
9
|
+
}
|
package/dist/flow-engine.js
CHANGED
|
@@ -507,7 +507,7 @@ export function revisionWithNote(store, card, target, note, actor, repos, eviden
|
|
|
507
507
|
return null;
|
|
508
508
|
const revised = requestResultChanges(store, evidenceRoot, {
|
|
509
509
|
run: runId, source: revisionSourceOf(store.getScope(runTask)?.digest ?? null), actor, repos,
|
|
510
|
-
batch: "", note, path: "", line: "", request: `flow-${card.id}-${card.entry}`, allowMode: true,
|
|
510
|
+
batch: "", note, path: "", line: "", request: `flow-${card.id}-${card.entry}`, allowMode: true, origin: "flow",
|
|
511
511
|
}, now);
|
|
512
512
|
return revised.ok ? revised.id : null;
|
|
513
513
|
}
|
|
@@ -140,7 +140,7 @@ export async function pullRequestStep(store, flow, definition, stage, card, now,
|
|
|
140
140
|
const runTask = store.externalIdFor(store.getRun(runId)?.taskRef ?? -1);
|
|
141
141
|
const revised = runTask === null ? null : requestResultChanges(store, io.evidenceRoot, {
|
|
142
142
|
run: runId, source: revisionSourceOf(store.getScope(runTask)?.digest ?? null), actor: "toolroll", repos: [flow.repo], batch: "",
|
|
143
|
-
note: found?.note ?? said, path: "", line: "", allowMode: true,
|
|
143
|
+
note: found?.note ?? said, path: "", line: "", allowMode: true, origin: "flow",
|
|
144
144
|
request: createHash("sha256").update(`flow-ci ${card.id} ${card.entry} ${publication.id}`).digest("hex").slice(0, 32),
|
|
145
145
|
}, now);
|
|
146
146
|
if (revised?.ok === true)
|
package/dist/lead-doors.js
CHANGED
|
@@ -25,7 +25,7 @@ import { teamChatAuthorization, subscriptionTeamChatProvider } from './team-chat
|
|
|
25
25
|
import { readProjectAccess } from "./project-access.js";
|
|
26
26
|
import { TeamLeads } from './team-leads.js';
|
|
27
27
|
import { fileTaskProposal } from "./proposal.js";
|
|
28
|
-
import { proposeGuarded } from "./scope.js";
|
|
28
|
+
import { machineActing, proposeGuarded } from "./scope.js";
|
|
29
29
|
import { isTaskSize, PHASES, specWords } from "./phase-routing.js";
|
|
30
30
|
import { isProviderId } from "./provider.js";
|
|
31
31
|
import { applyChatReview } from "./chat-review.js";
|
|
@@ -377,7 +377,9 @@ function executeProposal(store, actor, kind, payload, now, options) {
|
|
|
377
377
|
const request = payload;
|
|
378
378
|
if (request.snapshot == null || request.snapshot.task !== taskId || (request.operation !== "note" && request.operation !== "revise"))
|
|
379
379
|
return refuse("refused", "This feedback card is incomplete. Ask again.");
|
|
380
|
-
|
|
380
|
+
// A review card is the lead's (or a coordinator's) proposal; a person confirming it here decided the result needs
|
|
381
|
+
// these changes, so the revision is theirs. Under the lead's own chain or a remote run nobody confirmed it: lead.
|
|
382
|
+
const result = applyChatReview(store, actor, options.evidenceRoot, request, now, options.via !== "cli", machineActing() ? "lead" : "person");
|
|
381
383
|
return result.ok ? { ok: true, kind, taskId: result.taskId, said: result.said } : refuse("stale", result.message);
|
|
382
384
|
}
|
|
383
385
|
// The place the proposer saw must be the place the task holds now
|
package/dist/lead-tools.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { rulesOf, ruleWords as earnedRuleWords, suggestionsFor, suggestionWords, trialOffersFor, trialOfferWords, trialsOf, trialWords } from "./route-rules.js";
|
|
1
2
|
import { sameFeedbackImage } from "./result-review.js";
|
|
2
3
|
import { sweepLine, sweepTable } from "./lead-sweep.js";
|
|
3
4
|
import { connectionsOf, oneClickOf } from "./mcp-connect.js";
|
|
@@ -1602,7 +1603,7 @@ const LEAD_TOOL_HANDLERS = {
|
|
|
1602
1603
|
},
|
|
1603
1604
|
},
|
|
1604
1605
|
get_models: {
|
|
1605
|
-
description: "Default agent per role with the exact model each name runs now, installed CLI versions and updates,
|
|
1606
|
+
description: "Default agent per role with the exact model each name runs now, installed CLI versions and updates, models released in the last two weeks, and cheaper models your projects' own results suggest (with their earned rules), cheaper models to try and the trials in use. Read-only; accepting and starting a trial happen in Settings → Models; Not now is propose_action suggestion_not_now; stopping a trial is propose_action trial_stop.",
|
|
1606
1607
|
handle: (ctx) => {
|
|
1607
1608
|
const roles = ["plan", "build", "review", "repair"].map(phase => {
|
|
1608
1609
|
const row = ctx.store.phaseConfig(INSTALLATION_SCOPE, phase);
|
|
@@ -1610,7 +1611,14 @@ const LEAD_TOOL_HANDLERS = {
|
|
|
1610
1611
|
});
|
|
1611
1612
|
const tools = runtimeStates(ctx.store).map(one => ({ name: one.name, installed: one.installed, latest: one.latest, updateAvailable: one.behind }));
|
|
1612
1613
|
const newModels = ["claude", "codex", "gemini"].flatMap(source => seenModels(ctx.store, source)).filter(model => isNewModel(model, ctx.now)).map(model => ({ name: model.name, id: model.id, price: priceWords(model), released: model.releasedAt }));
|
|
1613
|
-
|
|
1614
|
+
// Results routing: the same suggestions and rules Settings → Models and `toolroll models suggestions` show.
|
|
1615
|
+
const suggestions = ctx.who.repos.flatMap(repo => suggestionsFor(ctx.store, repo, ctx.now)).map(one => ({ id: one.id, repo: one.repo, words: suggestionWords(one),
|
|
1616
|
+
review: `/settings/models?scope=${encodeURIComponent(one.repo)}&suggestion=${encodeURIComponent(one.id)}#models-suggested` }));
|
|
1617
|
+
const rules = ctx.who.repos.flatMap(repo => rulesOf(ctx.store, repo, ctx.now)).map(one => ({ id: one.id, repo: one.repo, state: one.state, words: earnedRuleWords(ctx.store, one) }));
|
|
1618
|
+
const trialOffers = ctx.who.repos.flatMap(repo => trialOffersFor(ctx.store, repo, ctx.now)).map(one => ({ id: one.id, repo: one.repo, words: trialOfferWords(one),
|
|
1619
|
+
start: `/settings/models?scope=${encodeURIComponent(one.repo)}#models-suggested` }));
|
|
1620
|
+
const trials = ctx.who.repos.flatMap(repo => trialsOf(ctx.store, repo, ctx.now)).map(one => ({ id: one.id, repo: one.repo, state: one.state, words: trialWords(ctx.store, one) }));
|
|
1621
|
+
return { ok: true, body: { roles, tools, newModels, suggestions, rules, trialOffers, trials, change: "/settings/models" } };
|
|
1614
1622
|
},
|
|
1615
1623
|
},
|
|
1616
1624
|
list_tasks: {
|
package/dist/local-owner.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export declare function authorityOf(value: unknown): Authority | null;
|
|
|
10
10
|
* The acts a local owner may do (plan C1). Ids only: each surface (a route row, a chat action, a CLI command) names
|
|
11
11
|
* the act it is, and an act not in this list is refused to a local owner.
|
|
12
12
|
*/
|
|
13
|
-
export declare const LOCAL_OWNER_ACTS: readonly ["file-task", "lead-send", "approve-plan", "answer-question", "stop-run", "pause-run", "resume-run", "complete-task", "request-revision", "archive-task", "add-project", "choose-default-agent", "mark-seen", "watch-agent", "read"];
|
|
13
|
+
export declare const LOCAL_OWNER_ACTS: readonly ["file-task", "lead-send", "approve-plan", "answer-question", "stop-run", "pause-run", "resume-run", "complete-task", "request-revision", "archive-task", "add-project", "choose-default-agent", "mark-seen", "watch-agent", "read", "routing-turn-off", "dismiss-suggestion", "trial-stop"];
|
|
14
14
|
export type LocalOwnerAct = (typeof LOCAL_OWNER_ACTS)[number];
|
|
15
15
|
/** Whether a runtime string names an act a local owner may do. */
|
|
16
16
|
export declare function localOwnerAct(value: unknown): value is LocalOwnerAct;
|
package/dist/local-owner.js
CHANGED
|
@@ -32,6 +32,13 @@ export const LOCAL_OWNER_ACTS = Object.freeze([
|
|
|
32
32
|
"mark-seen",
|
|
33
33
|
"watch-agent",
|
|
34
34
|
"read",
|
|
35
|
+
// Results routing (docs/plans/results-routing.md, Authority): lowering only — turning an earned rule off (or
|
|
36
|
+
// dropping a paused one), and saying Not now to a suggestion (or switching suggestions off). The writer
|
|
37
|
+
// (route-store.ts proveRuleDoor) checks the in-flight act is exactly the one it needs.
|
|
38
|
+
"routing-turn-off",
|
|
39
|
+
"dismiss-suggestion",
|
|
40
|
+
// Slice 3: stopping a trial of a cheaper model only lowers (proveRuleDoor checks this exact act).
|
|
41
|
+
"trial-stop",
|
|
35
42
|
]);
|
|
36
43
|
const ACT_SET = new Set(LOCAL_OWNER_ACTS);
|
|
37
44
|
/** Whether a runtime string names an act a local owner may do. */
|
|
@@ -150,6 +157,10 @@ export const CHAT_ACTION_LOCAL_ACTS = Object.freeze({
|
|
|
150
157
|
subagent_create: null, subagent_soul: null, subagent_state: null, subagent_note: null, subagent_ask: null,
|
|
151
158
|
subagent_answer: null, subagent_tools: null, subagent_memory: null, subagent_routine: null, subagent_undo: null,
|
|
152
159
|
kit_setup: null, decision_record: null, decision_retire: null, lead_about_you: null,
|
|
160
|
+
// Results routing: Not now only lowers (the rule door checks this exact act).
|
|
161
|
+
suggestion_not_now: "dismiss-suggestion",
|
|
162
|
+
// Stopping a trial only lowers (the rule door checks this exact act).
|
|
163
|
+
trial_stop: "trial-stop",
|
|
153
164
|
scope_approve: "approve-plan",
|
|
154
165
|
result_accept: "complete-task",
|
|
155
166
|
task_cancel: "stop-run",
|
package/dist/memory-cli.d.ts
CHANGED
|
@@ -200,6 +200,7 @@ export declare const MEMORY_DESCRIPTORS: readonly [{
|
|
|
200
200
|
readonly takesValue: false;
|
|
201
201
|
readonly meaning: 'read only the plane\'s own transcripts, not local Claude Code or Codex session files';
|
|
202
202
|
}];
|
|
203
|
+
readonly audience: 'operator';
|
|
203
204
|
}, {
|
|
204
205
|
readonly action: 'retire';
|
|
205
206
|
readonly synopsis: 'retire a decision that no longer holds: memory retire <id> --reason "<why>"';
|
|
@@ -250,6 +251,7 @@ export declare const MEMORY_DESCRIPTORS: readonly [{
|
|
|
250
251
|
readonly takesValue: false;
|
|
251
252
|
readonly meaning: 'read only the plane\'s own transcripts, not local Claude Code or Codex session files';
|
|
252
253
|
}];
|
|
254
|
+
readonly audience: 'operator';
|
|
253
255
|
}, {
|
|
254
256
|
readonly action: 'propose';
|
|
255
257
|
readonly synopsis: 'the backward pass: read recent sessions, keep a ledger of evidenced gaps, propose a few edits (writes nothing to memory)';
|
|
@@ -400,6 +402,7 @@ export declare const MEMORY_DESCRIPTORS: readonly [{
|
|
|
400
402
|
readonly takesValue: false;
|
|
401
403
|
readonly meaning: 'read only the plane\'s own transcripts, not local Claude Code or Codex session files';
|
|
402
404
|
}];
|
|
405
|
+
readonly audience: 'operator';
|
|
403
406
|
}, {
|
|
404
407
|
readonly action: 'status';
|
|
405
408
|
readonly synopsis: 'surface version, instruction budget, sessions analysed, open gaps and pending proposals';
|
package/dist/memory-cli.js
CHANGED
|
@@ -21,11 +21,11 @@ export const MEMORY_DESCRIPTORS = [
|
|
|
21
21
|
{ action: 'search', synopsis: 'search decisions, instructions, references, lessons and your conversations', mutation: 'none', takesQuery: true, flags },
|
|
22
22
|
{ action: 'decisions', synopsis: "list a project's active decisions (--all for history)", mutation: 'none', takesQuery: false, flags },
|
|
23
23
|
{ action: 'show', synopsis: 'read one decision with its reason and history', mutation: 'none', takesQuery: true, flags },
|
|
24
|
-
{ action: 'decide', synopsis: 'record a settled choice: memory decide "<choice>" --why "<reason>" --repo PATH', mutation: 'unkeyed', takesQuery: true, flags },
|
|
25
|
-
{ action: 'retire', synopsis: 'retire a decision that no longer holds: memory retire <id> --reason "<why>"', mutation: 'unkeyed', takesQuery: true, flags },
|
|
24
|
+
{ action: 'decide', synopsis: 'record a settled choice: memory decide "<choice>" --why "<reason>" --repo PATH', mutation: 'unkeyed', takesQuery: true, flags, audience: 'operator' },
|
|
25
|
+
{ action: 'retire', synopsis: 'retire a decision that no longer holds: memory retire <id> --reason "<why>"', mutation: 'unkeyed', takesQuery: true, flags, audience: 'operator' },
|
|
26
26
|
{ action: 'propose', synopsis: 'the backward pass: read recent sessions, keep a ledger of evidenced gaps, propose a few edits (writes nothing to memory)', mutation: 'unkeyed', takesQuery: false, flags },
|
|
27
27
|
{ action: 'review', synopsis: 'list pending memory proposals with their evidence', mutation: 'none', takesQuery: false, flags },
|
|
28
|
-
{ action: 'apply', synopsis: 'decide one proposal: memory apply <id> --decision accept|reject — the only writer', mutation: 'unkeyed', takesQuery: true, flags },
|
|
28
|
+
{ action: 'apply', synopsis: 'decide one proposal: memory apply <id> --decision accept|reject — the only writer', mutation: 'unkeyed', takesQuery: true, flags, audience: 'operator' },
|
|
29
29
|
{ action: 'status', synopsis: 'surface version, instruction budget, sessions analysed, open gaps and pending proposals', mutation: 'none', takesQuery: false, flags },
|
|
30
30
|
];
|
|
31
31
|
const line = (d) => `#${d.id} ${d.claim} — ${d.decidedBy}, ${d.decidedAt.slice(0, 10)}${d.status === 'active' ? '' : ` (${d.status})`}`;
|
package/dist/memory-pass.js
CHANGED
|
@@ -17,7 +17,7 @@ import { homedir } from 'node:os';
|
|
|
17
17
|
import { join } from 'node:path';
|
|
18
18
|
import { scanForSecrets, readVerifiedArtifact } from './evidence.js';
|
|
19
19
|
import { changeKnowledge, knowledgeView } from './project-knowledge.js';
|
|
20
|
-
import { listDecisions, recordDecision } from './project-memory.js';
|
|
20
|
+
import { listDecisions, recordDecision, refuseLeadMemoryWrite } from './project-memory.js';
|
|
21
21
|
import { composeLeadRequest, performLeadRequest, isDirectChatProvider, CHAT_KEY_ENV } from './converse.js';
|
|
22
22
|
import { performSubscriptionLeadRequest } from './subscription-chat.js';
|
|
23
23
|
import { TEXT_LIMITS } from './text-limits.js';
|
|
@@ -399,7 +399,11 @@ export function listProposals(store, repo, status = 'pending') {
|
|
|
399
399
|
}
|
|
400
400
|
/** The one writer: accept applies the edit through the knowledge or decision store as the person; reject remembers the fingerprint and its corroboration. */
|
|
401
401
|
export function decideProposal(store, args, now = new Date()) {
|
|
402
|
+
refuseLeadMemoryWrite();
|
|
402
403
|
return store.transact(() => {
|
|
404
|
+
// A person who may change this project's memory, read inside the write (a revoked account or lost access refuses).
|
|
405
|
+
if (!store.accountCanAccess(args.actor, args.repo) || store.accountOf(args.actor)?.role !== 'approver')
|
|
406
|
+
throw Error('Project memory is outside your access.');
|
|
403
407
|
const row = store.handle.prepare('SELECT * FROM memory_proposal WHERE id=? AND repo=?').get(args.id, args.repo);
|
|
404
408
|
if (!row)
|
|
405
409
|
throw Error('That proposal is unavailable.');
|
package/dist/model-choices.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type SeenModel } from "./model-catalog.js";
|
|
2
|
-
import { type PhaseRoute } from "./phase-routing.js";
|
|
2
|
+
import { type EarnedRule, type PhaseRoute } from "./phase-routing.js";
|
|
3
3
|
import type { Store } from "./store.js";
|
|
4
4
|
export type SlotPhase = "plan" | "build" | "repair" | "review";
|
|
5
5
|
export type SlotTier = "routine" | "light" | "strong";
|
|
@@ -76,7 +76,9 @@ export declare const ROUTE_CASES: readonly {
|
|
|
76
76
|
risky: boolean;
|
|
77
77
|
strict: boolean;
|
|
78
78
|
}[];
|
|
79
|
-
export declare function effectiveRoutes(store: Store, repo: string | null
|
|
79
|
+
export declare function effectiveRoutes(store: Store, repo: string | null, options?: {
|
|
80
|
+
rules?: readonly EarnedRule[] | false;
|
|
81
|
+
}): {
|
|
80
82
|
ok: true;
|
|
81
83
|
routes: {
|
|
82
84
|
label: string;
|
|
@@ -164,7 +166,9 @@ export type ScopeCheck = {
|
|
|
164
166
|
* or a route of any size or demand (small, routine, large, risky, strict) whose repair can't resume its builder,
|
|
165
167
|
* inherited rows included. An invalid result is never saved.
|
|
166
168
|
*/
|
|
167
|
-
export declare function checkScope(store: Store, repo: string | null
|
|
169
|
+
export declare function checkScope(store: Store, repo: string | null, options?: {
|
|
170
|
+
rules?: readonly EarnedRule[];
|
|
171
|
+
}): ScopeCheck;
|
|
168
172
|
/** A resolver's words for a browser: what is wrong, never a terminal command to fix it. */
|
|
169
173
|
export declare function browserWords(problem: string): string;
|
|
170
174
|
/** The problem alone, or null when tasks can be filed. */
|