engineering-memory 1.11.6 → 1.11.7
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 +2 -0
- package/package.json +29 -29
- package/runtime/dist/src/git/verification-gate.js +3 -3
- package/runtime/dist/src/mcp/questionnaire-tools.js +35 -7
- package/runtime/dist/src/mcp/tool-definitions.js +13 -4
- package/runtime/dist/src/runtime/bridge-service.js +16 -2
- package/runtime/dist/src/runtime/questionnaire-store.js +19 -1
- package/skill/SKILL.md +2 -0
- package/skill/references/questionnaires.md +24 -0
package/dispatcher/sections.mjs
CHANGED
|
@@ -27,6 +27,8 @@ Do not block independent task work on \`memory.propose_revision\` drafting, subm
|
|
|
27
27
|
|
|
28
28
|
Use the host's native questionnaire for every question to the user, including implementation choices, names, clarification, branch/worktree decisions and delivery. For a required decision, use \`questionnaire.ask\` to open a durable native MCP form and \`questionnaire.resume\` to return to the same unanswered question. Read the skill's questionnaires reference first. Never use request_user_input_async for a required decision: it does not wait for an answer. When the durable form is unavailable, use a blocking native control only where the host permits it: request_user_input in Codex or AskUserQuestion in Claude. Never replace the questionnaire with a chat instruction such as 'type this', 'reply yes', or 'write X if you want Y'. Do not open a survey web page. If the required native control is unavailable or prohibited for that kind of question, follow the host's tool restrictions, explain the limitation, and continue only work already authorized; do not fabricate a survey or silently choose an answer. A timeout, dismissed form, empty response or ended turn is not an answer. Keep the decision pending and resume it; do not start dependent work or report it as resolved. Existing answers remain valid through retries and handoffs.
|
|
29
29
|
|
|
30
|
+
When a durable MCP form cannot be displayed, read its \`hostFallback\` or call \`questionnaire.resume\` with \`presentation: host_native\` to get the original question without reopening the MCP form. Display the same question, all choices and notices through a blocking native control only if the host permits that control for this decision. After an actual native answer, call \`questionnaire.answer_from_host\` with the unchanged questionnaireId, requestKey and contentHash, the hostTool name and the returned choice/text. This is an agent-reported relay, not MCP transport attestation. Then retry the owning operation; its authority, content and version checks still apply. Never relay prose consent, a default, an asynchronous response or a cancelled/declined/missing answer. Decline alone is not proof that the host cannot display forms. Keep the decision pending if no permitted native control can represent it.
|
|
31
|
+
|
|
30
32
|
When the repository is unbound, use the native questionnaire required by the skill. Do not silently create or bind a project. Do not open a survey web page.
|
|
31
33
|
|
|
32
34
|
For project creation or adoption, read the skill's project-onboarding reference. Use read-only maturity evidence, then native selection of greenfield, exhaustive adoption or phased rework. A missing folder or Git repository uses preparation:true questionnaires before project.initialize. Recognize later memory-refresh intent semantically and confirm its scope natively; never trigger it from a fixed word list. Adoption and refresh cover architecture, state, services, network, data, screens and cross-module flows, not just page logic.`;
|
package/package.json
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "engineering-memory",
|
|
3
|
-
"version": "1.11.
|
|
4
|
-
"description": "Installs the Engineering Memory skill and its local MCP bridge. Sign in after installing; your organization and project are resolved from your account.",
|
|
5
|
-
"license": "UNLICENSED",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"engines": {
|
|
8
|
-
"node": ">=20"
|
|
9
|
-
},
|
|
10
|
-
"bin": {
|
|
11
|
-
"engineering-memory": "bin/engineering-memory.mjs"
|
|
12
|
-
},
|
|
13
|
-
"files": [
|
|
14
|
-
"bin",
|
|
15
|
-
"dispatcher",
|
|
16
|
-
"lib",
|
|
17
|
-
"install",
|
|
18
|
-
"skill",
|
|
19
|
-
"runtime"
|
|
20
|
-
],
|
|
21
|
-
"dependencies": {
|
|
22
|
-
"@modelcontextprotocol/server": "2.0.0",
|
|
23
|
-
"minimatch": "10.2.6",
|
|
24
|
-
"zod": "4.4.3"
|
|
25
|
-
},
|
|
26
|
-
"engineeringMemory": {
|
|
27
|
-
"apiUrl": "https://coral-app-zqmj6.ondigitalocean.app"
|
|
28
|
-
}
|
|
29
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "engineering-memory",
|
|
3
|
+
"version": "1.11.7",
|
|
4
|
+
"description": "Installs the Engineering Memory skill and its local MCP bridge. Sign in after installing; your organization and project are resolved from your account.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=20"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"engineering-memory": "bin/engineering-memory.mjs"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"bin",
|
|
15
|
+
"dispatcher",
|
|
16
|
+
"lib",
|
|
17
|
+
"install",
|
|
18
|
+
"skill",
|
|
19
|
+
"runtime"
|
|
20
|
+
],
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@modelcontextprotocol/server": "2.0.0",
|
|
23
|
+
"minimatch": "10.2.6",
|
|
24
|
+
"zod": "4.4.3"
|
|
25
|
+
},
|
|
26
|
+
"engineeringMemory": {
|
|
27
|
+
"apiUrl": "https://coral-app-zqmj6.ondigitalocean.app"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -50,9 +50,9 @@ export class VerificationGate {
|
|
|
50
50
|
}, this.root);
|
|
51
51
|
}
|
|
52
52
|
async rebindWorktreeOwnership(repoRoot, taskId, previous, generation) {
|
|
53
|
-
const
|
|
54
|
-
const identity = worktreeIdentity(
|
|
55
|
-
const path = this.pathFor(
|
|
53
|
+
const repository = await this.repositories.resolve(repoRoot);
|
|
54
|
+
const identity = worktreeIdentity(repository.repoRoot);
|
|
55
|
+
const path = this.pathFor(repository.repoFingerprint, identity, taskId);
|
|
56
56
|
const receipt = await readJson(path, this.root);
|
|
57
57
|
if (!receipt || receipt.taskId !== taskId || receipt.worktreeGeneration !== previous)
|
|
58
58
|
return;
|
|
@@ -10,13 +10,22 @@ export async function askQuestionnaire(server, service, input, context, previous
|
|
|
10
10
|
}
|
|
11
11
|
export async function resumeQuestionnaire(server, service, input, context) {
|
|
12
12
|
try {
|
|
13
|
-
return await present(server, service, await service.questionnaireResume(input), input.repoRoot, context, input.preparation);
|
|
13
|
+
return await present(server, service, await service.questionnaireResume(input), input.repoRoot, context, input.preparation, input.presentation);
|
|
14
14
|
}
|
|
15
15
|
catch (error) {
|
|
16
16
|
return failure(error);
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
-
async function
|
|
19
|
+
export async function answerQuestionnaireFromHost(service, input) {
|
|
20
|
+
try {
|
|
21
|
+
const resolved = await service.questionnaireAnswerFromHost(input);
|
|
22
|
+
return answered(resolved.record, resolved.answer, resolved.replayed);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
return failure(error);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async function present(server, service, record, repoRoot, context, preparation, presentation) {
|
|
20
29
|
if (record.status === 'withdrawn')
|
|
21
30
|
return result({
|
|
22
31
|
questionnaireId: record.questionnaireId,
|
|
@@ -28,6 +37,8 @@ async function present(server, service, record, repoRoot, context, preparation)
|
|
|
28
37
|
return answered(record, record.answer, true);
|
|
29
38
|
if (context.mcpReq.signal.aborted)
|
|
30
39
|
return pending(record, 'interrupted');
|
|
40
|
+
if (presentation === 'host_native')
|
|
41
|
+
return pending(record, 'host_native_requested');
|
|
31
42
|
const responses = context.mcpReq.inputResponses;
|
|
32
43
|
const response = inputResponse(responses, record.requestKey);
|
|
33
44
|
if (response.kind === 'elicit' && response.action === 'accept') {
|
|
@@ -53,15 +64,12 @@ async function present(server, service, record, repoRoot, context, preparation)
|
|
|
53
64
|
const elicitation = capabilities?.elicitation;
|
|
54
65
|
if (!elicitation || (!elicitation.form && elicitation.url !== undefined))
|
|
55
66
|
return pending(record, 'native_form_unavailable');
|
|
56
|
-
const presentationNotice = /^memory-batch-[a-f0-9]{64}-[0-9]+$/.test(record.questionnaireId)
|
|
57
|
-
? 'For an unversioned project proposal, approval updates legacy memory only. Source-aware tasks cannot read it until an inspection explicitly retains it. Check each sourceApplicabilityNotice in memory.list_proposals before approving.'
|
|
58
|
-
: undefined;
|
|
59
67
|
const copy = questionnaireCopy[record.language ?? 'en'];
|
|
60
68
|
return inputRequired({
|
|
61
69
|
inputRequests: {
|
|
62
70
|
[record.requestKey]: inputRequired.elicit({
|
|
63
71
|
mode: 'form',
|
|
64
|
-
message:
|
|
72
|
+
message: presentationMessage(record),
|
|
65
73
|
requestedSchema: {
|
|
66
74
|
type: 'object',
|
|
67
75
|
properties: {
|
|
@@ -122,14 +130,33 @@ const questionnaireCopy = {
|
|
|
122
130
|
otherAnswer: 'Diğer yanıt',
|
|
123
131
|
},
|
|
124
132
|
};
|
|
133
|
+
function presentationMessage(record) {
|
|
134
|
+
const notice = /^memory-batch-[a-f0-9]{64}-[0-9]+$/.test(record.questionnaireId)
|
|
135
|
+
? 'For an unversioned project proposal, approval updates legacy memory only. Source-aware tasks cannot read it until an inspection explicitly retains it. Check each sourceApplicabilityNotice in memory.list_proposals before approving.'
|
|
136
|
+
: undefined;
|
|
137
|
+
const copy = questionnaireCopy[record.language ?? 'en'];
|
|
138
|
+
return `${record.message}${notice ? `\n\n${notice}` : ''}\n\n${copy.pending}${record.allowFreeText ? copy.freeText : ''}${record.textField ? copy.storedText : ''}`;
|
|
139
|
+
}
|
|
125
140
|
function pending(record, reason) {
|
|
126
141
|
return result({
|
|
127
142
|
questionnaireId: record.questionnaireId,
|
|
128
143
|
status: 'pending',
|
|
129
144
|
reason,
|
|
130
145
|
answerAvailable: false,
|
|
146
|
+
hostFallback: {
|
|
147
|
+
operation: 'questionnaire.answer_from_host',
|
|
148
|
+
questionnaireId: record.questionnaireId,
|
|
149
|
+
requestKey: record.requestKey,
|
|
150
|
+
contentHash: record.contentHash,
|
|
151
|
+
message: presentationMessage(record),
|
|
152
|
+
options: record.options,
|
|
153
|
+
language: record.language ?? 'en',
|
|
154
|
+
allowFreeText: record.allowFreeText,
|
|
155
|
+
...(record.textField ? { textField: record.textField } : {}),
|
|
156
|
+
instructions: 'Only if the host permits a blocking native control for this decision, display this exact question, all options and notices in AskUserQuestion or request_user_input. Relay only the actual returned answer with these unchanged bindings. Closing, declining, timeout, prose consent and missing answers are not native answers. Do not reopen a dismissed question in a loop or label decline as proof of host incompatibility. The relay records agent-reported provenance, not MCP transport attestation. Retry the owning operation after acceptance; its authority and version checks still apply.',
|
|
157
|
+
},
|
|
131
158
|
nextAction: reason === 'native_form_unavailable'
|
|
132
|
-
? 'This host does not advertise native MCP forms. The decision remains pending. Explain the limitation and continue only independently authorized work. Use
|
|
159
|
+
? 'This host does not advertise native MCP forms. The decision remains pending. Explain the limitation and continue only independently authorized work. Use hostFallback only with a permitted blocking native control; otherwise keep pending. Never substitute an asynchronous question.'
|
|
133
160
|
: 'The decision remains pending without expiry. Call questionnaire.resume with this questionnaireId to show the same question again. Do not infer an answer or continue dependent work.',
|
|
134
161
|
});
|
|
135
162
|
}
|
|
@@ -140,6 +167,7 @@ function answered(record, answer, replayed) {
|
|
|
140
167
|
answerAvailable: answer !== undefined,
|
|
141
168
|
...(answer ? { answer } : {}),
|
|
142
169
|
replayed,
|
|
170
|
+
...(record.answerSource ? { answerSource: record.answerSource } : {}),
|
|
143
171
|
...(!answer
|
|
144
172
|
? {
|
|
145
173
|
nextAction: 'The free-text answer was accepted earlier but was not stored for privacy. It cannot be recovered or inferred. Use the original answer if still present in this conversation; otherwise collect it again through a new questionnaire or supported native input before dependent work.',
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as z from 'zod/v4';
|
|
2
2
|
import { validationIds } from '../runtime/bridge-service.js';
|
|
3
|
-
import { questionnaireDefinitionSchema } from '../runtime/questionnaire-store.js';
|
|
4
|
-
import { askQuestionnaire, resumeQuestionnaire } from './questionnaire-tools.js';
|
|
3
|
+
import { hostAnswerSchema, questionnaireDefinitionSchema } from '../runtime/questionnaire-store.js';
|
|
4
|
+
import { answerQuestionnaireFromHost, askQuestionnaire, resumeQuestionnaire, } from './questionnaire-tools.js';
|
|
5
5
|
const optionalRepoRoot = z.string().min(1).optional();
|
|
6
6
|
const stringList = z.array(z.string().min(1));
|
|
7
7
|
const jsonValue = z.lazy(() => z.union([
|
|
@@ -58,6 +58,7 @@ export const engineeringMemoryToolNames = [
|
|
|
58
58
|
'audit.list',
|
|
59
59
|
'questionnaire.ask',
|
|
60
60
|
'questionnaire.resume',
|
|
61
|
+
'questionnaire.answer_from_host',
|
|
61
62
|
'questionnaire.withdraw',
|
|
62
63
|
'session.entry',
|
|
63
64
|
'session.set_decision',
|
|
@@ -152,6 +153,13 @@ const reconciliationEntry = z.object({
|
|
|
152
153
|
reason: z.string().optional(),
|
|
153
154
|
});
|
|
154
155
|
export function registerQuestionnaireTools(server, service) {
|
|
156
|
+
server.registerTool('questionnaire.answer_from_host', {
|
|
157
|
+
description: 'Relay an actual answer from a host-permitted blocking native AskUserQuestion or request_user_input control to the exact durable question. First read hostFallback from the pending operation or questionnaire.resume with presentation host_native, display its unchanged question/options/notices and await the real native result. Never infer answers, use chat consent, asynchronous controls, defaults or a declined/dismissed form. The hostTool field is agent-reported provenance, not transport-verified attestation. Bind requestKey and contentHash exactly; changed scope, withdrawn questions, invalid or conflicting answers are refused. This only saves the answer; retry the owning operation for current authority and version checks.',
|
|
158
|
+
inputSchema: hostAnswerSchema.extend({
|
|
159
|
+
repoRoot: optionalRepoRoot,
|
|
160
|
+
preparation: z.boolean().optional(),
|
|
161
|
+
}),
|
|
162
|
+
}, async (input) => answerQuestionnaireFromHost(service, input));
|
|
155
163
|
server.registerTool('questionnaire.withdraw', {
|
|
156
164
|
description: 'Withdraw a pending decision only when the user explicitly cancels that decision or its owning workflow. This is not approval. An answered decision is immutable. Interruption, timeout and dismissal alone never authorize withdrawal.',
|
|
157
165
|
inputSchema: z.strictObject({
|
|
@@ -183,18 +191,19 @@ export function registerQuestionnaireTools(server, service) {
|
|
|
183
191
|
}
|
|
184
192
|
});
|
|
185
193
|
server.registerTool('questionnaire.ask', {
|
|
186
|
-
description: 'Persist a required decision before displaying a native questionnaire. Write the message and labels in the conversation language and set language (tr or en) for the native form help. Use a questionnaireId unique to this decision occurrence or task, and reuse it only for identical retries. A later decision or changed wording/options requires a new id. Only a schema-validated native acceptance answers it. Dismissal, timeout and missing replies remain pending without expiry. Never supply answers in
|
|
194
|
+
description: 'Persist a required decision before displaying a native questionnaire. Write the message and labels in the conversation language and set language (tr or en) for the native form help. Use a questionnaireId unique to this decision occurrence or task, and reuse it only for identical retries. A later decision or changed wording/options requires a new id. Only a schema-validated native acceptance answers it. Dismissal, timeout and missing replies remain pending without expiry. Never supply answers in questionnaire.ask arguments; use questionnaire.answer_from_host only after an actual permitted blocking native answer. Do not put personal information or secrets in the question or options. Free text is returned once and never stored; use explicit options for replayable decisions.',
|
|
187
195
|
inputSchema: questionnaireDefinitionSchema.extend({
|
|
188
196
|
repoRoot: optionalRepoRoot,
|
|
189
197
|
preparation: z.boolean().optional(),
|
|
190
198
|
}),
|
|
191
199
|
}, async (input, context) => await askQuestionnaire(server, service, input, context));
|
|
192
200
|
server.registerTool('questionnaire.resume', {
|
|
193
|
-
description: 'Display the original durable questionnaire after interruption or return its saved accepted choice. No timeout or dismissal resolves a pending question. A saved free-text receipt cannot replay the text and must never be treated as a recovered answer.',
|
|
201
|
+
description: 'Display the original durable questionnaire after interruption or return its saved accepted choice. No timeout or dismissal resolves a pending question. A saved free-text receipt cannot replay the text and must never be treated as a recovered answer. presentation host_native returns the original question and bindings for a permitted blocking host control without reopening the MCP form.',
|
|
194
202
|
inputSchema: z.strictObject({
|
|
195
203
|
repoRoot: optionalRepoRoot,
|
|
196
204
|
questionnaireId: z.string().regex(/^[A-Za-z0-9_-]{1,100}$/),
|
|
197
205
|
preparation: z.boolean().optional(),
|
|
206
|
+
presentation: z.literal('host_native').optional(),
|
|
198
207
|
}),
|
|
199
208
|
}, async (input, context) => await resumeQuestionnaire(server, service, input, context));
|
|
200
209
|
}
|
|
@@ -66,7 +66,18 @@ export class BridgeService {
|
|
|
66
66
|
if (stableStringify(scope) !== stableStringify(input.scope)) {
|
|
67
67
|
throw refuse('The account or repository binding changed while the questionnaire was open.', 'session.entry');
|
|
68
68
|
}
|
|
69
|
-
return await this.dependencies.questionnaires.accept(scope, input.questionnaireId, input.requestKey, input.answer);
|
|
69
|
+
return await this.dependencies.questionnaires.accept(scope, input.questionnaireId, input.requestKey, input.answer, input.answerSource);
|
|
70
|
+
}
|
|
71
|
+
async questionnaireAnswerFromHost(input) {
|
|
72
|
+
const record = await this.questionnaireResume(input);
|
|
73
|
+
if (record.requestKey !== input.requestKey || record.contentHash !== input.contentHash) {
|
|
74
|
+
throw refuse('The host answer belongs to a different question or content. Read the original with questionnaire.resume; do not reuse this answer for a changed decision.', 'questionnaire.resume');
|
|
75
|
+
}
|
|
76
|
+
return this.questionnaireAccept({
|
|
77
|
+
...input,
|
|
78
|
+
scope: record.scope,
|
|
79
|
+
answerSource: { kind: 'host_native_relay', hostTool: input.hostTool },
|
|
80
|
+
});
|
|
70
81
|
}
|
|
71
82
|
async questionnaireScope(repoRoot, preparation = false) {
|
|
72
83
|
await this.dependencies.principalState?.ensure();
|
|
@@ -643,7 +654,10 @@ export class BridgeService {
|
|
|
643
654
|
localPendingDeliveryCount: localJournal.pendingDeliveryCount,
|
|
644
655
|
conflicts,
|
|
645
656
|
requiresAttention: conflicts.length > 0,
|
|
646
|
-
editLeaseAllowed:
|
|
657
|
+
editLeaseAllowed: backendTask.status !== 'closed' &&
|
|
658
|
+
backendTask.status !== 'abandoned' &&
|
|
659
|
+
backendSession.status !== 'closed' &&
|
|
660
|
+
(conflicts.length === 0 || offlineDevelopmentAllowed),
|
|
647
661
|
offlineDevelopmentOnly: offlineDevelopmentAllowed,
|
|
648
662
|
recoveredVerification,
|
|
649
663
|
},
|
|
@@ -49,11 +49,26 @@ const storedQuestionSchema = questionnaireDefinitionSchema.extend({
|
|
|
49
49
|
requestKey: z.string().regex(/^questionnaire_[0-9a-f]{64}$/),
|
|
50
50
|
createdAt: z.string().datetime(),
|
|
51
51
|
});
|
|
52
|
+
export const hostAnswerSchema = z.strictObject({
|
|
53
|
+
questionnaireId: identifier,
|
|
54
|
+
requestKey: z.string().regex(/^questionnaire_[0-9a-f]{64}$/),
|
|
55
|
+
contentHash: z.string().regex(/^[0-9a-f]{64}$/),
|
|
56
|
+
hostTool: z.enum(['AskUserQuestion', 'request_user_input']),
|
|
57
|
+
answer: z.strictObject({
|
|
58
|
+
choice: z.string().min(1).max(80),
|
|
59
|
+
text: z.string().max(2000).optional(),
|
|
60
|
+
}),
|
|
61
|
+
});
|
|
62
|
+
const answerSourceSchema = z.strictObject({
|
|
63
|
+
kind: z.literal('host_native_relay'),
|
|
64
|
+
hostTool: z.enum(['AskUserQuestion', 'request_user_input']),
|
|
65
|
+
});
|
|
52
66
|
const storedAnswerSchema = z.strictObject({
|
|
53
67
|
requestKey: z.string().regex(/^questionnaire_[0-9a-f]{64}$/),
|
|
54
68
|
answeredAt: z.string().datetime(),
|
|
55
69
|
answerAvailable: z.boolean(),
|
|
56
70
|
withdrawn: z.literal(true).optional(),
|
|
71
|
+
answerSource: answerSourceSchema.optional(),
|
|
57
72
|
answer: z.strictObject({ choice: z.string(), text: z.string().optional() }).optional(),
|
|
58
73
|
});
|
|
59
74
|
export function questionnaireAnswerSchema(record) {
|
|
@@ -194,7 +209,9 @@ export class QuestionnaireStore {
|
|
|
194
209
|
}
|
|
195
210
|
return records.sort((left, right) => left.createdAt.localeCompare(right.createdAt));
|
|
196
211
|
}
|
|
197
|
-
async accept(scope, questionnaireId, requestKey, input) {
|
|
212
|
+
async accept(scope, questionnaireId, requestKey, input, answerSource) {
|
|
213
|
+
if (answerSource)
|
|
214
|
+
answerSourceSchema.parse(answerSource);
|
|
198
215
|
const record = await this.get(scope, questionnaireId);
|
|
199
216
|
if (!record || record.requestKey !== requestKey)
|
|
200
217
|
throw new Error('Questionnaire response is stale or belongs to a different scope.');
|
|
@@ -209,6 +226,7 @@ export class QuestionnaireStore {
|
|
|
209
226
|
}
|
|
210
227
|
const receipt = {
|
|
211
228
|
requestKey,
|
|
229
|
+
...(answerSource ? { answerSource } : {}),
|
|
212
230
|
answeredAt: new Date().toISOString(),
|
|
213
231
|
answerAvailable: answer.choice !== '__other__',
|
|
214
232
|
...(answer.choice !== '__other__'
|
package/skill/SKILL.md
CHANGED
|
@@ -13,6 +13,8 @@ Mandatory behavior:
|
|
|
13
13
|
|
|
14
14
|
Use the host's native questionnaire for every question to the user, including implementation choices, names, clarification, branch/worktree decisions and delivery. For a required decision, read [questionnaires.md](references/questionnaires.md), then use `questionnaire.ask` to open a durable native MCP form and `questionnaire.resume` to return to the same unanswered question. Never use request_user_input_async for a required decision: it does not wait for an answer. When the durable form is unavailable, use a blocking native control only where the host permits it: request_user_input in Codex or AskUserQuestion in Claude. Never replace the questionnaire with a chat instruction such as 'type this', 'reply yes', or 'write X if you want Y'. Do not open a survey web page. If the required native control is unavailable or prohibited for that kind of question, follow the host's tool restrictions, explain the limitation, and continue only work already authorized; do not fabricate a survey or silently choose an answer. A timeout, dismissed form, empty response or ended turn is not an answer. Keep the decision pending and resume it; do not start dependent work or report it as resolved. Existing answers remain valid through retries and handoffs.
|
|
15
15
|
|
|
16
|
+
When a durable MCP form cannot be displayed, read its `hostFallback` or call `questionnaire.resume` with `presentation: host_native` to get the original question without reopening the MCP form. Display the same question, all choices and notices through a blocking native control only if the host permits that control for this decision. After an actual native answer, call `questionnaire.answer_from_host` with the unchanged questionnaireId, requestKey and contentHash, the hostTool name and the returned choice/text. This is an agent-reported relay, not MCP transport attestation. Then retry the owning operation; its authority, content and version checks still apply. Never relay prose consent, a default, an asynchronous response or a cancelled/declined/missing answer. Decline alone is not proof that the host cannot display forms. Keep the decision pending if no permitted native control can represent it.
|
|
17
|
+
|
|
16
18
|
Do not block independent task work on `memory.propose_revision` drafting, submission or approval. Follow [memory-updates.md](references/memory-updates.md): use background agents or concurrent tools when the host permits them and continue useful work; without concurrency, checkpoint the pending draft and defer submission until needed. Wait only at the operation that depends on the proposal or revision. This scheduling rule applies to every project and AI host, while native approval and required task verification remain in force. Apply the same dependency rule to reconciliation, self-review, scaffold receipts, inventory upload, completed-unit evidence and test-result reporting. A bridge result with deliveryStatus pending means durable local recording, so continue independent work without polling or resending. Keep claim acquisition, required approval, edit leases and final verification as real dependencies.
|
|
17
19
|
|
|
18
20
|
1. Discover the repository binding through `session.entry`. Bindings live in the user-level Engineering Memory state directory, outside the repository and installed runtime. No project settings file is required. For a bound repository, the project's knowledge is in the backend, so answer nothing about it before bootstrapping; the absence of local design files or records says nothing about its stored knowledge.
|
|
@@ -20,6 +20,30 @@ The host owns the visible form and may impose a transport deadline or close it w
|
|
|
20
20
|
|
|
21
21
|
Never replace the questionnaire with a chat instruction such as 'type this', 'reply yes', or 'write X if you want Y'. Do not open a survey web page. Questions and saved answers must not contain credentials, personal data or production payloads. Authentication secrets belong in the existing browser authentication flow. Use an appropriate permitted native control for information that cannot be persisted; do not put it into a durable question's title, options or stored answer.
|
|
22
22
|
|
|
23
|
+
## Host-native answer relay
|
|
24
|
+
|
|
25
|
+
When a durable MCP form cannot be displayed, read its `hostFallback` or call `questionnaire.resume` with `presentation: host_native` to get the original question without reopening the MCP form. Display the same question, all choices and notices through a blocking native control only if the host permits that control for this decision. After an actual native answer, call `questionnaire.answer_from_host` with the unchanged questionnaireId, requestKey and contentHash, the hostTool name and the returned choice/text. This is an agent-reported relay, not MCP transport attestation. Then retry the owning operation; its authority, content and version checks still apply. Never relay prose consent, a default, an asynchronous response or a cancelled/declined/missing answer. Decline alone is not proof that the host cannot display forms. Keep the decision pending if no permitted native control can represent it.
|
|
26
|
+
|
|
27
|
+
For branch preferences, each role remains a separate decision bound to the exact preference
|
|
28
|
+
value and expectedVersion. A combined "yes to all branches" from a different question cannot
|
|
29
|
+
be relayed into three independent forms. Finish each original role question, then retry
|
|
30
|
+
project.set_git_preferences with its original arguments. The backend applies the group only
|
|
31
|
+
when every supplied role is approved and the version and authority still match. A concurrent
|
|
32
|
+
edit requires reading current preferences and fresh decisions for the new version.
|
|
33
|
+
|
|
34
|
+
Map the native result to the original option id only when the selected label identifies it
|
|
35
|
+
unambiguously; never substitute or omit choices to fit a host limit. If all choices and required
|
|
36
|
+
text cannot be represented by a permitted blocking control, leave the original question pending.
|
|
37
|
+
Do not use this relay to evade host approval restrictions or a user declining the owning action.
|
|
38
|
+
With a saved explicit defer, use a new decisionAttempt only after the user chooses to reconsider.
|
|
39
|
+
|
|
40
|
+
The relay stores host-native provenance beside the atomic answer. It does not store transcripts,
|
|
41
|
+
personal identities or host tool-call logs. Bounded decision text follows the existing privacy
|
|
42
|
+
checks; unrestricted Other text is returned once and cannot be recovered after restart. Installed
|
|
43
|
+
clients must restart after an upgrade to read the new relay receipt field; old receipts remain
|
|
44
|
+
readable by the new client. No server or SDK can independently prove an agent's claim that a
|
|
45
|
+
host-native form was shown; agents must use only the actual native result they observed.
|
|
46
|
+
|
|
23
47
|
## Authentication
|
|
24
48
|
|
|
25
49
|
When no Engineering Memory session exists, ask whether to sign in, create an account, or skip Engineering Memory for this task. Explain that skip is permitted only for an unbound repository. After sign in or sign up is selected, let the bridge open the browser authentication flow. Never ask for a password in the native questionnaire or chat. If a browser link is expired, rejected, already used, or otherwise unusable, call `auth.signin_browser` with `restart: true` and present only the newly returned URL.
|