engineering-memory 1.11.8 → 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.
Files changed (39) hide show
  1. package/dispatcher/sections.mjs +7 -3
  2. package/install/codex-approval.mjs +457 -0
  3. package/install/files.mjs +119 -9
  4. package/install/installer.mjs +44 -5
  5. package/install/launcher.mjs +95 -0
  6. package/package.json +1 -1
  7. package/runtime/build.json +1 -1
  8. package/runtime/dist/src/config.js +4 -0
  9. package/runtime/dist/src/git/git-inspector.js +82 -3
  10. package/runtime/dist/src/git/verification-gate.js +30 -17
  11. package/runtime/dist/src/index.js +25 -81
  12. package/runtime/dist/src/mcp/onboarding-tools.js +3 -16
  13. package/runtime/dist/src/mcp/questionnaire-tools.js +99 -15
  14. package/runtime/dist/src/mcp/server.js +14 -7
  15. package/runtime/dist/src/mcp/tool-annotations.js +111 -0
  16. package/runtime/dist/src/mcp/tool-definitions.js +148 -28
  17. package/runtime/dist/src/mcp/tool-host.js +12 -0
  18. package/runtime/dist/src/mcp/worktree-tools.js +323 -236
  19. package/runtime/dist/src/project/repository.js +11 -5
  20. package/runtime/dist/src/runtime/active-context-store.js +3 -0
  21. package/runtime/dist/src/runtime/branch-preferences.js +0 -19
  22. package/runtime/dist/src/runtime/bridge-service.js +1163 -231
  23. package/runtime/dist/src/runtime/create-bridge-service.js +82 -0
  24. package/runtime/dist/src/runtime/offline-outbox.js +33 -158
  25. package/runtime/dist/src/runtime/phase-timer.js +26 -0
  26. package/runtime/dist/src/runtime/privacy-detector.js +263 -0
  27. package/runtime/dist/src/runtime/questionnaire-store.js +210 -49
  28. package/runtime/dist/src/runtime/recovery-error.js +3 -1
  29. package/runtime/dist/src/runtime/runtime-entry.js +24 -0
  30. package/runtime/dist/src/runtime/runtime-host.js +263 -0
  31. package/runtime/dist/src/runtime/task-branch-store.js +17 -1
  32. package/runtime/dist/src/runtime/task-start.js +281 -0
  33. package/runtime/dist/src/runtime/worktree-pool.js +340 -24
  34. package/runtime/dist/src/utilities/process.js +1 -0
  35. package/skill/SKILL.md +3 -3
  36. package/skill/references/lifecycle.md +44 -19
  37. package/skill/references/memory-updates.md +9 -3
  38. package/skill/references/project-onboarding.md +19 -2
  39. package/skill/references/questionnaires.md +77 -15
@@ -1,13 +1,26 @@
1
1
  import { acceptedContent, CLIENT_CAPABILITIES_META_KEY, inputRequired, inputResponse, } from '@modelcontextprotocol/server';
2
2
  import { questionnaireAnswerSchema, } from '../runtime/questionnaire-store.js';
