engineering-memory 1.11.9 → 1.11.10
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/dispatcher/sections.mjs +6 -2
- package/install/codex-approval.mjs +457 -0
- package/install/files.mjs +5 -1
- package/install/installer.mjs +24 -1
- package/package.json +1 -1
- package/runtime/build.json +1 -1
- package/runtime/dist/src/config.js +1 -0
- package/runtime/dist/src/git/git-inspector.js +82 -3
- package/runtime/dist/src/git/verification-gate.js +30 -17
- package/runtime/dist/src/mcp/onboarding-tools.js +3 -16
- package/runtime/dist/src/mcp/questionnaire-tools.js +99 -15
- package/runtime/dist/src/mcp/tool-annotations.js +111 -0
- package/runtime/dist/src/mcp/tool-definitions.js +148 -28
- package/runtime/dist/src/mcp/worktree-tools.js +323 -236
- package/runtime/dist/src/project/repository.js +11 -5
- package/runtime/dist/src/runtime/active-context-store.js +3 -0
- package/runtime/dist/src/runtime/branch-preferences.js +0 -19
- package/runtime/dist/src/runtime/bridge-service.js +1131 -220
- package/runtime/dist/src/runtime/offline-outbox.js +33 -158
- package/runtime/dist/src/runtime/phase-timer.js +26 -0
- package/runtime/dist/src/runtime/privacy-detector.js +263 -0
- package/runtime/dist/src/runtime/questionnaire-store.js +210 -49
- package/runtime/dist/src/runtime/recovery-error.js +3 -1
- package/runtime/dist/src/runtime/runtime-entry.js +8 -0
- package/runtime/dist/src/runtime/runtime-host.js +1 -3
- package/runtime/dist/src/runtime/task-branch-store.js +17 -1
- package/runtime/dist/src/runtime/task-start.js +281 -0
- package/runtime/dist/src/runtime/worktree-pool.js +334 -24
- package/runtime/dist/src/utilities/process.js +1 -0
- package/skill/SKILL.md +3 -3
- package/skill/references/lifecycle.md +44 -19
- package/skill/references/memory-updates.md +9 -3
- package/skill/references/project-onboarding.md +19 -2
- package/skill/references/questionnaires.md +77 -15
|
@@ -6,7 +6,71 @@ import { assertManagedPath, ensureManagedDirectory, isNodeError, readJson, remov
|
|
|
6
6
|
import { sha256, stableStringify } from '../utilities/hash.js';
|
|
7
7
|
import { assertSafeToPersist } from './offline-outbox.js';
|
|
8
8
|
const identifier = z.string().regex(/^[A-Za-z0-9_-]{1,100}$/);
|
|
9
|
-
|
|
9
|
+
const questionId = z.string().regex(/^[a-z][a-z0-9_]{0,39}$/);
|
|
10
|
+
const optionSchema = z.strictObject({
|
|
11
|
+
id: z
|
|
12
|
+
.string()
|
|
13
|
+
.regex(/^[A-Za-z0-9_-]{1,80}$/)
|
|
14
|
+
.refine((value) => value !== '__other__'),
|
|
15
|
+
label: z.string().trim().min(1).max(240),
|
|
16
|
+
});
|
|
17
|
+
const textFieldSchema = z.strictObject({
|
|
18
|
+
title: z.string().trim().min(1).max(120),
|
|
19
|
+
maxLength: z.number().int().min(1).max(2000),
|
|
20
|
+
requiredForChoice: z
|
|
21
|
+
.string()
|
|
22
|
+
.regex(/^[A-Za-z0-9_-]{1,80}$/)
|
|
23
|
+
.optional(),
|
|
24
|
+
valueType: z.enum(['text', 'uuid_list']).optional(),
|
|
25
|
+
});
|
|
26
|
+
const subQuestionSchema = z
|
|
27
|
+
.strictObject({
|
|
28
|
+
id: questionId,
|
|
29
|
+
message: z.string().trim().min(1).max(600),
|
|
30
|
+
options: z
|
|
31
|
+
.array(optionSchema)
|
|
32
|
+
.min(2)
|
|
33
|
+
.max(4)
|
|
34
|
+
.refine((values) => new Set(values.map((value) => value.id)).size === values.length),
|
|
35
|
+
otherAsText: z.boolean().default(false),
|
|
36
|
+
textField: textFieldSchema.optional(),
|
|
37
|
+
})
|
|
38
|
+
.refine((value) => !value.otherAsText ||
|
|
39
|
+
(value.options.some((option) => option.id === 'other') &&
|
|
40
|
+
value.textField?.requiredForChoice === 'other'), 'otherAsText requires an "other" option with a textField required for that choice.');
|
|
41
|
+
const ownerSchema = z.strictObject({
|
|
42
|
+
tool: z.string().min(1).max(80),
|
|
43
|
+
externalTaskId: z.string().min(1).max(160).optional(),
|
|
44
|
+
decisionAttempt: z.number().int().min(1).max(10000).optional(),
|
|
45
|
+
retryArguments: z.record(z.string(), z.unknown()).optional(),
|
|
46
|
+
});
|
|
47
|
+
function definitionIssues(value) {
|
|
48
|
+
const issues = [];
|
|
49
|
+
const hasOptions = value.options !== undefined;
|
|
50
|
+
const hasQuestions = value.questions !== undefined;
|
|
51
|
+
if (hasOptions === hasQuestions)
|
|
52
|
+
issues.push('Exactly one of options or questions must be set.');
|
|
53
|
+
if (hasQuestions && (value.allowFreeText || value.textField))
|
|
54
|
+
issues.push('allowFreeText and textField apply only to a single-question definition.');
|
|
55
|
+
if (value.terminalChoices) {
|
|
56
|
+
if (!hasQuestions)
|
|
57
|
+
issues.push('terminalChoices requires questions.');
|
|
58
|
+
else {
|
|
59
|
+
const ids = new Set((value.questions ?? []).map((question) => question.id));
|
|
60
|
+
for (const terminal of value.terminalChoices)
|
|
61
|
+
if (!ids.has(terminal.questionId))
|
|
62
|
+
issues.push(`terminalChoices references unknown question "${terminal.questionId}".`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return issues;
|
|
66
|
+
}
|
|
67
|
+
function attachDefinitionChecks(schema) {
|
|
68
|
+
return schema.superRefine((value, ctx) => {
|
|
69
|
+
for (const message of definitionIssues(value))
|
|
70
|
+
ctx.addIssue({ code: 'custom', message });
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
const questionnaireDefinitionShape = z.strictObject({
|
|
10
74
|
questionnaireId: identifier,
|
|
11
75
|
message: z.string().trim().min(1).max(2000),
|
|
12
76
|
language: z
|
|
@@ -14,51 +78,60 @@ export const questionnaireDefinitionSchema = z.strictObject({
|
|
|
14
78
|
.optional()
|
|
15
79
|
.describe('Language of the current conversation: tr for Turkish, en for English. Translate the message and option labels into that language too.'),
|
|
16
80
|
options: z
|
|
17
|
-
.array(
|
|
18
|
-
id: z
|
|
19
|
-
.string()
|
|
20
|
-
.regex(/^[A-Za-z0-9_-]{1,80}$/)
|
|
21
|
-
.refine((value) => value !== '__other__'),
|
|
22
|
-
label: z.string().trim().min(1).max(240),
|
|
23
|
-
}))
|
|
81
|
+
.array(optionSchema)
|
|
24
82
|
.min(2)
|
|
25
83
|
.max(12)
|
|
26
|
-
.refine((values) => new Set(values.map((value) => value.id)).size === values.length)
|
|
84
|
+
.refine((values) => new Set(values.map((value) => value.id)).size === values.length)
|
|
85
|
+
.optional(),
|
|
27
86
|
allowFreeText: z.boolean().default(false),
|
|
28
|
-
textField:
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
87
|
+
textField: textFieldSchema.optional(),
|
|
88
|
+
questions: z
|
|
89
|
+
.array(subQuestionSchema)
|
|
90
|
+
.min(1)
|
|
91
|
+
.max(8)
|
|
92
|
+
.refine((values) => new Set(values.map((value) => value.id)).size === values.length)
|
|
93
|
+
.optional(),
|
|
94
|
+
terminalChoices: z
|
|
95
|
+
.array(z.strictObject({ questionId, choice: z.string().regex(/^[A-Za-z0-9_-]{1,80}$/) }))
|
|
96
|
+
.max(8)
|
|
38
97
|
.optional(),
|
|
39
98
|
});
|
|
99
|
+
export const questionnaireDefinitionSchema = attachDefinitionChecks(questionnaireDefinitionShape);
|
|
100
|
+
const boundDefinitionShape = questionnaireDefinitionShape.extend({
|
|
101
|
+
binding: z.record(z.string(), z.json()).optional(),
|
|
102
|
+
});
|
|
103
|
+
const boundDefinitionSchema = attachDefinitionChecks(boundDefinitionShape);
|
|
40
104
|
const scopeSchema = z.strictObject({
|
|
41
105
|
principalHash: z.string().regex(/^[0-9a-f]{64}$/),
|
|
42
106
|
repoFingerprint: z.string().regex(/^[0-9a-f]{64}$/),
|
|
43
107
|
projectId: z.string().uuid().nullable(),
|
|
44
108
|
});
|
|
45
|
-
const storedQuestionSchema =
|
|
109
|
+
const storedQuestionSchema = attachDefinitionChecks(boundDefinitionShape.extend({
|
|
46
110
|
schemaVersion: z.literal(1),
|
|
47
111
|
scope: scopeSchema,
|
|
48
112
|
contentHash: z.string().regex(/^[0-9a-f]{64}$/),
|
|
49
113
|
requestKey: z.string().regex(/^questionnaire_[0-9a-f]{64}$/),
|
|
50
114
|
createdAt: z.string().datetime(),
|
|
51
|
-
|
|
52
|
-
|
|
115
|
+
owner: ownerSchema.optional(),
|
|
116
|
+
}));
|
|
117
|
+
const singleAnswerShape = z.strictObject({ choice: z.string(), text: z.string().optional() });
|
|
118
|
+
export const hostAnswerSchema = z
|
|
119
|
+
.strictObject({
|
|
53
120
|
questionnaireId: identifier,
|
|
54
121
|
requestKey: z.string().regex(/^questionnaire_[0-9a-f]{64}$/),
|
|
55
122
|
contentHash: z.string().regex(/^[0-9a-f]{64}$/),
|
|
56
123
|
hostTool: z.enum(['AskUserQuestion', 'request_user_input']),
|
|
57
|
-
answer: z
|
|
124
|
+
answer: z
|
|
125
|
+
.strictObject({ choice: z.string().min(1).max(80), text: z.string().max(2000).optional() })
|
|
126
|
+
.optional(),
|
|
127
|
+
answers: z
|
|
128
|
+
.record(questionId, z.strictObject({
|
|
58
129
|
choice: z.string().min(1).max(80),
|
|
59
130
|
text: z.string().max(2000).optional(),
|
|
60
|
-
})
|
|
61
|
-
|
|
131
|
+
}))
|
|
132
|
+
.optional(),
|
|
133
|
+
})
|
|
134
|
+
.refine((value) => Boolean(value.answer) !== Boolean(value.answers), 'Exactly one of answer or answers must be set.');
|
|
62
135
|
const answerSourceSchema = z.strictObject({
|
|
63
136
|
kind: z.literal('host_native_relay'),
|
|
64
137
|
hostTool: z.enum(['AskUserQuestion', 'request_user_input']),
|
|
@@ -69,9 +142,10 @@ const storedAnswerSchema = z.strictObject({
|
|
|
69
142
|
answerAvailable: z.boolean(),
|
|
70
143
|
withdrawn: z.literal(true).optional(),
|
|
71
144
|
answerSource: answerSourceSchema.optional(),
|
|
72
|
-
answer:
|
|
145
|
+
answer: singleAnswerShape.optional(),
|
|
146
|
+
answers: z.record(questionId, singleAnswerShape).optional(),
|
|
73
147
|
});
|
|
74
|
-
|
|
148
|
+
function singleQuestionAnswerSchema(record) {
|
|
75
149
|
const textSchema = record.textField
|
|
76
150
|
? record.textField.valueType === 'uuid_list'
|
|
77
151
|
? z
|
|
@@ -111,20 +185,55 @@ export function questionnaireAnswerSchema(record) {
|
|
|
111
185
|
return answer.text === undefined;
|
|
112
186
|
});
|
|
113
187
|
}
|
|
188
|
+
function multiQuestionAnswerSchema(record) {
|
|
189
|
+
const terminal = record.terminalChoices ?? [];
|
|
190
|
+
const shape = {};
|
|
191
|
+
for (const sub of record.questions) {
|
|
192
|
+
const optionIds = sub.options.map((option) => option.id);
|
|
193
|
+
const textSchema = sub.textField
|
|
194
|
+
? z.string().trim().min(1).max(sub.textField.maxLength).optional()
|
|
195
|
+
: z.never().optional();
|
|
196
|
+
shape[sub.id] = z
|
|
197
|
+
.strictObject({
|
|
198
|
+
choice: z.enum(optionIds),
|
|
199
|
+
text: z.preprocess((value) => (typeof value === 'string' && value.trim() === '' ? undefined : value), textSchema),
|
|
200
|
+
})
|
|
201
|
+
.refine((answer) => {
|
|
202
|
+
if (sub.textField?.requiredForChoice === answer.choice)
|
|
203
|
+
return typeof answer.text === 'string';
|
|
204
|
+
return answer.text === undefined;
|
|
205
|
+
})
|
|
206
|
+
.optional();
|
|
207
|
+
}
|
|
208
|
+
return z.strictObject(shape).refine((value) => {
|
|
209
|
+
const answers = value;
|
|
210
|
+
const terminalHit = terminal.some((choice) => answers[choice.questionId]?.choice === choice.choice);
|
|
211
|
+
if (terminalHit)
|
|
212
|
+
return true;
|
|
213
|
+
return record.questions.every((sub) => answers[sub.id] !== undefined);
|
|
214
|
+
}, 'All questions must be answered unless a terminal choice was picked.');
|
|
215
|
+
}
|
|
216
|
+
export function questionnaireAnswerSchema(record) {
|
|
217
|
+
return record.questions
|
|
218
|
+
? multiQuestionAnswerSchema(record)
|
|
219
|
+
: singleQuestionAnswerSchema(record);
|
|
220
|
+
}
|
|
114
221
|
export class QuestionnaireStore {
|
|
115
222
|
root;
|
|
116
223
|
constructor(stateRoot) {
|
|
117
224
|
this.root = join(stateRoot, 'questionnaires');
|
|
118
225
|
}
|
|
119
|
-
async ask(scope, input, previousDefinitions = []) {
|
|
120
|
-
const definition =
|
|
226
|
+
async ask(scope, input, previousDefinitions = [], owner) {
|
|
227
|
+
const definition = boundDefinitionSchema.parse(input);
|
|
121
228
|
const previousHashes = previousDefinitions.map((previous) => {
|
|
122
|
-
const parsed =
|
|
229
|
+
const parsed = boundDefinitionSchema.parse(previous);
|
|
123
230
|
if (parsed.questionnaireId !== definition.questionnaireId)
|
|
124
231
|
throw new Error('Previous questionnaire content must belong to the same decision.');
|
|
125
232
|
return sha256(stableStringify(parsed));
|
|
126
233
|
});
|
|
127
234
|
scopeSchema.parse(scope);
|
|
235
|
+
if (owner)
|
|
236
|
+
ownerSchema.parse(owner);
|
|
128
237
|
assertSafeToPersist(definition);
|
|
129
238
|
const contentHash = sha256(stableStringify(definition));
|
|
130
239
|
const question = {
|
|
@@ -134,6 +243,7 @@ export class QuestionnaireStore {
|
|
|
134
243
|
contentHash,
|
|
135
244
|
requestKey: `questionnaire_${sha256(`${stableStringify(scope)}\n${contentHash}\n${randomUUID()}`)}`,
|
|
136
245
|
createdAt: new Date().toISOString(),
|
|
246
|
+
...(owner ? { owner } : {}),
|
|
137
247
|
};
|
|
138
248
|
await this.publish(this.pathFor(scope, definition.questionnaireId, 'pending.json'), question);
|
|
139
249
|
const record = await this.get(scope, definition.questionnaireId);
|
|
@@ -148,13 +258,16 @@ export class QuestionnaireStore {
|
|
|
148
258
|
if (!raw)
|
|
149
259
|
return null;
|
|
150
260
|
const question = storedQuestionSchema.parse(raw);
|
|
151
|
-
const definition =
|
|
261
|
+
const definition = boundDefinitionSchema.parse({
|
|
152
262
|
questionnaireId: question.questionnaireId,
|
|
153
263
|
message: question.message,
|
|
154
264
|
language: question.language,
|
|
155
265
|
options: question.options,
|
|
156
266
|
allowFreeText: question.allowFreeText,
|
|
157
267
|
textField: question.textField,
|
|
268
|
+
questions: question.questions,
|
|
269
|
+
terminalChoices: question.terminalChoices,
|
|
270
|
+
binding: question.binding,
|
|
158
271
|
});
|
|
159
272
|
if (stableStringify(question.scope) !== stableStringify(scope) ||
|
|
160
273
|
question.questionnaireId !== questionnaireId ||
|
|
@@ -167,11 +280,36 @@ export class QuestionnaireStore {
|
|
|
167
280
|
return { ...question, status: 'pending' };
|
|
168
281
|
const answer = storedAnswerSchema.parse(rawAnswer);
|
|
169
282
|
if (answer.withdrawn) {
|
|
170
|
-
if (answer.requestKey !== question.requestKey ||
|
|
283
|
+
if (answer.requestKey !== question.requestKey ||
|
|
284
|
+
answer.answerAvailable ||
|
|
285
|
+
answer.answer ||
|
|
286
|
+
answer.answers)
|
|
171
287
|
throw new Error('Invalid questionnaire withdrawal receipt.');
|
|
172
288
|
return { ...question, ...answer, status: 'withdrawn' };
|
|
173
289
|
}
|
|
290
|
+
if (question.questions) {
|
|
291
|
+
if (answer.requestKey !== question.requestKey ||
|
|
292
|
+
!answer.answerAvailable ||
|
|
293
|
+
!answer.answers ||
|
|
294
|
+
answer.answer !== undefined)
|
|
295
|
+
throw new Error('Questionnaire answer does not match its durable question.');
|
|
296
|
+
for (const [id, value] of Object.entries(answer.answers)) {
|
|
297
|
+
const sub = question.questions.find((item) => item.id === id);
|
|
298
|
+
if (!sub || !sub.options.some((option) => option.id === value.choice))
|
|
299
|
+
throw new Error('Questionnaire answer does not match its durable question.');
|
|
300
|
+
if (value.text !== undefined && !sub.textField)
|
|
301
|
+
throw new Error('Questionnaire text answer does not match its durable question.');
|
|
302
|
+
if (sub.textField?.requiredForChoice === value.choice && typeof value.text !== 'string')
|
|
303
|
+
throw new Error('Questionnaire text answer is required for this choice.');
|
|
304
|
+
}
|
|
305
|
+
const terminalHit = (question.terminalChoices ?? []).some((choice) => answer.answers[choice.questionId]?.choice === choice.choice);
|
|
306
|
+
if (!terminalHit && !question.questions.every((sub) => answer.answers[sub.id] !== undefined))
|
|
307
|
+
throw new Error('Questionnaire answer is missing a required question.');
|
|
308
|
+
assertSafeToPersist(answer);
|
|
309
|
+
return { ...question, ...answer, status: 'answered' };
|
|
310
|
+
}
|
|
174
311
|
if (answer.requestKey !== question.requestKey ||
|
|
312
|
+
answer.answers !== undefined ||
|
|
175
313
|
(answer.answerAvailable
|
|
176
314
|
? !answer.answer || !question.options.some((option) => option.id === answer.answer.choice)
|
|
177
315
|
: !question.allowFreeText || answer.answer !== undefined)) {
|
|
@@ -187,6 +325,12 @@ export class QuestionnaireStore {
|
|
|
187
325
|
return { ...question, ...answer, status: 'answered' };
|
|
188
326
|
}
|
|
189
327
|
async pending(scope) {
|
|
328
|
+
return (await this.scan(scope)).filter((record) => record.status === 'pending');
|
|
329
|
+
}
|
|
330
|
+
async list(scope) {
|
|
331
|
+
return this.scan(scope);
|
|
332
|
+
}
|
|
333
|
+
async scan(scope) {
|
|
190
334
|
const directory = this.scopeRoot(scope);
|
|
191
335
|
await ensureManagedDirectory(this.root, directory);
|
|
192
336
|
const entries = await readdir(directory, { withFileTypes: true });
|
|
@@ -204,7 +348,7 @@ export class QuestionnaireStore {
|
|
|
204
348
|
if (sha256(question.questionnaireId) !== entry.name)
|
|
205
349
|
throw new Error('Questionnaire directory does not match its id.');
|
|
206
350
|
const record = await this.get(scope, question.questionnaireId);
|
|
207
|
-
if (record
|
|
351
|
+
if (record)
|
|
208
352
|
records.push(record);
|
|
209
353
|
}
|
|
210
354
|
return records.sort((left, right) => left.createdAt.localeCompare(right.createdAt));
|
|
@@ -217,31 +361,48 @@ export class QuestionnaireStore {
|
|
|
217
361
|
throw new Error('Questionnaire response is stale or belongs to a different scope.');
|
|
218
362
|
if (record.status === 'withdrawn')
|
|
219
363
|
throw new Error('Questionnaire was explicitly withdrawn. Start a new decision if the user resumes the work.');
|
|
220
|
-
const
|
|
221
|
-
assertSafeToPersist(
|
|
364
|
+
const parsed = questionnaireAnswerSchema(record).parse(input);
|
|
365
|
+
assertSafeToPersist(parsed);
|
|
222
366
|
if (record.status === 'answered') {
|
|
223
|
-
|
|
367
|
+
const stored = record.questions ? record.answers : record.answer;
|
|
368
|
+
if (stored !== undefined && stableStringify(stored) !== stableStringify(parsed))
|
|
224
369
|
throw new Error('Questionnaire was already answered differently.');
|
|
225
|
-
return { record, answer:
|
|
370
|
+
return { record, answer: stored, replayed: true };
|
|
226
371
|
}
|
|
227
|
-
const receipt =
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
372
|
+
const receipt = record.questions
|
|
373
|
+
? {
|
|
374
|
+
requestKey,
|
|
375
|
+
...(answerSource ? { answerSource } : {}),
|
|
376
|
+
answeredAt: new Date().toISOString(),
|
|
377
|
+
answerAvailable: true,
|
|
378
|
+
answers: parsed,
|
|
379
|
+
}
|
|
380
|
+
: (() => {
|
|
381
|
+
const answer = parsed;
|
|
382
|
+
return {
|
|
383
|
+
requestKey,
|
|
384
|
+
...(answerSource ? { answerSource } : {}),
|
|
385
|
+
answeredAt: new Date().toISOString(),
|
|
386
|
+
answerAvailable: answer.choice !== '__other__',
|
|
387
|
+
...(answer.choice !== '__other__'
|
|
388
|
+
? { answer: { choice: answer.choice, ...(answer.text ? { text: answer.text } : {}) } }
|
|
389
|
+
: {}),
|
|
390
|
+
};
|
|
391
|
+
})();
|
|
236
392
|
const published = await this.publish(this.pathFor(scope, questionnaireId, 'answer.json'), receipt);
|
|
237
393
|
const resolved = (await this.get(scope, questionnaireId));
|
|
238
394
|
if (resolved.status === 'withdrawn')
|
|
239
395
|
throw new Error('Questionnaire was explicitly withdrawn before this response.');
|
|
396
|
+
const resolvedStored = record.questions ? resolved.answers : resolved.answer;
|
|
240
397
|
if (!published &&
|
|
241
|
-
|
|
242
|
-
stableStringify(
|
|
398
|
+
resolvedStored !== undefined &&
|
|
399
|
+
stableStringify(resolvedStored) !== stableStringify(parsed))
|
|
243
400
|
throw new Error('Questionnaire was already answered differently.');
|
|
244
|
-
return {
|
|
401
|
+
return {
|
|
402
|
+
record: resolved,
|
|
403
|
+
answer: published ? parsed : resolvedStored,
|
|
404
|
+
replayed: !published,
|
|
405
|
+
};
|
|
245
406
|
}
|
|
246
407
|
async withdraw(scope, questionnaireId) {
|
|
247
408
|
const record = await this.get(scope, questionnaireId);
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export class BridgeRecoveryError extends Error {
|
|
2
2
|
recovery;
|
|
3
|
-
|
|
3
|
+
details;
|
|
4
|
+
constructor(message, recovery, details) {
|
|
4
5
|
super(`${message} Recovery: call ${recovery}.`);
|
|
5
6
|
this.recovery = recovery;
|
|
7
|
+
this.details = details;
|
|
6
8
|
this.name = 'BridgeRecoveryError';
|
|
7
9
|
}
|
|
8
10
|
}
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { registerAllTools } from '../mcp/server.js';
|
|
2
|
+
import { toolAnnotations } from '../mcp/tool-annotations.js';
|
|
2
3
|
import { recordingHost } from '../mcp/tool-host.js';
|
|
3
4
|
import { createBridgeService } from './create-bridge-service.js';
|
|
4
5
|
export function collectTools(service, server) {
|
|
5
6
|
const recording = recordingHost(server);
|
|
6
7
|
registerAllTools(recording.host, service);
|
|
8
|
+
const described = toolAnnotations;
|
|
9
|
+
for (const spec of recording.tools) {
|
|
10
|
+
const annotations = described[spec.name];
|
|
11
|
+
if (!annotations)
|
|
12
|
+
throw new Error(`Tool ${spec.name} has no entry in tool-annotations.ts; describe it there.`);
|
|
13
|
+
spec.config = { ...spec.config, annotations: { ...annotations, ...spec.config.annotations } };
|
|
14
|
+
}
|
|
7
15
|
return recording.tools;
|
|
8
16
|
}
|
|
9
17
|
export function createRuntime(options) {
|
|
@@ -171,9 +171,7 @@ export class RuntimeHost {
|
|
|
171
171
|
...(spec.config.outputSchema !== undefined
|
|
172
172
|
? { outputSchema: spec.config.outputSchema }
|
|
173
173
|
: {}),
|
|
174
|
-
|
|
175
|
-
? { annotations: spec.config.annotations }
|
|
176
|
-
: {}),
|
|
174
|
+
annotations: spec.config.annotations ?? {},
|
|
177
175
|
});
|
|
178
176
|
existing.signature = signature;
|
|
179
177
|
}
|
|
@@ -89,10 +89,26 @@ export class TaskBranchStore {
|
|
|
89
89
|
throw new BridgeRecoveryError('The task has finished but its worktree reservation could not be released. Resume the task to retry cleanup.', 'session.resume');
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
|
+
async detach(expected, oid) {
|
|
93
|
+
const current = await this.read();
|
|
94
|
+
const changed = () => new BridgeRecoveryError(`The reservation of this folder changed after the question about ${expected.externalTaskId} was asked. Call task.branch again; its question shows which task holds the folder now.`, 'task.branch');
|
|
95
|
+
if (!current ||
|
|
96
|
+
current.oid !== oid ||
|
|
97
|
+
current.decision.projectId !== expected.projectId ||
|
|
98
|
+
current.decision.externalTaskId !== expected.externalTaskId ||
|
|
99
|
+
current.decision.taskId !== expected.taskId)
|
|
100
|
+
throw changed();
|
|
101
|
+
const result = await this.runner.run('git', ['update-ref', '--no-deref', '-d', decisionRef, oid], { cwd: this.repoRoot });
|
|
102
|
+
if (result.exitCode !== 0) {
|
|
103
|
+
if ((await this.read())?.oid !== oid)
|
|
104
|
+
throw changed();
|
|
105
|
+
throw new BridgeRecoveryError('Git could not move the task out of this folder. Retry task.branch; nothing was changed.', 'task.branch');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
92
108
|
assertSame(current, requested) {
|
|
93
109
|
if (current.projectId !== requested.projectId ||
|
|
94
110
|
current.externalTaskId !== requested.externalTaskId) {
|
|
95
|
-
throw new BridgeRecoveryError(`This
|
|
111
|
+
throw new BridgeRecoveryError(`This folder is held by task ${current.externalTaskId}. Continue that task, or call task.branch for the new task: its question offers a separate worktree, or a new branch in this folder after asking whether ${current.externalTaskId} should move out.`, 'task.branch');
|
|
96
112
|
}
|
|
97
113
|
if (current.branch !== requested.branch) {
|
|
98
114
|
throw new BridgeRecoveryError(`This task selected ${current.branch ?? 'detached HEAD'}. Return to that branch before continuing.`, 'task.branch');
|