toolroll 0.9.32 → 0.9.34
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 +22 -0
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +17 -17
- package/dist/browser-crew.js +1 -0
- package/dist/browser-workspace.d.ts +2 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +124 -63
- package/dist/coding-context.js +2 -2
- package/dist/coding-handoff.js +2 -2
- package/dist/contracts/contract-test.d.ts +45 -0
- package/dist/contracts/contract-test.js +69 -0
- package/dist/contracts/contract.d.ts +59 -0
- package/dist/contracts/contract.js +181 -0
- package/dist/contracts/plan.d.ts +58 -0
- package/dist/contracts/plan.js +67 -0
- package/dist/desktop-update.d.ts +1 -1
- package/dist/evidence.js +2 -2
- 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/operate.js +65 -13
- package/dist/plan.d.ts +12 -12
- package/dist/plan.js +129 -72
- package/dist/planner-source.js +2 -1
- package/dist/planner.js +55 -25
- package/dist/project-admission.d.ts +93 -0
- package/dist/project-admission.js +134 -0
- package/dist/project-delete-ui.js +5 -2
- 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/runner.d.ts +10 -0
- package/dist/runner.js +16 -0
- package/dist/scope.js +4 -3
- package/dist/serve.d.ts +4 -0
- package/dist/serve.js +111 -65
- package/dist/store.d.ts +2 -0
- package/dist/store.js +4 -0
- package/dist/surface.js +9 -3
- package/dist/text-limits.d.ts +17 -0
- package/dist/text-limits.js +16 -0
- package/dist/when-html.d.ts +2 -0
- package/dist/when-html.js +13 -0
- package/package.json +3 -2
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one way Toolroll describes a contract (docs/plans/zod-revamp.md): one Zod schema, and everything else — the
|
|
3
|
+
* TypeScript type, the runtime check, the JSON Schema a model gets, the text limits — derived from it.
|
|
4
|
+
*
|
|
5
|
+
* The helpers here hold the ground rules every contract shares: limits come from TEXT_LIMITS, persisted payloads are
|
|
6
|
+
* versioned, refusals name the path, and a model-facing schema is exactly what its JSON Schema says (no refinements or
|
|
7
|
+
* transforms; those checks run in plain code after parsing, with a named error).
|
|
8
|
+
*/
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { limitRule, TEXT_LIMITS } from "../text-limits.js";
|
|
11
|
+
/**
|
|
12
|
+
* A string bounded by its TEXT_LIMITS entry, never a literal. The limit rule rides along as the field's description,
|
|
13
|
+
* so a model reads the bound before it writes. Zod counts UTF-16 code units; for a limit named in bytes this is the
|
|
14
|
+
* schema's necessary bound (a string within N bytes is within N code units), and the byte count itself is checked in
|
|
15
|
+
* plain code after parsing.
|
|
16
|
+
*/
|
|
17
|
+
export function limited(field, limitKey) {
|
|
18
|
+
const limit = TEXT_LIMITS[limitKey];
|
|
19
|
+
const unit = limitKey.endsWith("Bytes") ? "bytes" : "characters";
|
|
20
|
+
return z.string().max(limit, { error: `over ${limit.toLocaleString("en-US")} ${unit}` }).describe(limitRule(field, limit, unit));
|
|
21
|
+
}
|
|
22
|
+
/** A versioned payload envelope: a strict object whose `version` is exactly `version`. */
|
|
23
|
+
export function versioned(version, shape) {
|
|
24
|
+
return z.strictObject({ version: z.literal(version), ...shape });
|
|
25
|
+
}
|
|
26
|
+
/** `["steps", 0, "routes", 0, "goesTo"]` as `steps[0].routes[0].goesTo`. */
|
|
27
|
+
export function pathOf(path) {
|
|
28
|
+
let out = "";
|
|
29
|
+
for (const part of path) {
|
|
30
|
+
if (typeof part === "number")
|
|
31
|
+
out += `[${part}]`;
|
|
32
|
+
else
|
|
33
|
+
out += out === "" ? String(part) : `.${String(part)}`;
|
|
34
|
+
}
|
|
35
|
+
return out === "" ? "payload" : out;
|
|
36
|
+
}
|
|
37
|
+
function got(input) {
|
|
38
|
+
if (input === null)
|
|
39
|
+
return "null";
|
|
40
|
+
if (Array.isArray(input))
|
|
41
|
+
return "an array";
|
|
42
|
+
return typeof input === "object" ? "an object" : `a ${typeof input}`;
|
|
43
|
+
}
|
|
44
|
+
function issuesOf(issue) {
|
|
45
|
+
const at = pathOf(issue.path);
|
|
46
|
+
const one = (kind, what) => ({ path: at, kind, line: `${at}: ${what}` });
|
|
47
|
+
switch (issue.code) {
|
|
48
|
+
case "unrecognized_keys":
|
|
49
|
+
return issue.keys.map(key => ({ path: at, kind: "unknown-key", line: `${at}: unknown key '${key}'` }));
|
|
50
|
+
case "invalid_type": {
|
|
51
|
+
const missing = "input" in issue ? issue.input === undefined : / received undefined$/.test(issue.message);
|
|
52
|
+
if (missing)
|
|
53
|
+
return [one("required", "required")];
|
|
54
|
+
return [one("wrong-type", `must be ${issue.expected === "array" ? "an array" : issue.expected === "object" ? "an object" : `a ${issue.expected}`}${"input" in issue ? ` (got ${got(issue.input)})` : ""}`)];
|
|
55
|
+
}
|
|
56
|
+
case "too_big":
|
|
57
|
+
if (issue.origin === "array" || issue.origin === "set")
|
|
58
|
+
return [one("too-many", `at most ${String(issue.maximum)} items`)];
|
|
59
|
+
return [one("too-long", issue.origin === "string" && issue.message.startsWith("over ") ? issue.message : `at most ${String(issue.maximum)}${issue.origin === "string" ? " characters" : ""}`)];
|
|
60
|
+
case "too_small":
|
|
61
|
+
if (issue.origin === "string" && Number(issue.minimum) === 1)
|
|
62
|
+
return [one("empty", "must not be empty")];
|
|
63
|
+
if (issue.origin === "array" || issue.origin === "set")
|
|
64
|
+
return [one("too-few", `at least ${String(issue.minimum)} item${Number(issue.minimum) === 1 ? "" : "s"}`)];
|
|
65
|
+
return [one("too-few", `at least ${String(issue.minimum)}`)];
|
|
66
|
+
case "invalid_value":
|
|
67
|
+
return [one("bad-value", `must be ${issue.values.length === 1 ? JSON.stringify(issue.values[0]) : `one of ${issue.values.map(value => JSON.stringify(value)).join(", ")}`}`)];
|
|
68
|
+
case "invalid_union":
|
|
69
|
+
return [one("invalid", "does not match any allowed shape")];
|
|
70
|
+
default:
|
|
71
|
+
return [one("invalid", issue.message)];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** A Zod error as path-named issues, in the error's own order. */
|
|
75
|
+
export function contractIssues(error) {
|
|
76
|
+
return error.issues.flatMap(issuesOf);
|
|
77
|
+
}
|
|
78
|
+
/** A Zod error as path-named lines: `steps[0].routes[0].goesTo: required`, `routes[0]: unknown key 'to'`. */
|
|
79
|
+
export function contractError(error) {
|
|
80
|
+
return contractIssues(error).map(issue => issue.line);
|
|
81
|
+
}
|
|
82
|
+
/** Parse with the input reported, so a missing field reads `required` rather than a wrong type. */
|
|
83
|
+
export function parseContract(schema, input) {
|
|
84
|
+
const parsed = schema.safeParse(input, { reportInput: true });
|
|
85
|
+
return parsed.success ? { ok: true, value: parsed.data } : { ok: false, issues: contractIssues(parsed.error) };
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Read a versioned payload. The current version parses as itself; an older known version (0 for a payload saved
|
|
89
|
+
* before it carried `version`) is upgraded by its adapter and then parsed by the same schema; a newer version is
|
|
90
|
+
* refused plainly; anything else is an unknown version.
|
|
91
|
+
*/
|
|
92
|
+
export function readVersioned(schema, input, upgrades = {}) {
|
|
93
|
+
const current = schema.shape.version.value;
|
|
94
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
95
|
+
return { ok: false, issues: [{ path: "payload", kind: "wrong-type", line: `payload: must be an object (got ${got(input)})` }] };
|
|
96
|
+
}
|
|
97
|
+
const body = input;
|
|
98
|
+
const version = Object.prototype.hasOwnProperty.call(body, "version") ? body["version"] : 0;
|
|
99
|
+
if (version === current)
|
|
100
|
+
return parseContract(schema, body);
|
|
101
|
+
if (typeof version === "number" && Number.isInteger(version) && version > current) {
|
|
102
|
+
return { ok: false, issues: [{ path: "version", kind: "newer-version", line: `version: made by a newer Toolroll (version ${version}; this one reads up to ${current})` }] };
|
|
103
|
+
}
|
|
104
|
+
const upgrade = typeof version === "number" ? upgrades[version] : undefined;
|
|
105
|
+
if (upgrade === undefined) {
|
|
106
|
+
return { ok: false, issues: [{ path: "version", kind: "bad-value", line: `version: unknown version ${JSON.stringify(version)} (this Toolroll reads ${current}${Object.keys(upgrades).length === 0 ? "" : ` and ${Object.keys(upgrades).join(", ")}`})` }] };
|
|
107
|
+
}
|
|
108
|
+
return parseContract(schema, upgrade(body));
|
|
109
|
+
}
|
|
110
|
+
/** The Zod node kinds a model-facing schema may use: each says exactly what its JSON Schema says. */
|
|
111
|
+
const MODEL_FACING_TYPES = new Set(["string", "number", "int", "boolean", "null", "literal", "enum", "array", "object", "optional", "nullable", "union", "tuple", "record", "lazy", "readonly", "unknown", "any"]);
|
|
112
|
+
/** Checks that only constrain (never rewrite) and that JSON Schema can state. */
|
|
113
|
+
const MODEL_FACING_CHECKS = new Set(["max_length", "min_length", "length_equals", "greater_than", "less_than", "multiple_of", "number_format", "string_format", "size_equals", "max_size", "min_size"]);
|
|
114
|
+
/** Every node of a schema graph, refusing a refinement, transform, preprocess or unknown node by its schema path. */
|
|
115
|
+
function assertModelFacing(schema, at, seen) {
|
|
116
|
+
if (seen.has(schema))
|
|
117
|
+
return;
|
|
118
|
+
seen.add(schema);
|
|
119
|
+
const def = schema._zod.def;
|
|
120
|
+
const where = at === "" ? "the schema" : at;
|
|
121
|
+
if (def.type === "pipe") {
|
|
122
|
+
const into = def["in"]?._zod.def.type;
|
|
123
|
+
const out = def["out"]?._zod.def.type;
|
|
124
|
+
const what = into === "transform" ? "preprocess" : out === "transform" ? "transform" : "pipe";
|
|
125
|
+
throw new Error(`toModelSchema: ${where} uses ${what}; a model-facing schema must be JSON-Schema-exact — check it in plain code after parsing`);
|
|
126
|
+
}
|
|
127
|
+
if (def.type === "transform")
|
|
128
|
+
throw new Error(`toModelSchema: ${where} uses transform; a model-facing schema must be JSON-Schema-exact — check it in plain code after parsing`);
|
|
129
|
+
if (!MODEL_FACING_TYPES.has(def.type))
|
|
130
|
+
throw new Error(`toModelSchema: ${where} uses a ${def.type} node, which a model-facing schema may not use`);
|
|
131
|
+
for (const check of def.checks ?? []) {
|
|
132
|
+
const kind = check._zod.def.check;
|
|
133
|
+
if (kind === "custom")
|
|
134
|
+
throw new Error(`toModelSchema: ${where} uses refine or superRefine; a model-facing schema must be JSON-Schema-exact — check it in plain code after parsing`);
|
|
135
|
+
if (!MODEL_FACING_CHECKS.has(kind))
|
|
136
|
+
throw new Error(`toModelSchema: ${where} uses a ${kind} check, which rewrites or cannot be stated in JSON Schema`);
|
|
137
|
+
}
|
|
138
|
+
const child = (value, path) => assertModelFacing(value, path, seen);
|
|
139
|
+
const join = (key) => (at === "" ? key : `${at}.${key}`);
|
|
140
|
+
switch (def.type) {
|
|
141
|
+
case "object":
|
|
142
|
+
for (const [key, value] of Object.entries(def["shape"]))
|
|
143
|
+
child(value, join(key));
|
|
144
|
+
// A strict object's catchall is `never` (additionalProperties: false); any other catchall is a schema too.
|
|
145
|
+
if (def["catchall"] !== undefined && def["catchall"]._zod.def.type !== "never")
|
|
146
|
+
child(def["catchall"], join("*"));
|
|
147
|
+
break;
|
|
148
|
+
case "array":
|
|
149
|
+
child(def["element"], `${at}[]`);
|
|
150
|
+
break;
|
|
151
|
+
case "optional":
|
|
152
|
+
case "nullable":
|
|
153
|
+
case "readonly":
|
|
154
|
+
child(def["innerType"], at);
|
|
155
|
+
break;
|
|
156
|
+
case "union":
|
|
157
|
+
def["options"].forEach((option, index) => child(option, `${at}|${index}`));
|
|
158
|
+
break;
|
|
159
|
+
case "tuple":
|
|
160
|
+
def["items"].forEach((item, index) => child(item, `${at}[${index}]`));
|
|
161
|
+
if (def["rest"] !== undefined && def["rest"] !== null)
|
|
162
|
+
child(def["rest"], `${at}[]`);
|
|
163
|
+
break;
|
|
164
|
+
case "record":
|
|
165
|
+
child(def["keyType"], `${at}{key}`);
|
|
166
|
+
child(def["valueType"], `${at}{}`);
|
|
167
|
+
break;
|
|
168
|
+
case "lazy":
|
|
169
|
+
child(def["getter"](), at);
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* The JSON Schema a model gets for `schema` (Claude's `--json-schema`). Throws when the schema uses refine,
|
|
175
|
+
* superRefine, transform or preprocess anywhere: what a model is told must be everything the schema checks.
|
|
176
|
+
*/
|
|
177
|
+
export function toModelSchema(schema) {
|
|
178
|
+
assertModelFacing(schema, "", new Set());
|
|
179
|
+
const { $schema: _dialect, ...json } = z.toJSONSchema(schema, { target: "draft-2020-12", unrepresentable: "throw" });
|
|
180
|
+
return json;
|
|
181
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The planner's handoff: one schema for the file a planner writes, the JSON Schema it is given (`--json-schema`) and
|
|
3
|
+
* the parser that reads it back (`parsePlan` in plan.ts). The rules JSON Schema cannot state — the plan document's
|
|
4
|
+
* sections, UTF-8 byte limits, control characters, duplicate criterion ids, the Proof naming every criterion and the
|
|
5
|
+
* silent-amendment contract check — run in plain code after parsing, each with a named error.
|
|
6
|
+
*/
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { type ContractResult } from "./contract.js";
|
|
9
|
+
/** How many paths a plan may say it touches. */
|
|
10
|
+
export declare const PLAN_TOUCHES = 32;
|
|
11
|
+
/** One acceptance criterion: the scope's own shape (scope.ts `AcceptanceCriterion`), `how` advisory and unsigned. */
|
|
12
|
+
export declare const acceptanceCriterionSchema: z.ZodObject<{
|
|
13
|
+
id: z.ZodString;
|
|
14
|
+
statement: z.ZodString;
|
|
15
|
+
how: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
16
|
+
evidence: z.ZodArray<z.ZodEnum<{
|
|
17
|
+
"changed-path": "changed-path";
|
|
18
|
+
check: "check";
|
|
19
|
+
"manual-review": "manual-review";
|
|
20
|
+
screenshot: "screenshot";
|
|
21
|
+
}>>;
|
|
22
|
+
}, z.core.$strict>;
|
|
23
|
+
export declare const PLAN_VERSION = 1;
|
|
24
|
+
export declare const planSchema: z.ZodObject<{
|
|
25
|
+
version: z.ZodLiteral<1>;
|
|
26
|
+
goal: z.ZodString;
|
|
27
|
+
outOfScope: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
28
|
+
touches: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
29
|
+
acceptance: z.ZodArray<z.ZodObject<{
|
|
30
|
+
id: z.ZodString;
|
|
31
|
+
statement: z.ZodString;
|
|
32
|
+
how: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
33
|
+
evidence: z.ZodArray<z.ZodEnum<{
|
|
34
|
+
"changed-path": "changed-path";
|
|
35
|
+
check: "check";
|
|
36
|
+
"manual-review": "manual-review";
|
|
37
|
+
screenshot: "screenshot";
|
|
38
|
+
}>>;
|
|
39
|
+
}, z.core.$strict>>;
|
|
40
|
+
plan: z.ZodString;
|
|
41
|
+
amendment: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
42
|
+
}, z.core.$strict>;
|
|
43
|
+
export type PlanPayload = z.infer<typeof planSchema>;
|
|
44
|
+
/** What the planner's `--json-schema` is: the plan schema, exactly. */
|
|
45
|
+
export declare const PLAN_MODEL_SCHEMA: Readonly<Record<string, unknown>>;
|
|
46
|
+
/**
|
|
47
|
+
* A plan written before plans carried `version` (every plan through 0.9.33), as version 1. The old parser read only
|
|
48
|
+
* the fields it knew and ignored any others, read a null goal, plan or acceptance as missing and null touches as
|
|
49
|
+
* none; this keeps exactly that, so every plan that parsed then parses now. Values themselves are never rewritten.
|
|
50
|
+
*/
|
|
51
|
+
export declare function upgradeUnversionedPlan(body: Record<string, unknown>): Record<string, unknown>;
|
|
52
|
+
export declare const PLAN_UPGRADES: {
|
|
53
|
+
readonly 0: typeof upgradeUnversionedPlan;
|
|
54
|
+
};
|
|
55
|
+
/** The payload as version 1, upgraded when it was saved unversioned; null for anything that is not an object. */
|
|
56
|
+
export declare function planPayloadBody(input: unknown): Record<string, unknown> | null;
|
|
57
|
+
/** Read a plan payload: version 1 as itself, an unversioned one upgraded, a newer one refused plainly. */
|
|
58
|
+
export declare function readPlanPayload(input: unknown): ContractResult<PlanPayload>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The planner's handoff: one schema for the file a planner writes, the JSON Schema it is given (`--json-schema`) and
|
|
3
|
+
* the parser that reads it back (`parsePlan` in plan.ts). The rules JSON Schema cannot state — the plan document's
|
|
4
|
+
* sections, UTF-8 byte limits, control characters, duplicate criterion ids, the Proof naming every criterion and the
|
|
5
|
+
* silent-amendment contract check — run in plain code after parsing, each with a named error.
|
|
6
|
+
*/
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { ACCEPTANCE_LIMITS, EVIDENCE_KINDS } from "../scope.js";
|
|
9
|
+
import { limited, readVersioned, toModelSchema, versioned } from "./contract.js";
|
|
10
|
+
/** How many paths a plan may say it touches. */
|
|
11
|
+
export const PLAN_TOUCHES = 32;
|
|
12
|
+
/** One acceptance criterion: the scope's own shape (scope.ts `AcceptanceCriterion`), `how` advisory and unsigned. */
|
|
13
|
+
export const acceptanceCriterionSchema = z.strictObject({
|
|
14
|
+
id: limited("acceptance id", "acceptanceIdBytes").min(1),
|
|
15
|
+
statement: limited("acceptance statement", "acceptanceStatementBytes").min(1),
|
|
16
|
+
how: limited("acceptance how", "acceptanceHowBytes").nullable().optional(),
|
|
17
|
+
evidence: z.array(z.enum(EVIDENCE_KINDS)).min(1).max(EVIDENCE_KINDS.length),
|
|
18
|
+
});
|
|
19
|
+
export const PLAN_VERSION = 1;
|
|
20
|
+
export const planSchema = versioned(PLAN_VERSION, {
|
|
21
|
+
goal: limited("goal", "goal").min(1),
|
|
22
|
+
outOfScope: limited("outOfScope", "goal").nullable().optional(),
|
|
23
|
+
touches: z.array(limited("touch", "planTouch").min(1)).max(PLAN_TOUCHES).optional(),
|
|
24
|
+
acceptance: z.array(acceptanceCriterionSchema).min(1).max(ACCEPTANCE_LIMITS.criteria),
|
|
25
|
+
plan: limited("plan", "planDocumentBytes").min(1),
|
|
26
|
+
amendment: limited("amendment", "planAmendment").nullable().optional(),
|
|
27
|
+
});
|
|
28
|
+
/** What the planner's `--json-schema` is: the plan schema, exactly. */
|
|
29
|
+
export const PLAN_MODEL_SCHEMA = toModelSchema(planSchema);
|
|
30
|
+
const PLAN_FIELDS = ["goal", "outOfScope", "touches", "acceptance", "plan", "amendment"];
|
|
31
|
+
const CRITERION_FIELDS = ["id", "statement", "how", "evidence"];
|
|
32
|
+
function pick(body, fields) {
|
|
33
|
+
const out = {};
|
|
34
|
+
for (const field of fields) {
|
|
35
|
+
if (Object.prototype.hasOwnProperty.call(body, field) && body[field] !== undefined)
|
|
36
|
+
out[field] = body[field];
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
41
|
+
/**
|
|
42
|
+
* A plan written before plans carried `version` (every plan through 0.9.33), as version 1. The old parser read only
|
|
43
|
+
* the fields it knew and ignored any others, read a null goal, plan or acceptance as missing and null touches as
|
|
44
|
+
* none; this keeps exactly that, so every plan that parsed then parses now. Values themselves are never rewritten.
|
|
45
|
+
*/
|
|
46
|
+
export function upgradeUnversionedPlan(body) {
|
|
47
|
+
const out = { version: PLAN_VERSION, ...pick(body, PLAN_FIELDS) };
|
|
48
|
+
for (const field of ["goal", "plan", "touches", "acceptance"]) {
|
|
49
|
+
if (out[field] === null)
|
|
50
|
+
delete out[field];
|
|
51
|
+
}
|
|
52
|
+
if (Array.isArray(out["acceptance"])) {
|
|
53
|
+
out["acceptance"] = out["acceptance"].map(criterion => (isRecord(criterion) ? pick(criterion, CRITERION_FIELDS) : criterion));
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
export const PLAN_UPGRADES = { 0: upgradeUnversionedPlan };
|
|
58
|
+
/** The payload as version 1, upgraded when it was saved unversioned; null for anything that is not an object. */
|
|
59
|
+
export function planPayloadBody(input) {
|
|
60
|
+
if (!isRecord(input))
|
|
61
|
+
return null;
|
|
62
|
+
return Object.prototype.hasOwnProperty.call(input, "version") ? input : upgradeUnversionedPlan(input);
|
|
63
|
+
}
|
|
64
|
+
/** Read a plan payload: version 1 as itself, an unversioned one upgraded, a newer one refused plainly. */
|
|
65
|
+
export function readPlanPayload(input) {
|
|
66
|
+
return readVersioned(planSchema, input, PLAN_UPGRADES);
|
|
67
|
+
}
|
package/dist/desktop-update.d.ts
CHANGED
|
@@ -125,13 +125,13 @@ export declare function prepareDesktopUpdate(stateDir: string, installed: string
|
|
|
125
125
|
export declare function sqliteLock(file: string, busyTimeoutMs?: number): DatabaseSync | null;
|
|
126
126
|
export declare const installationLock: (j: UpdateJournal) => DatabaseSync | null;
|
|
127
127
|
export declare function desktopUpdateStatus(stateDir: string): {
|
|
128
|
+
error?: never;
|
|
128
129
|
active: boolean;
|
|
129
130
|
phase: string;
|
|
130
131
|
detail: string;
|
|
131
132
|
running: boolean;
|
|
132
133
|
canResume: boolean;
|
|
133
134
|
canCancel: boolean;
|
|
134
|
-
error?: never;
|
|
135
135
|
automaticRecovery?: never;
|
|
136
136
|
recoveryAttempts?: never;
|
|
137
137
|
recoveryRepairs?: never;
|
package/dist/evidence.js
CHANGED
|
@@ -18,7 +18,7 @@ import { closeSync, existsSync, constants, fstatSync, mkdirSync, openSync, readd
|
|
|
18
18
|
import { join, sep } from "node:path";
|
|
19
19
|
import { namedPath } from "./names.js";
|
|
20
20
|
import { LIMITS } from "./decision.js";
|
|
21
|
-
import {
|
|
21
|
+
import { TEXT_LIMITS } from "./text-limits.js";
|
|
22
22
|
import { parseReport, REPORT_LIMITS } from "./scout-report.js";
|
|
23
23
|
import { parseProof, PROOF_LIMITS } from "./proof.js";
|
|
24
24
|
import { encodeBaseTreeSnapshot, parseBaseTreeSnapshot } from "./peek.js";
|
|
@@ -84,7 +84,7 @@ export const EVIDENCE_CAPS = {
|
|
|
84
84
|
diff: 256 * 1024,
|
|
85
85
|
status: 64 * 1024,
|
|
86
86
|
"park-payload": LIMITS.payload,
|
|
87
|
-
plan:
|
|
87
|
+
plan: TEXT_LIMITS.planDocumentBytes,
|
|
88
88
|
"terminal-diff": 256 * 1024,
|
|
89
89
|
"diff-stat": 32 * 1024,
|
|
90
90
|
// The enveloped base snapshot (live peek): 20k entries of path+sha+size.
|
package/dist/guides.js
CHANGED
|
@@ -419,6 +419,9 @@ and crew agent reads from it before working:
|
|
|
419
419
|
- Instructions and references change on the Knowledge page under Settings,
|
|
420
420
|
or through the lead's knowledge cards. Every change keeps a revision you
|
|
421
421
|
can restore.
|
|
422
|
+
- If a project was replaced at the same path, its saved knowledge waits:
|
|
423
|
+
tasks run without it until an approver applies it on the Knowledge page
|
|
424
|
+
or with \`toolroll knowledge apply --repo PATH --as <you> --token <t>\`.
|
|
422
425
|
- Agent conclusions are not instructions: a crew agent reports; a person or
|
|
423
426
|
the lead's confirmed card records.
|
|
424
427
|
|
package/dist/knowledge-cli.d.ts
CHANGED
|
@@ -89,7 +89,38 @@ export declare const KNOWLEDGE_DESCRIPTORS: readonly [{
|
|
|
89
89
|
readonly takesValue: true;
|
|
90
90
|
readonly meaning: 'environment variable containing a scoped coordinator token';
|
|
91
91
|
}];
|
|
92
|
+
}, {
|
|
93
|
+
readonly action: 'apply';
|
|
94
|
+
readonly synopsis: "carry this project's saved knowledge from before a change forward as a new revision; an approver's --as/--token";
|
|
95
|
+
readonly mutation: 'identity-idempotent';
|
|
96
|
+
readonly takesQuery: false;
|
|
97
|
+
readonly flags: readonly [{
|
|
98
|
+
readonly name: 'repo';
|
|
99
|
+
readonly takesValue: true;
|
|
100
|
+
readonly meaning: 'admitted project checkout root';
|
|
101
|
+
}, {
|
|
102
|
+
readonly name: 'as';
|
|
103
|
+
readonly takesValue: true;
|
|
104
|
+
readonly meaning: 'approver account name';
|
|
105
|
+
}, {
|
|
106
|
+
readonly name: 'token';
|
|
107
|
+
readonly takesValue: true;
|
|
108
|
+
readonly meaning: "the approver's password (never a lead token)";
|
|
109
|
+
}, {
|
|
110
|
+
readonly name: 'json';
|
|
111
|
+
readonly takesValue: false;
|
|
112
|
+
readonly meaning: 'versioned structured response';
|
|
113
|
+
}, {
|
|
114
|
+
readonly name: 'db';
|
|
115
|
+
readonly takesValue: true;
|
|
116
|
+
readonly meaning: 'local Toolroll database';
|
|
117
|
+
}];
|
|
118
|
+
readonly audience: 'operator';
|
|
92
119
|
}];
|
|
120
|
+
/** `knowledge apply`: the caller has verified `actor` as an approver of `repo` (never the lead). */
|
|
121
|
+
export declare function runKnowledgeApply(options: Map<string, string | true>, context: KnowledgeCliContext & {
|
|
122
|
+
actor: string | null;
|
|
123
|
+
}): number;
|
|
93
124
|
export type KnowledgeCliContext = {
|
|
94
125
|
store: Store;
|
|
95
126
|
write: (line: string) => void;
|
package/dist/knowledge-cli.js
CHANGED
|
@@ -6,6 +6,7 @@ import { authenticateCoordinator } from './coordinator.js';
|
|
|
6
6
|
import { envelopeJson } from './envelope.js';
|
|
7
7
|
import { repositoryContext } from './repository-context.js';
|
|
8
8
|
import { namedPath } from './names.js';
|
|
9
|
+
import { applySavedKnowledge } from './project-knowledge.js';
|
|
9
10
|
const flags = [
|
|
10
11
|
{ name: 'repo', takesValue: true, meaning: 'admitted project checkout root' },
|
|
11
12
|
{ name: 'base', takesValue: true, meaning: 'exact committed task base; defaults to current HEAD' },
|
|
@@ -14,11 +15,43 @@ const flags = [
|
|
|
14
15
|
{ name: 'token-file', takesValue: true, meaning: 'explicit scoped coordinator credential file' },
|
|
15
16
|
{ name: 'token-env', takesValue: true, meaning: 'environment variable containing a scoped coordinator token' },
|
|
16
17
|
];
|
|
18
|
+
const applyFlags = [
|
|
19
|
+
{ name: 'repo', takesValue: true, meaning: 'admitted project checkout root' },
|
|
20
|
+
{ name: 'as', takesValue: true, meaning: 'approver account name' },
|
|
21
|
+
{ name: 'token', takesValue: true, meaning: "the approver's password (never a lead token)" },
|
|
22
|
+
{ name: 'json', takesValue: false, meaning: 'versioned structured response' },
|
|
23
|
+
{ name: 'db', takesValue: true, meaning: 'local Toolroll database' },
|
|
24
|
+
];
|
|
17
25
|
export const KNOWLEDGE_DESCRIPTORS = [
|
|
18
26
|
{ action: 'search', synopsis: 'find bounded source excerpts and relevant code symbols', mutation: 'none', takesQuery: true, flags },
|
|
19
27
|
{ action: 'impact', synopsis: 'inspect static imports affected by one file or unambiguous symbol', mutation: 'none', takesQuery: true, flags },
|
|
20
28
|
{ action: 'refresh', synopsis: 'refresh the optional local code index; never starts a model or task', mutation: 'identity-idempotent', takesQuery: false, flags },
|
|
29
|
+
// A person's action: the approver carries saved knowledge from before a project change forward.
|
|
30
|
+
{ action: 'apply', synopsis: "carry this project's saved knowledge from before a change forward as a new revision; an approver's --as/--token", mutation: 'identity-idempotent', takesQuery: false, flags: applyFlags, audience: 'operator' },
|
|
21
31
|
];
|
|
32
|
+
/** `knowledge apply`: the caller has verified `actor` as an approver of `repo` (never the lead). */
|
|
33
|
+
export function runKnowledgeApply(options, context) {
|
|
34
|
+
const command = 'knowledge apply';
|
|
35
|
+
const fail = (reason, message) => { context.write(context.json ? envelopeJson({ ok: false, command, reason, message }) : message); return reason === 'usage' ? 2 : 3; };
|
|
36
|
+
for (const key of options.keys())
|
|
37
|
+
if (!applyFlags.some(flag => flag.name === key))
|
|
38
|
+
return fail('usage', `--${key} is not a knowledge apply option.`);
|
|
39
|
+
const repo = options.get('repo');
|
|
40
|
+
if (typeof repo !== 'string' || !repo.trim() || repo.length > 4096 || /[\x00-\x1f]/.test(repo))
|
|
41
|
+
return fail('usage', 'Use --repo with the project checkout root.');
|
|
42
|
+
if (context.actor === null)
|
|
43
|
+
return fail('unauthenticated', 'An approver of this project applies saved knowledge: --as <you> --token <password>.');
|
|
44
|
+
let revision;
|
|
45
|
+
try {
|
|
46
|
+
revision = applySavedKnowledge(context.store, resolve(repo), context.actor, context.now);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
return fail('failed', error instanceof Error ? error.message : 'Saved knowledge could not be applied.');
|
|
50
|
+
}
|
|
51
|
+
const message = revision === null ? 'Nothing to apply.' : `Applied saved knowledge as version ${revision}.`;
|
|
52
|
+
context.write(context.json ? envelopeJson({ ok: true, command, result: { revision, message } }) : message);
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
22
55
|
function lines(result) {
|
|
23
56
|
return [
|
|
24
57
|
`Repository context: ${result.index.status}${result.index.engine === 'text-search' ? ' · source search' : ' · code relationships'}`,
|
|
@@ -32,8 +65,8 @@ export async function runKnowledgeCommand(positional, options, context) {
|
|
|
32
65
|
const spec = KNOWLEDGE_DESCRIPTORS.find(s => s.action === positional[0]);
|
|
33
66
|
const command = spec ? `knowledge ${spec.action}` : 'knowledge';
|
|
34
67
|
const fail = (reason, message) => { context.write(context.json ? envelopeJson({ ok: false, command, reason, message }) : message); return reason === 'usage' ? 2 : 3; };
|
|
35
|
-
if (!spec)
|
|
36
|
-
return fail('usage', 'Use knowledge search <query>, impact <file-or-symbol>, or
|
|
68
|
+
if (!spec || spec.action === 'apply')
|
|
69
|
+
return fail('usage', 'Use knowledge search <query>, impact <file-or-symbol>, refresh or apply with --repo PATH.');
|
|
37
70
|
if (positional.length !== (spec.takesQuery ? 2 : 1))
|
|
38
71
|
return fail('usage', spec.takesQuery ? 'Provide one quoted query or source path.' : 'knowledge refresh takes no query.');
|
|
39
72
|
for (const key of options.keys())
|
package/dist/knowledge-ui.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type KnowledgeDraft, type KnowledgeSelection, type KnowledgeView } from './project-knowledge.js';
|
|
2
2
|
export declare const KNOWLEDGE_CSS = ".knowledge{max-width:780px;min-width:0;overflow-wrap:anywhere}.knowledge .card{padding:20px;min-width:0;margin:16px 0}.knowledge h2{margin:0 0 12px}.knowledge form{display:grid;gap:12px;margin:0}.knowledge label{display:grid;gap:6px}.knowledge textarea,.knowledge input,.knowledge select{box-sizing:border-box;width:100%;min-width:0;max-width:100%}.knowledge textarea{resize:vertical;min-height:110px}.knowledge button,.knowledge summary,.knowledge .button-link{min-height:44px}.knowledge button{justify-self:start;white-space:nowrap}.knowledge summary{padding:12px 0;cursor:pointer}.knowledge pre{white-space:pre-wrap;overflow-wrap:anywhere;font:inherit}.knowledge .knowledge-actions{display:flex;gap:12px;flex-wrap:wrap}.knowledge .meta{font-size:.86rem}.knowledge .knowledge-source{display:block}.knowledge details+details{border-top:1px solid var(--so-line)}.memory-stats{display:flex;flex-wrap:wrap;gap:.4rem}.memory-stats span{padding:.15rem .55rem;border-radius:.375rem;background:var(--so-neutral-soft);color:var(--so-neutral-ink)}@media(max-width:600px){.knowledge .card{padding:16px}.knowledge h2{font-size:1.1rem}}";
|
|
3
3
|
export declare function knowledgeHtml(view: KnowledgeView, csrf: string, canManage: boolean, draft?: KnowledgeDraft, error?: string): string;
|
|
4
4
|
export declare function knowledgeContextHtml(snapshot: KnowledgeSelection | null): string;
|
package/dist/knowledge-ui.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { STALE_KNOWLEDGE } from './project-knowledge.js';
|
|
1
2
|
const e = (s) => String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
2
3
|
const fields = (data) => Object.entries(data).map(([k, v]) => `<input type="hidden" name="${e(k)}" value="${e(v)}">`).join('');
|
|
3
4
|
export const KNOWLEDGE_CSS = `.knowledge{max-width:780px;min-width:0;overflow-wrap:anywhere}.knowledge .card{padding:20px;min-width:0;margin:16px 0}.knowledge h2{margin:0 0 12px}.knowledge form{display:grid;gap:12px;margin:0}.knowledge label{display:grid;gap:6px}.knowledge textarea,.knowledge input,.knowledge select{box-sizing:border-box;width:100%;min-width:0;max-width:100%}.knowledge textarea{resize:vertical;min-height:110px}.knowledge button,.knowledge summary,.knowledge .button-link{min-height:44px}.knowledge button{justify-self:start;white-space:nowrap}.knowledge summary{padding:12px 0;cursor:pointer}.knowledge pre{white-space:pre-wrap;overflow-wrap:anywhere;font:inherit}.knowledge .knowledge-actions{display:flex;gap:12px;flex-wrap:wrap}.knowledge .meta{font-size:.86rem}.knowledge .knowledge-source{display:block}.knowledge details+details{border-top:1px solid var(--so-line)}.memory-stats{display:flex;flex-wrap:wrap;gap:.4rem}.memory-stats span{padding:.15rem .55rem;border-radius:.375rem;background:var(--so-neutral-soft);color:var(--so-neutral-ink)}@media(max-width:600px){.knowledge .card{padding:16px}.knowledge h2{font-size:1.1rem}}`;
|
|
@@ -8,6 +9,12 @@ export function knowledgeHtml(view, csrf, canManage, draft, error) {
|
|
|
8
9
|
const referenceFields = (d) => `<label>Name<input name="title" maxlength="120" value="${e(d.title)}" required></label><label>Reference text<textarea name="content" rows="5" maxlength="12000">${e(d.content)}</textarea></label><details${d.path ? ' open' : ''}><summary>Use a project document</summary><label>Committed .md or .txt path<input name="path" value="${e(d.path)}" placeholder="docs/design-guide.md"></label><p class="meta">Uses the committed file instead of the text above. Save again to refresh it.</p></details><button type="submit">Save reference</button>`;
|
|
9
10
|
const refs = view.knowledge.references.map(ref => `<details class="card"${draft?.id === ref.id ? ' open' : ''}><summary><strong>${e(ref.title)}</strong></summary>${ref.path ? `<span class="meta knowledge-source">${e(ref.path)} · ${e(ref.sourceRevision?.slice(0, 7))}</span>` : ''}<pre>${e(ref.content)}</pre>${manage ? `<details${draft?.id === ref.id ? ' open' : ''}><summary>Edit reference</summary>${form('save', referenceFields(draft?.id === ref.id ? draft : { title: ref.title, content: ref.path ? '' : ref.content, path: ref.path ?? '' }), { id: ref.id })}<details><summary>Remove reference</summary>${form('remove', '<p>Remove from future tasks. Previous run context stays available.</p><button>Remove reference</button>', { id: ref.id })}</details></details>` : ''}</details>`).join('');
|
|
10
11
|
const history = view.history.map(h => `<li><a class="button-link" href="/settings/knowledge?repo=${encodeURIComponent(view.repo)}&version=${h.revision}">Version ${h.revision}</a> · ${e(h.actor)} · ${e(h.at.slice(0, 16).replace('T', ' '))} UTC</li>`).join('');
|
|
12
|
+
if (view.stale) {
|
|
13
|
+
// One notice, the saved knowledge on demand, one approver action. Editing waits until it is applied.
|
|
14
|
+
const saved = view.stale.knowledge;
|
|
15
|
+
const review = `<details><summary>Review saved knowledge</summary><pre class="knowledge-instructions">${e(saved.instructions || 'No project instructions.')}</pre>${saved.references.map(ref => `<details><summary>${e(ref.title)}</summary><pre>${e(ref.content)}</pre></details>`).join('')}</details>`;
|
|
16
|
+
return `<section class="knowledge knowledge-library">${error ? `<p class="problem" role="alert">${e(error)}</p>` : ''}<section class="card knowledge-stale"><p role="status">${e(STALE_KNOWLEDGE)}.</p>${review}${manage ? form('apply', '<button>Apply saved knowledge</button>') : ''}</section></section>`;
|
|
17
|
+
}
|
|
11
18
|
const instructions = `<pre class="knowledge-instructions">${e(view.knowledge.instructions || 'No project instructions yet.')}</pre>`;
|
|
12
19
|
const editor = manage ? `<details class="knowledge-editor"${draft?.instructions !== undefined ? ' open' : ''}><summary>${view.knowledge.instructions ? 'Edit instructions' : 'Add instructions'}</summary>${form('instructions', `<textarea aria-label="Project instructions" name="instructions" rows="5" maxlength="4000" placeholder="Goals, preferences, and what good work looks like…">${e(draft?.instructions ?? view.knowledge.instructions)}</textarea><button>Save instructions</button>`)}</details>` : '';
|
|
13
20
|
return `<section class="knowledge knowledge-library">${error ? `<p class="problem" role="alert">${e(error)}</p>` : ''}<section class="knowledge-intro"><h2>Instructions</h2>${instructions}${editor}</section><section><h2>References</h2>${refs || '<p class="meta">Add a spec, design guide, or useful project document.</p>'}${manage ? `<details class="card"${draft?.title && !draft.id ? ' open' : ''}><summary>Add reference</summary>${form('save', referenceFields(draft?.id ? {} : draft ?? {}))}</details>` : ''}</section>${history ? `<details><summary>Change history</summary><ol>${history}</ol></details>` : ''}</section>`;
|