3
- export async function askQuestionnaire(server, service, input, context, previousDefinitions = []) {
3
+ export async function askQuestionnaire(server, service, input, context, previousDefinitions = [], owner) {
4
4
  try {
5
- return await present(server, service, await service.questionnaireAsk(input, previousDefinitions), input.repoRoot, context, input.preparation);
5
+ return await present(server, service, await service.questionnaireAsk(input, previousDefinitions, owner), input.repoRoot, context, input.preparation, input.presentation);
6
6
  }
7
7
  catch (error) {
8
8
  return failure(error);
9
9
  }
10
10
  }
11
+ export function answerChoice(result) {
12
+ return answerData(result)?.choice;
13
+ }
14
+ export function answerData(result) {
15
+ const response = result;
16
+ const text = response.content?.find((item) => item.type === 'text')?.text;
17
+ if (!text)
18
+ return undefined;
19
+ const value = JSON.parse(text);
20
+ return value.ok && value.data?.status === 'answered' && value.data.answerAvailable
21
+ ? value.data.answer
22
+ : undefined;
23
+ }
11
24
  export async function resumeQuestionnaire(server, service, input, context) {
12
25
  try {
13
26
  return await present(server, service, await service.questionnaireResume(input), input.repoRoot, context, input.preparation, input.presentation);
@@ -19,7 +32,7 @@ export async function resumeQuestionnaire(server, service, input, context) {
19
32
  export async function answerQuestionnaireFromHost(service, input) {
20
33
  try {
21
34
  const resolved = await service.questionnaireAnswerFromHost(input);
22
- return answered(resolved.record, resolved.answer, resolved.replayed);
35
+ return answered(resolved.record, resolved.answer, resolved.replayed, resolved.retry);
23
36
  }
24
37
  catch (error) {
25
38
  return failure(error);
@@ -33,8 +46,11 @@ async function present(server, service, record, repoRoot, context, preparation,
33
46
  answerAvailable: false,
34
47
  nextAction: 'This decision was explicitly withdrawn. Do not continue dependent work. Use a new questionnaireId or onboarding requestKey only if the user resumes that work.',
35
48
  });
36
- if (record.status === 'answered')
37
- return answered(record, record.answer, true);
49
+ if (record.status === 'answered') {
50
+ if (inputResponse(context.mcpReq.inputResponses, record.requestKey).kind !== 'missing')
51
+ consumedResponses(context).add(record.requestKey);
52
+ return answered(record, record.questions ? record.answers : record.answer, true);
53
+ }
38
54
  if (context.mcpReq.signal.aborted)
39
55
  return pending(record, 'interrupted');
40
56
  if (presentation === 'host_native')
@@ -42,22 +58,27 @@ async function present(server, service, record, repoRoot, context, preparation,
42
58
  const responses = context.mcpReq.inputResponses;
43
59
  const response = inputResponse(responses, record.requestKey);
44
60
  if (response.kind === 'elicit' && response.action === 'accept') {
45
- const answer = acceptedContent(responses, record.requestKey, questionnaireAnswerSchema(record));
61
+ const content = acceptedContent(responses, record.requestKey);
62
+ const parsed = questionnaireAnswerSchema(record).safeParse(record.questions && content ? formAnswers(record, content) : content);
63
+ const answer = parsed.success ? parsed.data : undefined;
46
64
  if (!answer)
47
65
  return pending(record, 'invalid_response');
66
+ consumedResponses(context).add(record.requestKey);
48
67
  const resolved = await service.questionnaireAccept({
49
68
  repoRoot,
50
69
  preparation,
51
70
  questionnaireId: record.questionnaireId,
52
71
  requestKey: record.requestKey,
53
72
  scope: record.scope,
54
- answer,
73
+ answer: answer,
55
74
  });
56
75
  return answered(resolved.record, resolved.answer, resolved.replayed);
57
76
  }
58
77
  if (response.kind === 'elicit')
59
78
  return pending(record, response.action);
60
- if (responses !== undefined || context.mcpReq.droppedInputResponseKeys?.length)
79
+ if (response.kind !== 'missing' ||
80
+ context.mcpReq.droppedInputResponseKeys?.length ||
81
+ Object.keys(responses ?? {}).some((key) => !consumedResponses(context).has(key)))
61
82
  return pending(record, 'missing_or_stale_response');
62
83
  const envelope = context.mcpReq.envelope;
63
84
  const capabilities = envelope?.[CLIENT_CAPABILITIES_META_KEY] ?? server.server.getClientCapabilities();
@@ -65,6 +86,35 @@ async function present(server, service, record, repoRoot, context, preparation,
65
86
  if (!elicitation || (!elicitation.form && elicitation.url !== undefined))
66
87
  return pending(record, 'native_form_unavailable');
67
88
  const copy = questionnaireCopy[record.language ?? 'en'];
89
+ if (record.questions) {
90
+ const properties = {};
91
+ const required = [];
92
+ for (const sub of record.questions) {
93
+ properties[sub.id] = {
94
+ type: 'string',
95
+ title: sub.message,
96
+ enum: sub.options.map((option) => option.id),
97
+ enumNames: sub.options.map((option) => option.label),
98
+ };
99
+ required.push(sub.id);
100
+ if (sub.textField)
101
+ properties[`${sub.id}_text`] = {
102
+ type: 'string',
103
+ title: sub.textField.title,
104
+ minLength: 1,
105
+ maxLength: sub.textField.maxLength,
106
+ };
107
+ }
108
+ return inputRequired({
109
+ inputRequests: {
110
+ [record.requestKey]: inputRequired.elicit({
111
+ mode: 'form',
112
+ message: presentationMessage(record),
113
+ requestedSchema: { type: 'object', properties, required },
114
+ }),
115
+ },
116
+ });
117
+ }
68
118
  return inputRequired({
69
119
  inputRequests: {
70
120
  [record.requestKey]: inputRequired.elicit({
@@ -112,11 +162,18 @@ async function present(server, service, record, repoRoot, context, preparation,
112
162
  },
113
163
  });
114
164
  }
165
+ const consumed = new WeakMap();
166
+ function consumedResponses(context) {
167
+ const keys = consumed.get(context.mcpReq) ?? new Set();
168
+ consumed.set(context.mcpReq, keys);
169
+ return keys;
170
+ }
115
171
  const questionnaireCopy = {
116
172
  en: {
117
173
  pending: 'Choose an answer to resolve this decision. Closing or cancelling the form leaves it pending.',
118
174
  freeText: ' For another answer choose Other and fill in text. Free text is not stored and cannot be replayed.',
119
175
  storedText: ' The bounded text field is stored as part of this exact decision and is replayed after restart.',
176
+ multi: ' AskUserQuestion takes up to 4 questions per call. Show every question in as few calls as needed, then relay all answers in one questionnaire.answer_from_host.',
120
177
  answer: 'Answer',
121
178
  other: 'Other',
122
179
  otherAnswer: 'Other answer',
@@ -125,17 +182,33 @@ const questionnaireCopy = {
125
182
  pending: 'Kararını kaydetmek için bir seçenek seç. Formu kapatmak veya iptal etmek onay sayılmaz; soru beklemede kalır.',
126
183
  freeText: ' Farklı bir yanıt için Diğer seçeneğini seçip metin alanını doldur. Serbest yanıt saklanmaz ve daha sonra geri getirilemez.',
127
184
  storedText: ' Bu metin alanındaki yanıt kararınla birlikte saklanır; yeniden başlattığında korunur.',
185
+ multi: ' AskUserQuestion tek çağrıda en fazla 4 soru alır. Her soruyu en az çağrıyla göster, sonra tüm yanıtları tek bir questionnaire.answer_from_host çağrısında ilet.',
128
186
  answer: 'Yanıt',
129
187
  other: 'Diğer',
130
188
  otherAnswer: 'Diğer yanıt',
131
189
  },
132
190
  };
191
+ function formAnswers(record, content) {
192
+ const answers = {};
193
+ for (const sub of record.questions ?? []) {
194
+ if (content[sub.id] === undefined)
195
+ continue;
196
+ const text = content[`${sub.id}_text`];
197
+ answers[sub.id] = {
198
+ choice: content[sub.id],
199
+ ...(text !== undefined && sub.textField?.requiredForChoice === content[sub.id]
200
+ ? { text }
201
+ : {}),
202
+ };
203
+ }
204
+ return answers;
205
+ }
133
206
  function presentationMessage(record) {
134
207
  const notice = /^memory-batch-[a-f0-9]{64}-[0-9]+$/.test(record.questionnaireId)
135
208
  ? '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
209
  : undefined;
137
210
  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 : ''}`;
211
+ return `${record.message}${notice ? `\n\n${notice}` : ''}\n\n${copy.pending}${record.allowFreeText ? copy.freeText : ''}${record.textField ? copy.storedText : ''}${record.questions ? copy.multi : ''}`;
139
212
  }
140
213
  function pending(record, reason) {
141
214
  return result({
@@ -149,25 +222,36 @@ function pending(record, reason) {
149
222
  requestKey: record.requestKey,
150
223
  contentHash: record.contentHash,
151
224
  message: presentationMessage(record),
152
- options: record.options,
153
225
  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.',
226
+ ...(record.questions
227
+ ? {
228
+ questions: record.questions,
229
+ ...(record.terminalChoices ? { terminalChoices: record.terminalChoices } : {}),
230
+ }
231
+ : {
232
+ options: record.options,
233
+ allowFreeText: record.allowFreeText,
234
+ ...(record.textField ? { textField: record.textField } : {}),
235
+ }),
236
+ instructions: record.questions
237
+ ? 'Only if the host permits a blocking native control for this decision, display every question, its options and notices in AskUserQuestion or request_user_input, one call at a time if needed. Relay every answer together in one questionnaire.answer_from_host call as `answers`, keyed by question id. Closing, declining, timeout, prose consent and missing answers are not native answers for any question. 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.'
238
+ : '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
239
  },
158
240
  nextAction: reason === 'native_form_unavailable'
159
241
  ? '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.'
160
242
  : '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.',
161
243
  });
162
244
  }
163
- function answered(record, answer, replayed) {
245
+ function answered(record, answer, replayed, retry) {
246
+ const key = record.questions ? 'answers' : 'answer';
164
247
  return result({
165
248
  questionnaireId: record.questionnaireId,
166
249
  status: 'answered',
167
250
  answerAvailable: answer !== undefined,
168
- ...(answer ? { answer } : {}),
251
+ ...(answer ? { [key]: answer } : {}),
169
252
  replayed,
170
253
  ...(record.answerSource ? { answerSource: record.answerSource } : {}),
254
+ ...(retry ? { retry } : {}),
171
255
  ...(!answer
172
256
  ? {
173
257
  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.',
@@ -2,17 +2,24 @@ import { registerWorktreeTools } from './worktree-tools.js';
2
2
  import { McpServer } from '@modelcontextprotocol/server';
3
3
  import { registerOnboardingTools } from './onboarding-tools.js';
4
4
  import { registerEngineeringMemoryTools, registerQuestionnaireTools } from './tool-definitions.js';
5
- export function createEngineeringMemoryServer(service) {
6
- const server = new McpServer({
5
+ export const serverInstructions = 'Start with session.entry and follow the returned lifecycle. Do not block independent task work on memory.propose_revision drafting, submission or approval. If the host permits background agents or concurrent calls, dispatch independent proposals and continue useful work; otherwise checkpoint the draft and defer submission until needed. Checkpoint, correction, reconciliation, self-review and scaffold receipts use the durable local outbox; after deliveryStatus pending, continue independent work without polling or resending. Inventory upload, claim-bound evidence submission and test-result reporting may overlap independent analysis using host-permitted concurrent calls. Wait only when the next operation needs the result; required task deliveries must settle before verification. Preserve task/source identity and native user approval; never treat pending work as verified.';
6
+ export function createMcpServer() {
7
+ return new McpServer({
7
8
  name: 'engineering-memory',
8
9
  version: '0.1.0',
9
10
  }, {
10
- instructions: 'Start with session.entry and follow the returned lifecycle. Do not block independent task work on memory.propose_revision drafting, submission or approval. If the host permits background agents or concurrent calls, dispatch independent proposals and continue useful work; otherwise checkpoint the draft and defer submission until needed. Checkpoint, correction, reconciliation, self-review and scaffold receipts use the durable local outbox; after deliveryStatus pending, continue independent work without polling or resending. Inventory upload, claim-bound evidence submission and test-result reporting may overlap independent analysis using host-permitted concurrent calls. Wait only when the next operation needs the result; required task deliveries must settle before verification. Preserve task/source identity and native user approval; never treat pending work as verified.',
11
+ instructions: serverInstructions,
11
12
  });
12
- registerEngineeringMemoryTools(server, service);
13
- registerQuestionnaireTools(server, service);
14
- registerOnboardingTools(server, service);
15
- registerWorktreeTools(server, service);
13
+ }
14
+ export function registerAllTools(host, service) {
15
+ registerEngineeringMemoryTools(host, service);
16
+ registerQuestionnaireTools(host, service);
17
+ registerOnboardingTools(host, service);
18
+ registerWorktreeTools(host, service);
19
+ }
20
+ export function createEngineeringMemoryServer(service) {
21
+ const server = createMcpServer();
22
+ registerAllTools(server, service);
16
23
  return server;
17
24
  }
18
25
  //# sourceMappingURL=server.js.map
@@ -0,0 +1,111 @@
1
+ const read = { readOnlyHint: true, openWorldHint: false }; // receipts and registry upkeep still read
2
+ const write = {
3
+ readOnlyHint: false,
4
+ destructiveHint: false,
5
+ openWorldHint: false,
6
+ };
7
+ const repeatableWrite = { ...write, idempotentHint: true };
8
+ const destructive = {
9
+ readOnlyHint: false,
10
+ destructiveHint: true,
11
+ openWorldHint: false,
12
+ };
13
+ const outward = {
14
+ readOnlyHint: false,
15
+ destructiveHint: false,
16
+ openWorldHint: true,
17
+ };
18
+ export const toolAnnotations = {
19
+ 'project.git_preferences': read,
20
+ 'project.set_git_preferences': write,
21
+ 'worktree.policy': read,
22
+ 'worktree.set_policy': write,
23
+ 'worktree.list': read,
24
+ 'worktree.reconcile': destructive, // releaseUnowned hands a recovered checkout back
25
+ 'task.heartbeat': repeatableWrite,
26
+ 'task.pause': write,
27
+ 'worktree.release': destructive,
28
+ 'audit.list': read,
29
+ 'questionnaire.ask': repeatableWrite,
30
+ 'questionnaire.resume': repeatableWrite, // records an accepted answer
31
+ 'questionnaire.answer_from_host': repeatableWrite,
32
+ 'questionnaire.withdraw': destructive,
33
+ 'session.entry': read,
34
+ 'session.set_decision': repeatableWrite,
35
+ 'session.decline_update': repeatableWrite,
36
+ 'session.answer_shadow_notice': repeatableWrite,
37
+ 'session.bootstrap': write,
38
+ 'session.resume': write,
39
+ 'context.prepare_change': write,
40
+ 'context.refresh': write,
41
+ 'project.inspect': read,
42
+ 'project.onboard': write,
43
+ 'project.onboarding_status': read,
44
+ 'project.rework_plan': write,
45
+ 'project.initialize': write,
46
+ 'memory.source': read,
47
+ 'memory.sync_start': repeatableWrite,
48
+ 'memory.sync_status': read,
49
+ 'memory.sync_inventory': write,
50
+ 'memory.sync_plan': write,
51
+ 'memory.sync_next': write,
52
+ 'memory.sync_submit': write,
53
+ 'memory.sync_reopen': write,
54
+ 'memory.sync_delta': read,
55
+ 'memory.sync_verify': repeatableWrite,
56
+ 'memory.catalog': read,
57
+ 'memory.sources': read,
58
+ 'memory.source_targets': read,
59
+ 'memory.publish_task': write,
60
+ 'memory.read_revisions': read,
61
+ 'memory.review_batch': write,
62
+ 'memory.query': read,
63
+ 'memory.history': read,
64
+ 'memory.propose_revision': write,
65
+ 'memory.list_proposals': read,
66
+ 'memory.review_proposal': write,
67
+ 'task.checkpoint': write,
68
+ 'task.record_correction': write,
69
+ 'task.self_review': write,
70
+ 'task.reconcile': write,
71
+ 'task.resolve_pending_delivery': destructive, // discards a pending delivery
72
+ 'task.verify': write,
73
+ 'task.close': write,
74
+ 'task.abandon': destructive,
75
+ 'task.branch': outward, // fetches the chosen base from the Git remote
76
+ 'architecture.plan': read,
77
+ 'architecture.module': read,
78
+ 'architecture.record_application': write,
79
+ 'organization.list': read,
80
+ 'organization.create': write,
81
+ 'organization.update': write,
82
+ 'organization.member_list': read,
83
+ 'organization.member_upsert': write,
84
+ 'project.setup': write,
85
+ 'project.list': read,
86
+ 'project.resolve': repeatableWrite, // binds an explicitly selected project
87
+ 'project.archive': destructive,
88
+ 'project.restore': write,
89
+ 'project.member_add': write,
90
+ 'project.member_list': read,
91
+ 'work_item.create': write,
92
+ 'work_item.update': write,
93
+ 'work_item.archive': destructive,
94
+ 'work_item.restore': write,
95
+ 'work_item.runs': read,
96
+ 'work_item.record_test': write,
97
+ 'work_item.test_results': read,
98
+ 'work_item.plan': read,
99
+ 'work_item.confirm_plan': write,
100
+ 'project.update': write,
101
+ 'work_item.list': read,
102
+ 'work_item.get': read,
103
+ 'project.clone': outward,
104
+ 'project.link': write,
105
+ 'project.unlink': destructive,
106
+ 'project.links': read,
107
+ 'auth.status': read,
108
+ 'auth.signin_browser': outward,
109
+ 'auth.logout': destructive,
110
+ };
111
+ //# sourceMappingURL=tool-annotations.js.map