toolroll 0.9.36 → 0.9.37
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 +58 -0
- package/dist/assignment-adapters.d.ts +12 -48
- package/dist/assignment-adapters.js +33 -43
- package/dist/build-review.d.ts +1 -1
- package/dist/build-review.js +2 -1
- package/dist/chat-acceptance.d.ts +1 -1
- package/dist/chat-actions.d.ts +18 -12
- package/dist/chat-actions.js +15 -65
- package/dist/chat-ask.js +5 -5
- package/dist/chat-decide.d.ts +14 -1
- package/dist/chat-decide.js +12 -6
- package/dist/chat-delivery-state.d.ts +12 -70
- package/dist/chat-delivery-state.js +17 -135
- package/dist/chat-delivery.js +58 -30
- package/dist/chat-flow.js +8 -6
- package/dist/chat-inbox.js +1 -1
- package/dist/coding-context.d.ts +2 -39
- package/dist/coding-context.js +2 -1
- package/dist/coding-handoff.d.ts +6 -25
- package/dist/coding-handoff.js +11 -5
- package/dist/coding-types.d.ts +2 -26
- package/dist/coding-workspace.d.ts +2 -0
- package/dist/coding-workspace.js +50 -14
- package/dist/contracts/acceptance-terms.d.ts +14 -0
- package/dist/contracts/acceptance-terms.js +16 -0
- package/dist/contracts/chat-actions.d.ts +328 -0
- package/dist/contracts/chat-actions.js +133 -0
- package/dist/contracts/chat-callback-rows.d.ts +59 -0
- package/dist/contracts/chat-callback-rows.js +46 -0
- package/dist/contracts/chat-content.d.ts +276 -0
- package/dist/contracts/chat-content.js +127 -0
- package/dist/contracts/chat-tables.d.ts +6 -0
- package/dist/contracts/chat-tables.js +136 -0
- package/dist/contracts/coding-context.d.ts +97 -0
- package/dist/contracts/coding-context.js +60 -0
- package/dist/contracts/coding-handoff.d.ts +44 -0
- package/dist/contracts/coding-handoff.js +47 -0
- package/dist/contracts/coding-workspace.d.ts +165 -0
- package/dist/contracts/coding-workspace.js +71 -0
- package/dist/contracts/contract.d.ts +7 -2
- package/dist/contracts/contract.js +21 -12
- package/dist/contracts/decision.d.ts +45 -0
- package/dist/contracts/decision.js +45 -0
- package/dist/contracts/discord-callback.d.ts +45 -0
- package/dist/contracts/discord-callback.js +34 -0
- package/dist/contracts/flow-card.d.ts +50 -0
- package/dist/contracts/flow-card.js +46 -0
- package/dist/contracts/flow-send.d.ts +119 -0
- package/dist/contracts/flow-send.js +112 -0
- package/dist/contracts/flow.d.ts +5 -0
- package/dist/contracts/flow.js +5 -0
- package/dist/contracts/gateway-tools.d.ts +374 -0
- package/dist/contracts/gateway-tools.js +94 -0
- package/dist/contracts/lead-context.d.ts +165 -0
- package/dist/contracts/lead-context.js +73 -0
- package/dist/contracts/lead-tools.d.ts +1905 -0
- package/dist/contracts/lead-tools.js +265 -0
- package/dist/contracts/memory-pass.d.ts +158 -0
- package/dist/contracts/memory-pass.js +102 -0
- package/dist/contracts/project-knowledge.d.ts +126 -0
- package/dist/contracts/project-knowledge.js +93 -0
- package/dist/contracts/project-memory.d.ts +117 -0
- package/dist/contracts/project-memory.js +51 -0
- package/dist/contracts/project-skills.d.ts +121 -0
- package/dist/contracts/project-skills.js +76 -0
- package/dist/contracts/review-findings.d.ts +46 -0
- package/dist/contracts/review-findings.js +48 -0
- package/dist/contracts/route.d.ts +484 -0
- package/dist/contracts/route.js +128 -0
- package/dist/contracts/scope.d.ts +105 -0
- package/dist/contracts/scope.js +114 -0
- package/dist/contracts/scout-report.d.ts +234 -0
- package/dist/contracts/scout-report.js +141 -0
- package/dist/contracts/slack-callback.d.ts +43 -0
- package/dist/contracts/slack-callback.js +35 -0
- package/dist/contracts/sort-answer.d.ts +83 -0
- package/dist/contracts/sort-answer.js +77 -0
- package/dist/contracts/stage-output.d.ts +127 -0
- package/dist/contracts/stage-output.js +166 -0
- package/dist/contracts/task-sizing.d.ts +24 -0
- package/dist/contracts/task-sizing.js +28 -0
- package/dist/contracts/teammate-turn.d.ts +43 -0
- package/dist/contracts/teammate-turn.js +51 -0
- package/dist/contracts/teams-callback.d.ts +14 -0
- package/dist/contracts/teams-callback.js +14 -0
- package/dist/contracts/telegram-callback.d.ts +120 -0
- package/dist/contracts/telegram-callback.js +97 -0
- package/dist/decision.d.ts +11 -20
- package/dist/decision.js +86 -167
- package/dist/desktop-update.d.ts +2 -2
- package/dist/discord-chat.js +31 -20
- package/dist/flow-engine.js +14 -17
- package/dist/flow-items.d.ts +4 -13
- package/dist/flow-items.js +1 -15
- package/dist/flow-send.d.ts +6 -37
- package/dist/flow-send.js +18 -14
- package/dist/flow-share.js +7 -1
- package/dist/flow-sort.d.ts +7 -26
- package/dist/flow-sort.js +25 -20
- package/dist/flow-steps.js +3 -2
- package/dist/flows.d.ts +2 -4
- package/dist/flows.js +9 -5
- package/dist/invoke.js +4 -4
- package/dist/lead-context.d.ts +4 -2
- package/dist/lead-context.js +24 -17
- package/dist/mate-tools.d.ts +4 -0
- package/dist/mate-tools.js +108 -275
- package/dist/mcp-connect.d.ts +24 -4
- package/dist/mcp-connect.js +78 -15
- package/dist/mcp.js +84 -216
- package/dist/memory-pass.d.ts +17 -42
- package/dist/memory-pass.js +54 -26
- package/dist/phase-routing.d.ts +19 -72
- package/dist/phase-routing.js +34 -164
- package/dist/project-knowledge.d.ts +16 -35
- package/dist/project-knowledge.js +42 -21
- package/dist/project-memory.d.ts +2 -28
- package/dist/project-memory.js +28 -13
- package/dist/project-skills.d.ts +16 -38
- package/dist/project-skills.js +41 -32
- package/dist/project-tools.d.ts +1 -0
- package/dist/project-tools.js +4 -4
- package/dist/provider.d.ts +2 -0
- package/dist/provider.js +4 -16
- package/dist/result-shots.d.ts +1 -1
- package/dist/result-shots.js +2 -1
- package/dist/review-switch.d.ts +3 -8
- package/dist/reviewer.d.ts +8 -12
- package/dist/reviewer.js +11 -35
- package/dist/scope.d.ts +21 -43
- package/dist/scope.js +42 -128
- package/dist/scout-report.d.ts +9 -194
- package/dist/scout-report.js +113 -264
- package/dist/scout.js +15 -11
- package/dist/serve.js +9 -2
- package/dist/setup-guide.d.ts +2 -2
- package/dist/slack-chat.js +26 -20
- package/dist/slack-state.js +2 -1
- package/dist/store.d.ts +9 -32
- package/dist/store.js +18 -15
- package/dist/structured-output.js +2 -1
- package/dist/subscription-chat.d.ts +4 -0
- package/dist/subscription-chat.js +4 -1
- package/dist/task-review-brief.d.ts +1 -7
- package/dist/task-review-brief.js +6 -6
- package/dist/task-sizing.d.ts +3 -27
- package/dist/task-sizing.js +11 -26
- package/dist/task-text.d.ts +0 -5
- package/dist/task-text.js +1 -3
- package/dist/teammate-question.js +7 -5
- package/dist/teammate-work.js +9 -5
- package/dist/teammates.d.ts +13 -64
- package/dist/teammates.js +32 -33
- package/dist/teams-chat.js +20 -11
- package/dist/telegram-flow.js +4 -3
- package/dist/telegram-mate.d.ts +4 -14
- package/dist/telegram-mate.js +8 -7
- package/dist/telegram-team.d.ts +2 -19
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +52 -42
- package/dist/text-limits.d.ts +142 -1
- package/dist/text-limits.js +144 -1
- package/dist/tools-ui.d.ts +2 -1
- package/dist/tools-ui.js +7 -4
- package/package.json +1 -1
|
@@ -5,6 +5,9 @@ import { currentActor } from './actor.js';
|
|
|
5
5
|
import { decisionLines, getDecision, listDecisions } from './project-memory.js';
|
|
6
6
|
import { scanForSecrets } from './evidence.js';
|
|
7
7
|
import { repositoryContextRead } from './repository-context.js';
|
|
8
|
+
import { TEXT_LIMITS } from './text-limits.js';
|
|
9
|
+
import { contractError } from './contracts/contract.js';
|
|
10
|
+
import { KNOWLEDGE_COUNTS, KNOWLEDGE_SELECTION_VERSION, KNOWLEDGE_VERSION, knowledgeSchema, knowledgeSelectionSchema, readKnowledge, readKnowledgeSelection } from './contracts/project-knowledge.js';
|
|
8
11
|
export const KNOWLEDGE_SCHEMA = `
|
|
9
12
|
CREATE TABLE IF NOT EXISTS project_knowledge (
|
|
10
13
|
repo TEXT PRIMARY KEY, identity TEXT NOT NULL, revision INTEGER NOT NULL, payload TEXT NOT NULL, sha TEXT NOT NULL
|
|
@@ -49,10 +52,21 @@ export const STALE_KNOWLEDGE = "This project's saved knowledge is from before a
|
|
|
49
52
|
const STALE_OMITTED = { title: 'Saved project knowledge', reason: 'Saved knowledge is from before a change; it was not applied' };
|
|
50
53
|
/** Revisions under any identity this project accepts, as a bound JSON list. */
|
|
51
54
|
const IN_ACCEPTED = 'identity IN (SELECT value FROM json_each(?))';
|
|
55
|
+
/** The stored bytes are checked against their digest first; only then are they parsed, and upgraded in memory. */
|
|
52
56
|
function decode(payload, sha) {
|
|
53
57
|
if (learningSha(payload) !== sha)
|
|
54
58
|
throw Error('Project knowledge could not be verified.');
|
|
55
|
-
|
|
59
|
+
const read = readKnowledge(JSON.parse(payload));
|
|
60
|
+
if (!read.ok)
|
|
61
|
+
throw Error('Project knowledge could not be verified.');
|
|
62
|
+
return read.value;
|
|
63
|
+
}
|
|
64
|
+
/** Knowledge (or a selection) as it is written: through its schema, so a refusal names the field. */
|
|
65
|
+
function encode(schema, value, what) {
|
|
66
|
+
const read = schema.safeParse(value, { reportInput: true });
|
|
67
|
+
if (!read.success)
|
|
68
|
+
throw Error(`${what} could not be saved: ${contractError(read.error).join('; ')}`);
|
|
69
|
+
return JSON.stringify(read.data);
|
|
56
70
|
}
|
|
57
71
|
/** Saved knowledge under another identity is stale: it is returned for review, never applied. */
|
|
58
72
|
function current(store, repo, accepted) {
|
|
@@ -73,9 +87,9 @@ function document(repo, revision, path) {
|
|
|
73
87
|
if (!match)
|
|
74
88
|
throw Error('This reference must be a committed text file, not a folder or link.');
|
|
75
89
|
const sha = match[1];
|
|
76
|
-
if (Number(git(repo, ['cat-file', '-s', sha])) >
|
|
90
|
+
if (Number(git(repo, ['cat-file', '-s', sha])) > TEXT_LIMITS.knowledgeReferenceBytes)
|
|
77
91
|
throw Error('Choose a shorter reference (up to 12 KB).');
|
|
78
|
-
return { content: clean(git(repo, ['cat-file', 'blob', sha]),
|
|
92
|
+
return { content: clean(git(repo, ['cat-file', 'blob', sha]), TEXT_LIMITS.knowledgeReferenceBytes), sha };
|
|
79
93
|
}
|
|
80
94
|
export function knowledgeView(store, repo, actor) {
|
|
81
95
|
const { identity, accepted } = admission(store, repo, actor), { stale, ...value } = current(store, repo, accepted);
|
|
@@ -128,7 +142,7 @@ export function changeKnowledge(store, args, now = new Date()) {
|
|
|
128
142
|
throw Error('Knowledge changed in another window. Review your draft below before saving again.');
|
|
129
143
|
let knowledge = existing.knowledge;
|
|
130
144
|
if (args.action === 'instructions')
|
|
131
|
-
knowledge.instructions = clean(args.draft.instructions ?? '',
|
|
145
|
+
knowledge.instructions = clean(args.draft.instructions ?? '', TEXT_LIMITS.knowledgeInstructionsBytes);
|
|
132
146
|
else if (args.action === 'restore') {
|
|
133
147
|
const row = store.handle.prepare(`SELECT payload,sha FROM knowledge_change WHERE repo=? AND ${IN_ACCEPTED} AND revision=?`).get(args.repo, JSON.stringify([...accepted]), args.restore ?? -1);
|
|
134
148
|
if (!row)
|
|
@@ -141,26 +155,26 @@ export function changeKnowledge(store, args, now = new Date()) {
|
|
|
141
155
|
knowledge.references = knowledge.references.filter(r => r.id !== args.draft.id);
|
|
142
156
|
}
|
|
143
157
|
else if (args.action === 'save') {
|
|
144
|
-
const title = clean(args.draft.title ?? '',
|
|
158
|
+
const title = clean(args.draft.title ?? '', TEXT_LIMITS.knowledgeTitleBytes);
|
|
145
159
|
if (!title)
|
|
146
160
|
throw Error('Give this reference a short name.');
|
|
147
161
|
const id = args.draft.id || learningSha(`${identity}:${existing.revision + 1}:${title}`).slice(0, 20);
|
|
148
162
|
if (args.draft.id && !knowledge.references.some(r => r.id === id))
|
|
149
163
|
throw Error('That reference is no longer available.');
|
|
150
|
-
const path = clean(args.draft.path ?? '',
|
|
164
|
+
const path = clean(args.draft.path ?? '', TEXT_LIMITS.knowledgePathBytes) || null;
|
|
151
165
|
const sourceRevision = path ? git(args.repo, ['rev-parse', 'HEAD']) : null;
|
|
152
166
|
const source = path ? document(args.repo, sourceRevision, path) : null;
|
|
153
|
-
const content = source?.content ?? clean(args.draft.content ?? '',
|
|
167
|
+
const content = source?.content ?? clean(args.draft.content ?? '', TEXT_LIMITS.knowledgeReferenceBytes);
|
|
154
168
|
if (!content)
|
|
155
169
|
throw Error('Add reference text or select a committed project document.');
|
|
156
170
|
const ref = { id, title, content, path, sourceSha: source?.sha ?? null, sourceRevision };
|
|
157
171
|
knowledge.references = [...knowledge.references.filter(r => r.id !== id), ref];
|
|
158
|
-
if (knowledge.references.length >
|
|
172
|
+
if (knowledge.references.length > KNOWLEDGE_COUNTS.references)
|
|
159
173
|
throw Error('Keep up to 12 focused references. Remove one before adding another.');
|
|
160
174
|
}
|
|
161
175
|
else
|
|
162
176
|
throw Error('Choose a supported knowledge action.');
|
|
163
|
-
const payload =
|
|
177
|
+
const payload = encode(knowledgeSchema, { version: KNOWLEDGE_VERSION, ...knowledge }, 'Project knowledge'), sha = learningSha(payload), revision = existing.revision + 1;
|
|
164
178
|
store.handle.prepare('INSERT INTO knowledge_change(repo,identity,revision,actor,at,payload,sha) VALUES (?,?,?,?,?,?,?)').run(args.repo, identity, revision, args.actor, now.toISOString(), payload, sha);
|
|
165
179
|
store.handle.prepare('INSERT INTO project_knowledge VALUES (?,?,?,?,?) ON CONFLICT(repo) DO UPDATE SET identity=excluded.identity,revision=excluded.revision,payload=excluded.payload,sha=excluded.sha').run(args.repo, identity, revision, payload, sha);
|
|
166
180
|
});
|
|
@@ -191,7 +205,7 @@ function select(store, repo, accepted, query, head) {
|
|
|
191
205
|
const ranked = knowledge.references.map(ref => ({ ref, score: [...tokens(`${ref.title} ${ref.path ?? ''}`)].reduce((n, t) => n + (q.has(t) ? 3 : 0), 0) + [...tokens(ref.content)].reduce((n, t) => n + (q.has(t) ? 1 : 0), 0) })).sort((a, b) => b.score - a.score || a.ref.id.localeCompare(b.ref.id));
|
|
192
206
|
const references = [];
|
|
193
207
|
for (const { ref, score } of ranked) {
|
|
194
|
-
let reason = score === 0 ? 'Not relevant to this task' : references.length >=
|
|
208
|
+
let reason = score === 0 ? 'Not relevant to this task' : references.length >= KNOWLEDGE_COUNTS.selected ? 'More relevant references selected' : '';
|
|
195
209
|
if (ref.path) {
|
|
196
210
|
try {
|
|
197
211
|
if (document(repo, head, ref.path).sha !== ref.sourceSha)
|
|
@@ -201,7 +215,7 @@ function select(store, repo, accepted, query, head) {
|
|
|
201
215
|
reason = 'Source unavailable; refresh this reference';
|
|
202
216
|
}
|
|
203
217
|
}
|
|
204
|
-
if (!reason && Buffer.byteLength(JSON.stringify({ instructions: knowledge.instructions, references: [...references, ref] })) >
|
|
218
|
+
if (!reason && Buffer.byteLength(JSON.stringify({ instructions: knowledge.instructions, references: [...references, ref] })) > TEXT_LIMITS.knowledgeContextBytes)
|
|
205
219
|
reason = 'Context size limit';
|
|
206
220
|
if (reason)
|
|
207
221
|
omitted.push({ title: ref.title, reason });
|
|
@@ -210,8 +224,8 @@ function select(store, repo, accepted, query, head) {
|
|
|
210
224
|
}
|
|
211
225
|
// Settled decisions ride the same frozen record, one line each; the why
|
|
212
226
|
// loads on demand by id, so nothing is repeated between brief and store.
|
|
213
|
-
const decisions = decisionLines(store, repo, query, { limit:
|
|
214
|
-
return { version:
|
|
227
|
+
const decisions = decisionLines(store, repo, query, { limit: KNOWLEDGE_COUNTS.decisions, bytes: Math.max(0, Math.min(TEXT_LIMITS.knowledgeDecisionsBytes, TEXT_LIMITS.knowledgeContextBytes - Buffer.byteLength(JSON.stringify({ instructions: knowledge.instructions, references })) - 200)) });
|
|
228
|
+
return { version: KNOWLEDGE_SELECTION_VERSION, revision, instructions: knowledge.instructions, references, omitted, inheritedFrom: null, ...(decisions.length ? { decisions } : {}) };
|
|
215
229
|
}
|
|
216
230
|
/** Read the same bounded, source-checked selection without inventing a worker run. */
|
|
217
231
|
export function selectProjectKnowledge(store, args) {
|
|
@@ -227,10 +241,17 @@ export function readKnowledgeSnapshot(store, runId) {
|
|
|
227
241
|
if (!row)
|
|
228
242
|
return null;
|
|
229
243
|
const run = store.getRun(runId), repo = run && store.refForId(run.taskRef)?.repo;
|
|
230
|
-
const
|
|
244
|
+
const unverified = () => Error('The context saved for this run could not be verified.');
|
|
245
|
+
// The stored bytes are checked against their digest before they are parsed.
|
|
246
|
+
if (!repo || row['repo'] !== repo || learningSha(String(row['payload'])) !== row['sha'])
|
|
247
|
+
throw unverified();
|
|
248
|
+
const read = readKnowledgeSelection(JSON.parse(String(row['payload'])));
|
|
249
|
+
if (!read.ok)
|
|
250
|
+
throw unverified();
|
|
251
|
+
const selection = read.value;
|
|
231
252
|
const empty = row['identity'] === 'unconfigured' && unconfigured(selection);
|
|
232
|
-
if (!
|
|
233
|
-
throw
|
|
253
|
+
if (!empty && !acceptedIdentities(store, repo).has(String(row['identity'])))
|
|
254
|
+
throw unverified();
|
|
234
255
|
return selection;
|
|
235
256
|
}
|
|
236
257
|
/** Freeze at provider admission. Reviewers see the builder's exact project context. */
|
|
@@ -259,18 +280,18 @@ export function knowledgeContext(store, runId, cacheRoot, validatedBuilderBase)
|
|
|
259
280
|
const configured = !!store.handle.prepare('SELECT 1 FROM project_knowledge WHERE repo=?').get(repo);
|
|
260
281
|
// A legacy source without a snapshot did not receive this feature's
|
|
261
282
|
// context. Never give its reviewer newly configured project guidance.
|
|
262
|
-
selection = inherited ? { ...inherited, inheritedFrom: parent.id } : !configured || parent ? { version:
|
|
283
|
+
selection = inherited ? { ...inherited, inheritedFrom: parent.id } : !configured || parent ? { version: KNOWLEDGE_SELECTION_VERSION, revision: 0, instructions: '', references: [], omitted: [], inheritedFrom: parent?.id ?? null } : select(store, repo, acceptedIdentities(store, repo), `${store.getTask(ref.externalId)?.title ?? ''} ${scope?.goal ?? ''} ${(scope?.touches ?? []).join(' ')}`, contextBase || git(repo, ['rev-parse', 'HEAD']));
|
|
263
284
|
// Optional source selection is captured once from this crew's actual
|
|
264
285
|
// checkout. It is reused with the immutable run snapshot on resume.
|
|
265
286
|
// Index/source failure stays context metadata, never an admission gate.
|
|
266
287
|
if (!parent && run.worktree && contextBase) {
|
|
267
|
-
const available =
|
|
268
|
-
if (available >=
|
|
269
|
-
selection.repository = repositoryContextRead({ repo: run.worktree, project: repo, baseRevision: contextBase, audience: 'crew', maxBytes: Math.min(
|
|
288
|
+
const available = TEXT_LIMITS.knowledgeContextBytes - Buffer.byteLength(JSON.stringify(selection)) - 30;
|
|
289
|
+
if (available >= TEXT_LIMITS.knowledgeRepositoryMinBytes)
|
|
290
|
+
selection.repository = repositoryContextRead({ repo: run.worktree, project: repo, baseRevision: contextBase, audience: 'crew', maxBytes: Math.min(TEXT_LIMITS.knowledgeRepositoryBytes, available), ...(cacheRoot === undefined ? {} : { cacheRoot }), query: `${store.getTask(ref.externalId)?.title ?? ''} ${scope?.goal ?? ''} ${(scope?.touches ?? []).join(' ')}` });
|
|
270
291
|
else
|
|
271
292
|
selection.omitted.push({ title: 'Repository context', reason: 'Context size limit' });
|
|
272
293
|
}
|
|
273
|
-
const payload =
|
|
294
|
+
const payload = encode(knowledgeSelectionSchema, selection, 'Project context');
|
|
274
295
|
store.handle.prepare('INSERT INTO knowledge_snapshot VALUES (?,?,?,?,?)').run(runId, repo, unconfigured(selection) ? 'unconfigured' : learningIdentity(repo), payload, learningSha(payload));
|
|
275
296
|
}
|
|
276
297
|
// JSON escapes ordinary newlines, but not Unicode line separators. Keep
|
package/dist/project-memory.d.ts
CHANGED
|
@@ -1,28 +1,7 @@
|
|
|
1
1
|
import type { Store } from './store.js';
|
|
2
|
+
import { type Decision, type DecisionDraft, type DecisionLine } from './contracts/project-memory.js';
|
|
3
|
+
export type { Decision, DecisionDraft, DecisionLine };
|
|
2
4
|
export declare const MEMORY_SCHEMA = "\nCREATE TABLE IF NOT EXISTS project_decision (\n id INTEGER PRIMARY KEY, repo TEXT NOT NULL, identity TEXT NOT NULL, revision INTEGER NOT NULL DEFAULT 1,\n claim TEXT NOT NULL, why TEXT NOT NULL,\n status TEXT NOT NULL CHECK(status IN ('active','superseded','retired')),\n supersedes INTEGER REFERENCES project_decision(id),\n decided_by TEXT NOT NULL, decided_at TEXT NOT NULL,\n source_kind TEXT NOT NULL CHECK(source_kind IN ('conversation','task','result','manual','backward-pass')),\n source_ref TEXT, recorded_by TEXT NOT NULL, sha TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS project_decision_repo ON project_decision(repo, status, id);\nCREATE TABLE IF NOT EXISTS decision_change (\n id INTEGER PRIMARY KEY, decision INTEGER NOT NULL REFERENCES project_decision(id), revision INTEGER NOT NULL,\n actor TEXT NOT NULL, at TEXT NOT NULL, action TEXT NOT NULL, payload TEXT NOT NULL, sha TEXT NOT NULL,\n UNIQUE(decision, revision)\n);\nCREATE TRIGGER IF NOT EXISTS decision_change_no_update BEFORE UPDATE ON decision_change BEGIN SELECT RAISE(ABORT,'Decision history is immutable'); END;\nCREATE TRIGGER IF NOT EXISTS decision_change_no_delete BEFORE DELETE ON decision_change BEGIN SELECT RAISE(ABORT,'Decision history is immutable'); END;\nCREATE TRIGGER IF NOT EXISTS project_decision_no_delete BEFORE DELETE ON project_decision BEGIN SELECT RAISE(ABORT,'Decisions are retired, never deleted'); END;\nCREATE VIRTUAL TABLE IF NOT EXISTS memory_search USING fts5(kind UNINDEXED, ref UNINDEXED, repo UNINDEXED, scope UNINDEXED, title, body, tokenize='unicode61');\n-- The backward pass (memory-pass.ts): sessions analysed once per surface\n-- version, gaps that accumulate corroboration across runs, and the\n-- proposals a person accepts or rejects. Rejections are remembered.\nCREATE TABLE IF NOT EXISTS memory_session (\n id TEXT PRIMARY KEY, repo TEXT NOT NULL, kind TEXT NOT NULL CHECK(kind IN ('claude','codex','lead','crew')),\n source TEXT NOT NULL, seen_at TEXT NOT NULL, surface TEXT NOT NULL, trace_sha TEXT NOT NULL,\n analyzed_at TEXT, verdict TEXT, problem TEXT\n);\nCREATE INDEX IF NOT EXISTS memory_session_repo ON memory_session(repo, seen_at);\nCREATE TABLE IF NOT EXISTS memory_gap (\n id INTEGER PRIMARY KEY, repo TEXT NOT NULL, key TEXT NOT NULL, mistake TEXT NOT NULL, proposed TEXT NOT NULL,\n domain TEXT NOT NULL CHECK(domain IN ('project','orchestration')), first_seen TEXT NOT NULL, last_seen TEXT NOT NULL,\n retired_at TEXT, retired_reason TEXT, UNIQUE(repo, key)\n);\nCREATE TABLE IF NOT EXISTS memory_sighting (\n gap INTEGER NOT NULL REFERENCES memory_gap(id), session TEXT NOT NULL REFERENCES memory_session(id),\n at TEXT NOT NULL, quote TEXT NOT NULL, PRIMARY KEY(gap, session)\n);\nCREATE TABLE IF NOT EXISTS memory_proposal (\n id INTEGER PRIMARY KEY, repo TEXT NOT NULL, kind TEXT NOT NULL CHECK(kind IN ('instruction-add','instruction-remove','decision-add')),\n fingerprint TEXT NOT NULL, title TEXT NOT NULL, rationale TEXT NOT NULL, before_text TEXT, after_text TEXT NOT NULL,\n evidence TEXT NOT NULL, sessions INTEGER NOT NULL, status TEXT NOT NULL CHECK(status IN ('pending','accepted','rejected','stale')),\n created_at TEXT NOT NULL, decided_by TEXT, decided_at TEXT, surface TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS memory_proposal_pending ON memory_proposal(repo, status, fingerprint);\nCREATE TABLE IF NOT EXISTS memory_rejection (\n repo TEXT NOT NULL, fingerprint TEXT NOT NULL, rejected_at TEXT NOT NULL, rejected_by TEXT NOT NULL, sessions INTEGER NOT NULL, PRIMARY KEY(repo, fingerprint)\n);\n";
|
|
3
|
-
export type Decision = {
|
|
4
|
-
id: number;
|
|
5
|
-
repo: string;
|
|
6
|
-
revision: number;
|
|
7
|
-
claim: string;
|
|
8
|
-
why: string;
|
|
9
|
-
status: 'active' | 'superseded' | 'retired';
|
|
10
|
-
supersedes: number | null;
|
|
11
|
-
decidedBy: string;
|
|
12
|
-
decidedAt: string;
|
|
13
|
-
sourceKind: 'conversation' | 'task' | 'result' | 'manual' | 'backward-pass';
|
|
14
|
-
sourceRef: string | null;
|
|
15
|
-
recordedBy: string;
|
|
16
|
-
};
|
|
17
|
-
export type DecisionDraft = {
|
|
18
|
-
claim: string;
|
|
19
|
-
why: string;
|
|
20
|
-
decidedBy?: string;
|
|
21
|
-
decidedAt?: string;
|
|
22
|
-
sourceKind?: Decision['sourceKind'];
|
|
23
|
-
sourceRef?: string | null;
|
|
24
|
-
supersedes?: number | null;
|
|
25
|
-
};
|
|
26
5
|
export declare function listDecisions(store: Store, repo: string, actor: string, options?: {
|
|
27
6
|
status?: Decision['status'] | 'all';
|
|
28
7
|
limit?: number;
|
|
@@ -47,11 +26,6 @@ export declare function decisionHistory(store: Store, repo: string, actor: strin
|
|
|
47
26
|
at: string;
|
|
48
27
|
action: string;
|
|
49
28
|
}[];
|
|
50
|
-
export type DecisionLine = {
|
|
51
|
-
id: number;
|
|
52
|
-
claim: string;
|
|
53
|
-
decidedAt: string;
|
|
54
|
-
};
|
|
55
29
|
/**
|
|
56
30
|
* The decisions worth one line each in a brief: relevant to the query first,
|
|
57
31
|
* then newest, within a byte budget. Bodies (the why) load on demand by id,
|
package/dist/project-memory.js
CHANGED
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
import { createHash } from 'node:crypto';
|
|
11
11
|
import { scanForSecrets } from './evidence.js';
|
|
12
12
|
import { learningIdentity } from './project-learning.js';
|
|
13
|
+
import { TEXT_LIMITS } from './text-limits.js';
|
|
14
|
+
import { contractError } from './contracts/contract.js';
|
|
15
|
+
import { DECISION_CHANGE_VERSION, decisionChangeSchema, decisionRecordSchema, decisionSchema } from './contracts/project-memory.js';
|
|
13
16
|
export const MEMORY_SCHEMA = `
|
|
14
17
|
CREATE TABLE IF NOT EXISTS project_decision (
|
|
15
18
|
id INTEGER PRIMARY KEY, repo TEXT NOT NULL, identity TEXT NOT NULL, revision INTEGER NOT NULL DEFAULT 1,
|
|
@@ -75,18 +78,29 @@ function admission(store, repo, actor, write = false) {
|
|
|
75
78
|
throw Error('Project memory is outside your access.');
|
|
76
79
|
return learningIdentity(repo);
|
|
77
80
|
}
|
|
81
|
+
/** A decision row through its schema; null when the row does not read as one (it is then unverifiable). */
|
|
78
82
|
function decisionOf(row) {
|
|
79
|
-
|
|
83
|
+
const read = decisionSchema.safeParse({ id: Number(row['id']), repo: String(row['repo']), revision: Number(row['revision']), claim: String(row['claim']), why: String(row['why']),
|
|
80
84
|
status: row['status'], supersedes: row['supersedes'] === null ? null : Number(row['supersedes']),
|
|
81
85
|
decidedBy: String(row['decided_by']), decidedAt: String(row['decided_at']), sourceKind: row['source_kind'],
|
|
82
|
-
sourceRef: row['source_ref'] === null ? null : String(row['source_ref']), recordedBy: String(row['recorded_by']) };
|
|
86
|
+
sourceRef: row['source_ref'] === null ? null : String(row['source_ref']), recordedBy: String(row['recorded_by']) });
|
|
87
|
+
return read.success ? read.data : null;
|
|
83
88
|
}
|
|
84
89
|
const digestOf = (d) => sha(JSON.stringify([d.repo, d.revision, d.claim, d.why, d.status, d.supersedes, d.decidedBy, d.decidedAt, d.sourceKind, d.sourceRef, d.recordedBy]));
|
|
90
|
+
/** A record or history entry through its schema before it is written; a refusal names the field. */
|
|
91
|
+
function checked(schema, value) {
|
|
92
|
+
const read = schema.safeParse(value);
|
|
93
|
+
if (!read.success)
|
|
94
|
+
throw Error(`This decision could not be saved: ${contractError(read.error).join('; ')}`);
|
|
95
|
+
return read.data;
|
|
96
|
+
}
|
|
97
|
+
const history = (payload) => JSON.stringify(checked(decisionChangeSchema, { version: DECISION_CHANGE_VERSION, ...payload }));
|
|
85
98
|
export function listDecisions(store, repo, actor, options = {}) {
|
|
86
99
|
admission(store, repo, actor);
|
|
87
100
|
const status = options.status ?? 'active', limit = Math.min(200, Math.max(1, options.limit ?? 50));
|
|
88
101
|
return store.handle.prepare(`SELECT * FROM project_decision WHERE repo=? AND (?='all' OR status=?) ORDER BY id DESC LIMIT ?`).all(repo, status, status, limit)
|
|
89
|
-
.
|
|
102
|
+
.flatMap(row => { const d = decisionOf(row); return d === null ? [] : [d]; })
|
|
103
|
+
.filter(d => digestOf(d) === String(store.handle.prepare('SELECT sha FROM project_decision WHERE id=?').get(d.id)['sha']));
|
|
90
104
|
}
|
|
91
105
|
export function getDecision(store, repo, actor, id) {
|
|
92
106
|
admission(store, repo, actor);
|
|
@@ -94,20 +108,20 @@ export function getDecision(store, repo, actor, id) {
|
|
|
94
108
|
if (!row)
|
|
95
109
|
return null;
|
|
96
110
|
const d = decisionOf(row);
|
|
97
|
-
if (digestOf(d) !== String(row['sha']))
|
|
111
|
+
if (d === null || digestOf(d) !== String(row['sha']))
|
|
98
112
|
throw Error('This decision could not be verified.');
|
|
99
113
|
return d;
|
|
100
114
|
}
|
|
101
115
|
/** Record one settled choice. A superseding decision retires the older one in the same transaction. */
|
|
102
116
|
export function recordDecision(store, args, now = new Date()) {
|
|
103
117
|
const identity = admission(store, args.repo, args.actor, true);
|
|
104
|
-
const claim = clean(args.draft.claim,
|
|
105
|
-
const decidedBy = args.draft.decidedBy === undefined ? args.actor : clean(args.draft.decidedBy,
|
|
118
|
+
const claim = clean(args.draft.claim, TEXT_LIMITS.decisionClaimBytes, 'The decision'), why = clean(args.draft.why, TEXT_LIMITS.decisionWhyBytes, 'The reason');
|
|
119
|
+
const decidedBy = args.draft.decidedBy === undefined ? args.actor : clean(args.draft.decidedBy, TEXT_LIMITS.decisionByBytes, 'Who decided');
|
|
106
120
|
const decidedAt = args.draft.decidedAt ?? now.toISOString();
|
|
107
121
|
if (!/^\d{4}-\d{2}-\d{2}T/.test(decidedAt))
|
|
108
122
|
throw Error('When it was decided must be an ISO timestamp.');
|
|
109
123
|
const sourceKind = args.draft.sourceKind ?? 'manual';
|
|
110
|
-
const sourceRef = args.draft.sourceRef === undefined || args.draft.sourceRef === null ? null : clean(String(args.draft.sourceRef),
|
|
124
|
+
const sourceRef = args.draft.sourceRef === undefined || args.draft.sourceRef === null ? null : clean(String(args.draft.sourceRef), TEXT_LIMITS.decisionSourceBytes, 'The source');
|
|
111
125
|
return store.transact(() => {
|
|
112
126
|
let supersedes = null;
|
|
113
127
|
if (args.draft.supersedes !== undefined && args.draft.supersedes !== null) {
|
|
@@ -116,11 +130,11 @@ export function recordDecision(store, args, now = new Date()) {
|
|
|
116
130
|
throw Error('The decision being replaced is not active.');
|
|
117
131
|
supersedes = older.id;
|
|
118
132
|
}
|
|
119
|
-
const record = { repo: args.repo, revision: 1, claim, why, status: 'active', supersedes, decidedBy, decidedAt, sourceKind, sourceRef, recordedBy: args.actor };
|
|
133
|
+
const record = checked(decisionRecordSchema, { repo: args.repo, revision: 1, claim, why, status: 'active', supersedes, decidedBy, decidedAt, sourceKind, sourceRef, recordedBy: args.actor });
|
|
120
134
|
const inserted = store.handle.prepare(`INSERT INTO project_decision(repo,identity,revision,claim,why,status,supersedes,decided_by,decided_at,source_kind,source_ref,recorded_by,sha)
|
|
121
135
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(args.repo, identity, 1, claim, why, 'active', supersedes, decidedBy, decidedAt, sourceKind, sourceRef, args.actor, digestOf(record));
|
|
122
136
|
const id = Number(inserted.lastInsertRowid);
|
|
123
|
-
store.handle.prepare('INSERT INTO decision_change(decision,revision,actor,at,action,payload,sha) VALUES (?,?,?,?,?,?,?)').run(id, 1, args.actor, now.toISOString(), 'record',
|
|
137
|
+
store.handle.prepare('INSERT INTO decision_change(decision,revision,actor,at,action,payload,sha) VALUES (?,?,?,?,?,?,?)').run(id, 1, args.actor, now.toISOString(), 'record', history(record), digestOf(record));
|
|
124
138
|
if (supersedes !== null)
|
|
125
139
|
changeStatus(store, args.repo, args.actor, supersedes, 'superseded', `Replaced by decision ${id}.`, now);
|
|
126
140
|
reindexProjectMemory(store, args.repo);
|
|
@@ -133,15 +147,16 @@ function changeStatus(store, repo, actor, id, status, reason, now) {
|
|
|
133
147
|
throw Error('That decision is unavailable.');
|
|
134
148
|
if (existing.status !== 'active')
|
|
135
149
|
throw Error('That decision is no longer active.');
|
|
136
|
-
const
|
|
150
|
+
const { id: _id, ...was } = existing;
|
|
151
|
+
const next = { ...was, revision: existing.revision + 1, status };
|
|
137
152
|
store.handle.prepare('UPDATE project_decision SET revision=?,status=?,sha=? WHERE id=?').run(next.revision, status, digestOf(next), id);
|
|
138
|
-
store.handle.prepare('INSERT INTO decision_change(decision,revision,actor,at,action,payload,sha) VALUES (?,?,?,?,?,?,?)').run(id, next.revision, actor, now.toISOString(), status,
|
|
153
|
+
store.handle.prepare('INSERT INTO decision_change(decision,revision,actor,at,action,payload,sha) VALUES (?,?,?,?,?,?,?)').run(id, next.revision, actor, now.toISOString(), status, history({ reason }), digestOf(next));
|
|
139
154
|
return { id, ...next };
|
|
140
155
|
}
|
|
141
156
|
/** Retire a decision that no longer holds; history keeps it. */
|
|
142
157
|
export function retireDecision(store, args, now = new Date()) {
|
|
143
158
|
admission(store, args.repo, args.actor, true);
|
|
144
|
-
const reason = clean(args.reason,
|
|
159
|
+
const reason = clean(args.reason, TEXT_LIMITS.decisionRetireBytes, 'The reason');
|
|
145
160
|
return store.transact(() => { const d = changeStatus(store, args.repo, args.actor, args.id, 'retired', reason, now); reindexProjectMemory(store, args.repo); return d; });
|
|
146
161
|
}
|
|
147
162
|
export function decisionHistory(store, repo, actor, id) {
|
|
@@ -166,7 +181,7 @@ export function decisionLines(store, repo, query, options = {}) {
|
|
|
166
181
|
return { line: { id: Number(row['id']), claim, decidedAt: String(row['decided_at']) }, score };
|
|
167
182
|
})
|
|
168
183
|
.sort((a, b) => b.score - a.score || b.line.id - a.line.id);
|
|
169
|
-
const limit = options.limit ?? 8, bytes = options.bytes ??
|
|
184
|
+
const limit = options.limit ?? 8, bytes = options.bytes ?? TEXT_LIMITS.knowledgeDecisionsBytes, out = [];
|
|
170
185
|
for (const { line } of ranked) {
|
|
171
186
|
if (out.length >= limit)
|
|
172
187
|
break;
|
package/dist/project-skills.d.ts
CHANGED
|
@@ -1,48 +1,27 @@
|
|
|
1
1
|
import type { Store } from "./store.js";
|
|
2
|
+
import { SKILL_LIMITS, type SavedSkill, type SkillFile, type SkillPackage, type SkillSelection, type SkillsSnapshot } from "./contracts/project-skills.js";
|
|
3
|
+
export { SKILL_LIMITS };
|
|
4
|
+
export type { SavedSkill, SkillFile, SkillPackage, SkillsSnapshot };
|
|
2
5
|
export declare const SKILLS_SCHEMA: string;
|
|
3
|
-
|
|
4
|
-
path: string;
|
|
5
|
-
base64: string;
|
|
6
|
-
};
|
|
7
|
-
export type SkillPackage = {
|
|
8
|
-
name: string;
|
|
9
|
-
description: string;
|
|
10
|
-
source: string;
|
|
11
|
-
requirements: string;
|
|
12
|
-
warnings: string[];
|
|
13
|
-
files: SkillFile[];
|
|
14
|
-
};
|
|
15
|
-
export type SavedSkill = SkillPackage & {
|
|
16
|
-
sha: string;
|
|
17
|
-
};
|
|
18
|
-
type Choice = {
|
|
19
|
-
sha: string;
|
|
20
|
-
enabled: boolean;
|
|
21
|
-
};
|
|
22
|
-
type Selection = Record<string, Choice>;
|
|
23
|
-
export type SkillsSnapshot = {
|
|
24
|
-
version: 1;
|
|
25
|
-
revision: number;
|
|
26
|
-
identity: string;
|
|
27
|
-
inheritedFrom: number | null;
|
|
28
|
-
test: boolean;
|
|
29
|
-
packages: SavedSkill[];
|
|
30
|
-
};
|
|
31
|
-
export declare const SKILL_LIMITS: {
|
|
32
|
-
files: number;
|
|
33
|
-
packageBytes: number;
|
|
34
|
-
fileBytes: number;
|
|
35
|
-
bodyBytes: number;
|
|
36
|
-
enabled: number;
|
|
37
|
-
selectionBytes: number;
|
|
38
|
-
};
|
|
6
|
+
type Selection = SkillSelection;
|
|
39
7
|
export declare function validateSkill(files: SkillFile[], source: string): SkillPackage;
|
|
40
8
|
export declare function skillsView(store: Store, repo: string, actor: string): {
|
|
41
9
|
revision: number;
|
|
42
10
|
selection: Selection;
|
|
43
11
|
repo: string;
|
|
44
12
|
identity: string;
|
|
45
|
-
library:
|
|
13
|
+
library: {
|
|
14
|
+
name: string;
|
|
15
|
+
description: string;
|
|
16
|
+
source: string;
|
|
17
|
+
requirements: string;
|
|
18
|
+
warnings: string[];
|
|
19
|
+
files: {
|
|
20
|
+
path: string;
|
|
21
|
+
base64: string;
|
|
22
|
+
}[];
|
|
23
|
+
sha: string;
|
|
24
|
+
}[];
|
|
46
25
|
history: {
|
|
47
26
|
revision: number;
|
|
48
27
|
actor: string;
|
|
@@ -159,4 +138,3 @@ export declare function githubSkill(url: string, fetcher?: typeof fetch): Promis
|
|
|
159
138
|
files: SkillFile[];
|
|
160
139
|
source: string;
|
|
161
140
|
}>;
|
|
162
|
-
export {};
|
package/dist/project-skills.js
CHANGED
|
@@ -6,6 +6,10 @@ import { parseDocument } from "yaml";
|
|
|
6
6
|
import { acceptedIdentities, learningIdentity, learningSha } from "./project-learning.js";
|
|
7
7
|
import { scanForSecrets } from "./evidence.js";
|
|
8
8
|
import { fileTaskProposal } from "./proposal.js";
|
|
9
|
+
import { TEXT_LIMITS } from "./text-limits.js";
|
|
10
|
+
import { contractError } from "./contracts/contract.js";
|
|
11
|
+
import { SKILL_COUNTS, SKILL_LIMITS, readSkillsSnapshotPayload, savedSkillPackageSchema, savedSkillsSnapshotSchema, skillPackageSchema, skillSelectionSchema, } from "./contracts/project-skills.js";
|
|
12
|
+
export { SKILL_LIMITS };
|
|
9
13
|
export const SKILLS_SCHEMA = `
|
|
10
14
|
CREATE TABLE IF NOT EXISTS skill_package (sha TEXT PRIMARY KEY, payload TEXT NOT NULL);
|
|
11
15
|
CREATE TABLE IF NOT EXISTS skill_owner (sha TEXT NOT NULL REFERENCES skill_package(sha), actor TEXT NOT NULL, at TEXT NOT NULL, PRIMARY KEY(sha,actor));
|
|
@@ -14,14 +18,6 @@ CREATE TABLE IF NOT EXISTS skill_snapshot (run INTEGER PRIMARY KEY REFERENCES ru
|
|
|
14
18
|
CREATE TABLE IF NOT EXISTS skill_test (task_ref INTEGER PRIMARY KEY REFERENCES task_ref(id), package TEXT NOT NULL REFERENCES skill_package(sha), actor TEXT NOT NULL, request_sha TEXT NOT NULL, sample TEXT NOT NULL, source_run INTEGER REFERENCES run(id), feedback TEXT, identity TEXT NOT NULL);
|
|
15
19
|
${["skill_package", "skill_owner", "project_skill_change", "skill_snapshot", "skill_test"].map((t) => `CREATE TRIGGER IF NOT EXISTS ${t}_no_update BEFORE UPDATE ON ${t} BEGIN SELECT RAISE(ABORT,'Skill history is immutable'); END; CREATE TRIGGER IF NOT EXISTS ${t}_no_delete BEFORE DELETE ON ${t} BEGIN SELECT RAISE(ABORT,'Skill history is immutable'); END;`).join("\n")}
|
|
16
20
|
`;
|
|
17
|
-
export const SKILL_LIMITS = {
|
|
18
|
-
files: 64,
|
|
19
|
-
packageBytes: 1024 * 1024,
|
|
20
|
-
fileBytes: 256 * 1024,
|
|
21
|
-
bodyBytes: 24 * 1024,
|
|
22
|
-
enabled: 8,
|
|
23
|
-
selectionBytes: 2 * 1024 * 1024,
|
|
24
|
-
};
|
|
25
21
|
const controls = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f\u200b-\u200f\u202a-\u202e\u2066-\u2069\ufffd]/u;
|
|
26
22
|
const text = (value, max, label) => {
|
|
27
23
|
if (typeof value !== "string" ||
|
|
@@ -44,7 +40,7 @@ export function validateSkill(files, source) {
|
|
|
44
40
|
.map((file) => {
|
|
45
41
|
if (!file ||
|
|
46
42
|
typeof file.path !== "string" ||
|
|
47
|
-
file.path.length >
|
|
43
|
+
file.path.length > TEXT_LIMITS.skillPath ||
|
|
48
44
|
!/^[a-zA-Z0-9_. -]+(?:\/[a-zA-Z0-9_. -]+)*$/.test(file.path) ||
|
|
49
45
|
file.path
|
|
50
46
|
.split("/")
|
|
@@ -91,13 +87,13 @@ export function validateSkill(files, source) {
|
|
|
91
87
|
}
|
|
92
88
|
if (!front || typeof front !== "object" || Array.isArray(front))
|
|
93
89
|
throw Error("Skill metadata must contain a name and description.");
|
|
94
|
-
const name = text(front["name"],
|
|
90
|
+
const name = text(front["name"], TEXT_LIMITS.skillNameBytes, "Skill name");
|
|
95
91
|
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name))
|
|
96
92
|
throw Error("Use a lowercase skill name with hyphens between words.");
|
|
97
|
-
const description = text(front["description"],
|
|
93
|
+
const description = text(front["description"], TEXT_LIMITS.skillDescriptionBytes, "Description");
|
|
98
94
|
const requirements = front["compatibility"] === undefined
|
|
99
95
|
? ""
|
|
100
|
-
: text(front["compatibility"],
|
|
96
|
+
: text(front["compatibility"], TEXT_LIMITS.skillRequirementsBytes, "Requirements");
|
|
101
97
|
const extensions = Object.keys(front).filter((k) => !["name", "description", "license", "compatibility", "metadata"].includes(k));
|
|
102
98
|
const warnings = [];
|
|
103
99
|
if (extensions.length || safe.some((f) => f.path === "agents/openai.yaml"))
|
|
@@ -106,14 +102,21 @@ export function validateSkill(files, source) {
|
|
|
106
102
|
warnings.push("Dynamic shell substitutions are not evaluated by the skill manager.");
|
|
107
103
|
if (safe.some((f) => f.path.startsWith("scripts/")))
|
|
108
104
|
warnings.push("Includes scripts. Agents may run them only within their task permissions.");
|
|
109
|
-
return {
|
|
105
|
+
return written(skillPackageSchema, {
|
|
110
106
|
name,
|
|
111
107
|
description,
|
|
112
|
-
source: text(source,
|
|
108
|
+
source: text(source, TEXT_LIMITS.skillSourceBytes, "Source"),
|
|
113
109
|
requirements,
|
|
114
110
|
warnings,
|
|
115
111
|
files: safe,
|
|
116
|
-
};
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
/** A payload through its schema before it is written; a refusal names the field. */
|
|
115
|
+
function written(schema, value) {
|
|
116
|
+
const read = schema.safeParse(value, { reportInput: true });
|
|
117
|
+
if (!read.success)
|
|
118
|
+
throw Error(`This skill could not be saved: ${contractError(read.error).join("; ")}`);
|
|
119
|
+
return read.data;
|
|
117
120
|
}
|
|
118
121
|
function admit(store, repo, actor, write = false) {
|
|
119
122
|
if (!store.schemaCurrent() ||
|
|
@@ -125,18 +128,24 @@ function admit(store, repo, actor, write = false) {
|
|
|
125
128
|
/** Revisions under any identity this project accepts, as a bound JSON list. */
|
|
126
129
|
const IN_ACCEPTED = "identity IN (SELECT value FROM json_each(?))";
|
|
127
130
|
const listOf = (accepted) => JSON.stringify([...accepted]);
|
|
128
|
-
|
|
131
|
+
/** Saved bytes are checked against their digest first; only then are they parsed through their schema. */
|
|
132
|
+
function unpack(payload, sha, read) {
|
|
129
133
|
if (typeof payload !== "string" || learningSha(payload) !== sha)
|
|
130
134
|
throw Error("Saved skills could not be verified.");
|
|
131
|
-
|
|
135
|
+
const value = read(JSON.parse(payload));
|
|
136
|
+
if (value === null)
|
|
137
|
+
throw Error("Saved skills could not be verified.");
|
|
138
|
+
return value;
|
|
132
139
|
}
|
|
140
|
+
const through = (schema) => (input) => { const read = schema.safeParse(input); return read.success ? read.data : null; };
|
|
141
|
+
const unpackSelection = (payload, sha) => unpack(payload, sha, through(skillSelectionSchema));
|
|
133
142
|
function packageOf(store, sha) {
|
|
134
143
|
const row = store.handle
|
|
135
144
|
.prepare("SELECT payload FROM skill_package WHERE sha=?")
|
|
136
145
|
.get(sha);
|
|
137
146
|
if (!row)
|
|
138
147
|
throw Error("That skill version is unavailable.");
|
|
139
|
-
return { ...unpack(row["payload"], sha), sha };
|
|
148
|
+
return { ...unpack(row["payload"], sha, through(savedSkillPackageSchema)), sha };
|
|
140
149
|
}
|
|
141
150
|
function current(store, repo, accepted) {
|
|
142
151
|
const row = store.handle
|
|
@@ -148,7 +157,7 @@ function current(store, repo, accepted) {
|
|
|
148
157
|
throw Error("The project changed. Its previous skills have not been applied.");
|
|
149
158
|
return {
|
|
150
159
|
revision: Number(row["revision"]),
|
|
151
|
-
selection:
|
|
160
|
+
selection: unpackSelection(row["payload"], row["sha"]),
|
|
152
161
|
};
|
|
153
162
|
}
|
|
154
163
|
export function skillsView(store, repo, actor) {
|
|
@@ -173,7 +182,7 @@ export function skillsView(store, repo, actor) {
|
|
|
173
182
|
}
|
|
174
183
|
}
|
|
175
184
|
if (row)
|
|
176
|
-
for (const c of Object.values(
|
|
185
|
+
for (const c of Object.values(unpackSelection(row["payload"], row["sha"])))
|
|
177
186
|
visible.add(c.sha);
|
|
178
187
|
}
|
|
179
188
|
for (const c of Object.values(value.selection))
|
|
@@ -192,7 +201,7 @@ export function skillsView(store, repo, actor) {
|
|
|
192
201
|
revision: Number(r["revision"]),
|
|
193
202
|
actor: String(r["actor"]),
|
|
194
203
|
at: String(r["at"]),
|
|
195
|
-
enabled: Object.entries(
|
|
204
|
+
enabled: Object.entries(unpackSelection(r["payload"], r["sha"]))
|
|
196
205
|
.filter(([, c]) => c.enabled)
|
|
197
206
|
.map(([name, c]) => ({ name, version: c.sha.slice(0, 10) })),
|
|
198
207
|
}));
|
|
@@ -215,7 +224,7 @@ export function skillsVersion(store, repo, actor, revision) {
|
|
|
215
224
|
const row = store.handle.prepare(`SELECT payload,sha FROM project_skill_change WHERE repo=? AND ${IN_ACCEPTED} AND revision=?`).get(repo, listOf(accepted), revision);
|
|
216
225
|
if (!row)
|
|
217
226
|
throw Error("That skills version is unavailable.");
|
|
218
|
-
const selection =
|
|
227
|
+
const selection = unpackSelection(row["payload"], row["sha"]);
|
|
219
228
|
for (const choice of Object.values(selection))
|
|
220
229
|
packageOf(store, choice.sha);
|
|
221
230
|
return selection;
|
|
@@ -229,7 +238,7 @@ export function importSkill(store, repo, actor, files, source, now = new Date())
|
|
|
229
238
|
.get(sha, actor) &&
|
|
230
239
|
Number(store.handle
|
|
231
240
|
.prepare("SELECT COUNT(*) AS n FROM skill_owner WHERE actor=?")
|
|
232
|
-
.get(actor)?.["n"]) >=
|
|
241
|
+
.get(actor)?.["n"]) >= SKILL_COUNTS.library)
|
|
233
242
|
throw Error("Your library has 100 saved versions. Reuse an existing version.");
|
|
234
243
|
store.handle
|
|
235
244
|
.prepare("INSERT OR IGNORE INTO skill_package VALUES (?,?)")
|
|
@@ -255,7 +264,7 @@ export function changeSkills(store, args, now = new Date()) {
|
|
|
255
264
|
.get(args.repo, listOf(accepted), args.restore ?? -1);
|
|
256
265
|
if (!row)
|
|
257
266
|
throw Error("That version is unavailable.");
|
|
258
|
-
selection =
|
|
267
|
+
selection = unpackSelection(row["payload"], row["sha"]);
|
|
259
268
|
}
|
|
260
269
|
else {
|
|
261
270
|
const skill = view.library.find((p) => p.sha === args.sha);
|
|
@@ -274,7 +283,7 @@ export function changeSkills(store, args, now = new Date()) {
|
|
|
274
283
|
if (enabled.length > SKILL_LIMITS.enabled ||
|
|
275
284
|
Buffer.byteLength(JSON.stringify(enabled)) > SKILL_LIMITS.selectionBytes)
|
|
276
285
|
throw Error("Enable up to 8 skills, totaling at most 2 MB.");
|
|
277
|
-
const payload = JSON.stringify(selection);
|
|
286
|
+
const payload = JSON.stringify(written(skillSelectionSchema, selection));
|
|
278
287
|
store.handle
|
|
279
288
|
.prepare("INSERT INTO project_skill_change VALUES (?,?,?,?,?,?,?)")
|
|
280
289
|
.run(args.repo, identity, view.revision + 1, args.actor, now.toISOString(), payload, learningSha(payload));
|
|
@@ -316,7 +325,7 @@ export function readSkillsSnapshot(store, runId) {
|
|
|
316
325
|
const run = store.getRun(runId);
|
|
317
326
|
if (!run || store.refForId(run.taskRef)?.repo !== row["repo"])
|
|
318
327
|
throw Error("Saved skills belong to another project.");
|
|
319
|
-
const saved = unpack(row["payload"], row["sha"]);
|
|
328
|
+
const saved = unpack(row["payload"], row["sha"], (input) => { const read = readSkillsSnapshotPayload(input); return read.ok ? read.value : null; });
|
|
320
329
|
const { packageShas, ...facts } = saved;
|
|
321
330
|
if (packageShas.length &&
|
|
322
331
|
!acceptedIdentities(store, String(row["repo"])).has(facts.identity))
|
|
@@ -401,10 +410,10 @@ export function freezeSkills(store, runId) {
|
|
|
401
410
|
}
|
|
402
411
|
}
|
|
403
412
|
const { packages, ...facts } = snapshot;
|
|
404
|
-
const payload = JSON.stringify({
|
|
413
|
+
const payload = JSON.stringify(written(savedSkillsSnapshotSchema, {
|
|
405
414
|
...facts,
|
|
406
415
|
packageShas: packages.map((p) => p.sha),
|
|
407
|
-
});
|
|
416
|
+
}));
|
|
408
417
|
store.handle
|
|
409
418
|
.prepare("INSERT INTO skill_snapshot VALUES (?,?,?,?)")
|
|
410
419
|
.run(runId, ref.repo, payload, learningSha(payload));
|
|
@@ -474,11 +483,11 @@ export function testSkill(store, args, now = new Date()) {
|
|
|
474
483
|
const skill = view.library.find((p) => p.sha === args.sha);
|
|
475
484
|
if (!skill)
|
|
476
485
|
throw Error("Choose an available skill.");
|
|
477
|
-
const sample = text(args.sample,
|
|
478
|
-
if (sample.length >
|
|
486
|
+
const sample = text(args.sample, TEXT_LIMITS.skillSampleBytes, "Sample request");
|
|
487
|
+
if (sample.length > TEXT_LIMITS.skillSample)
|
|
479
488
|
throw Error("Keep the sample request to 800 characters.");
|
|
480
|
-
const feedback = args.feedback === undefined ? null : text(args.feedback,
|
|
481
|
-
if (feedback && feedback.length >
|
|
489
|
+
const feedback = args.feedback === undefined ? null : text(args.feedback, TEXT_LIMITS.skillFeedbackBytes, "Feedback");
|
|
490
|
+
if (feedback && feedback.length > TEXT_LIMITS.skillFeedback)
|
|
482
491
|
throw Error("Keep feedback to 500 characters.");
|
|
483
492
|
if (args.sourceRun !== undefined) {
|
|
484
493
|
const source = skillTestResult(store, args.sourceRun, args.actor);
|
package/dist/project-tools.d.ts
CHANGED
|
@@ -201,6 +201,7 @@ export declare function prepareRunTools(store: Pick<Store, "projectTools" | "get
|
|
|
201
201
|
now: Date;
|
|
202
202
|
includeModel: boolean;
|
|
203
203
|
readOnly?: Readonly<Record<string, readonly string[]>>;
|
|
204
|
+
withheld?: Readonly<Record<string, string>>;
|
|
204
205
|
}): ToolLaunchArgs;
|
|
205
206
|
/** When a launch's tools cannot be prepared, it still gets none of anything else's: the same isolation with an empty list. */
|
|
206
207
|
export declare function noToolsArgs(provider: "claude" | "codex" | "openrouter" | "gemini", includeModel: boolean): ToolLaunchArgs;
|
package/dist/project-tools.js
CHANGED
|
@@ -694,7 +694,7 @@ export function toolLaunchFor(store, runId, home = homedir()) {
|
|
|
694
694
|
* with `argv`/`env` and calls `cleanup` when the process is done.
|
|
695
695
|
*/
|
|
696
696
|
export function prepareRunTools(store, runId, provider, options) {
|
|
697
|
-
const launch = limitedTo(toolLaunchFor(store, runId, options.home), options.readOnly);
|
|
697
|
+
const launch = limitedTo(toolLaunchFor(store, runId, options.home), options.readOnly, options.withheld);
|
|
698
698
|
const enabledTools = options.readOnly === undefined ? {} : { enabledTools: options.readOnly };
|
|
699
699
|
const prepared = provider === "claude" ? claudeToolArgs(launch)
|
|
700
700
|
: provider === "codex" ? codexToolArgs(launch, { includeModel: options.includeModel, ...enabledTools })
|
|
@@ -710,14 +710,14 @@ export function prepareRunTools(store, runId, provider, options) {
|
|
|
710
710
|
}), options.now);
|
|
711
711
|
return prepared;
|
|
712
712
|
}
|
|
713
|
-
/** A research step's launch: only the servers it may read, each limited to its read-only actions; the rest left out, saying why. */
|
|
714
|
-
function limitedTo(launch, readOnly) {
|
|
713
|
+
/** A research step's launch: only the servers it may read, each limited to its read-only actions; the rest left out, saying why (`withheld`, when a service has its own reason). */
|
|
714
|
+
function limitedTo(launch, readOnly, withheld = {}) {
|
|
715
715
|
if (readOnly === undefined)
|
|
716
716
|
return launch;
|
|
717
717
|
const readable = (name) => (readOnly[name] ?? []).length > 0;
|
|
718
718
|
return {
|
|
719
719
|
tools: launch.tools.filter(one => readable(one.spec.name)),
|
|
720
|
-
skipped: [...launch.skipped, ...launch.tools.filter(one => !readable(one.spec.name)).map(one => ({ name: one.spec.name, reason: "research reads only connected services' read-only actions" }))],
|
|
720
|
+
skipped: [...launch.skipped, ...launch.tools.filter(one => !readable(one.spec.name)).map(one => ({ name: one.spec.name, reason: withheld[one.spec.name] ?? "research reads only connected services' read-only actions" }))],
|
|
721
721
|
};
|
|
722
722
|
}
|
|
723
723
|
/** When a launch's tools cannot be prepared, it still gets none of anything else's: the same isolation with an empty list. */
|