engineering-memory 1.11.6 → 1.11.8

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.
@@ -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.6",
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.8",
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
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "gitHead": "eefcf3aac9c83ba9d0126d05309ca100947317d3"
3
+ }
@@ -50,9 +50,9 @@ export class VerificationGate {
50
50
  }, this.root);
51
51
  }
52
52
  async rebindWorktreeOwnership(repoRoot, taskId, previous, generation) {
53
- const fingerprint = await this.git.fingerprint(repoRoot);
54
- const identity = worktreeIdentity(await this.git.findRoot(repoRoot));
55
- const path = this.pathFor(fingerprint, identity, taskId);
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 present(server, service, record, repoRoot, context, preparation) {
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: `${record.message}${presentationNotice ? `\n\n${presentationNotice}` : ''}\n\n${copy.pending}${record.allowFreeText ? copy.freeText : ''}${record.textField ? copy.storedText : ''}`,
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 a supported native host and questionnaire.resume to answer; do not silently substitute an asynchronous question.'
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 tool arguments. 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.',
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();
@@ -388,9 +399,15 @@ export class BridgeService {
388
399
  (repository.projectId && repository.projectId !== projectId)) {
389
400
  throw refuse('The live task belongs to a different project than the one this repository is bound to.', 'project.resolve');
390
401
  }
391
- if (pointer?.worktreeGeneration && this.dependencies.worktreePool) {
402
+ const adoptable = pointer &&
403
+ !pointer.worktreeGeneration &&
404
+ normalizeTaskMode(pointer.mode) !== 'read_only' &&
405
+ this.dependencies.worktreePool
406
+ ? await this.dependencies.worktreePool.find(projectId, taskSlug, repository.repoRoot)
407
+ : null;
408
+ if (pointer && this.dependencies.worktreePool && (pointer.worktreeGeneration || adoptable)) {
392
409
  const pool = this.dependencies.worktreePool;
393
- const previous = await pool.find(projectId, taskSlug, repository.repoRoot);
410
+ const previous = adoptable ?? (await pool.find(projectId, taskSlug, repository.repoRoot));
394
411
  if (previous && previous.repoRoot !== repository.repoRoot)
395
412
  return asJsonValue({
396
413
  worktreeResumeRequired: true,
@@ -400,8 +417,11 @@ export class BridgeService {
400
417
  nextAction: 'Call session.resume from this returned repoRoot and use it for every subsequent command.',
401
418
  });
402
419
  const sourceCommit = previous?.baseCommit ?? pointer.source?.sourceCommit;
420
+ const branch = pointer.worktreeGeneration
421
+ ? pointer.branch
422
+ : (previous?.branch ?? pointer.branch);
403
423
  if ((!sourceCommit && !(previous && previous.baseCommit === null && !previous.managed)) ||
404
- pointer.branch === undefined)
424
+ branch === undefined)
405
425
  throw refuse('The task branch and base must be restored before resuming its worktree.', 'worktree.reconcile');
406
426
  const allocation = await pool.allocate({
407
427
  projectId,
@@ -409,7 +429,7 @@ export class BridgeService {
409
429
  repoFingerprint: repository.repoFingerprint,
410
430
  repoRoot: repository.repoRoot,
411
431
  externalTaskId: taskSlug,
412
- branch: pointer.branch,
432
+ branch,
413
433
  baseCommit: sourceCommit ?? null,
414
434
  keepCurrent: previous ? !previous.managed : false,
415
435
  resume: true,
@@ -422,6 +442,7 @@ export class BridgeService {
422
442
  ...pointer,
423
443
  worktreeId: sha256(allocation.repoRoot),
424
444
  worktreeGeneration: allocation.generation,
445
+ ...(branch !== undefined ? { branch } : {}),
425
446
  };
426
447
  await this.dependencies.activeContexts.save(pointer);
427
448
  if (allocation.repoRoot !== repository.repoRoot)
@@ -643,7 +664,10 @@ export class BridgeService {
643
664
  localPendingDeliveryCount: localJournal.pendingDeliveryCount,
644
665
  conflicts,
645
666
  requiresAttention: conflicts.length > 0,
646
- editLeaseAllowed: conflicts.length === 0 || offlineDevelopmentAllowed,
667
+ editLeaseAllowed: backendTask.status !== 'closed' &&
668
+ backendTask.status !== 'abandoned' &&
669
+ backendSession.status !== 'closed' &&
670
+ (conflicts.length === 0 || offlineDevelopmentAllowed),
647
671
  offlineDevelopmentOnly: offlineDevelopmentAllowed,
648
672
  recoveredVerification,
649
673
  },
@@ -1870,11 +1894,26 @@ export class BridgeService {
1870
1894
  const pool = this.dependencies.worktreePool;
1871
1895
  if (!projectId || !pool)
1872
1896
  throw refuse('Resolve the project with a pool-capable client.', 'project.resolve');
1873
- if (operation === 'list' || operation === 'reconcile')
1897
+ if (operation === 'list' || operation === 'reconcile') {
1898
+ const managedRoot = await pool.managedRoot();
1874
1899
  return asJsonValue({
1875
1900
  items: await pool.reconcile(projectId, repository.repoRoot, await this.poolPolicy()),
1876
1901
  selected: await pool.forPath(projectId, repository.repoRoot),
1902
+ managedRoot,
1903
+ ...(managedRoot.problem || managedRoot.synced
1904
+ ? {
1905
+ notice: [
1906
+ ...(managedRoot.problem ? [managedRoot.problem] : []),
1907
+ ...(managedRoot.synced
1908
+ ? [
1909
+ `Managed worktrees are created under ${managedRoot.root}, which a cloud sync client watches, so every Git operation there is re-scanned and uploaded. To create new worktrees elsewhere, write {"root": "<absolute directory>"} to ${managedRoot.override}; it takes effect for the next allocation, and existing worktrees keep working where they are.`,
1910
+ ]
1911
+ : []),
1912
+ ].join(' '),
1913
+ }
1914
+ : {}),
1877
1915
  });
1916
+ }
1878
1917
  if (operation === 'release_unowned') {
1879
1918
  const entry = await pool.forPath(projectId, repository.repoRoot);
1880
1919
  if (!entry || !input.sourceCommit || !input.generation || !input.decisionId)
@@ -1960,6 +1999,21 @@ export class BridgeService {
1960
1999
  resume: Boolean(existing),
1961
2000
  }, await this.poolPolicy(true));
1962
2001
  this.poolAllocations.set(allocation.repoRoot, allocation);
2002
+ if (existing && allocation.repoRoot === repository.repoRoot) {
2003
+ const current = await this.dependencies.activeContexts.loadForSlug(repository.repoFingerprint, input.externalTaskId);
2004
+ if (current &&
2005
+ current.taskSlug === input.externalTaskId &&
2006
+ !current.worktreeGeneration &&
2007
+ normalizeTaskMode(current.mode) !== 'read_only')
2008
+ await this.dependencies.activeContexts.save({
2009
+ ...current,
2010
+ worktreeId: sha256(allocation.repoRoot),
2011
+ worktreeGeneration: allocation.generation,
2012
+ ...(current.branch === undefined && allocation.branch !== null
2013
+ ? { branch: allocation.branch }
2014
+ : {}),
2015
+ });
2016
+ }
1963
2017
  return asJsonValue({
1964
2018
  ...allocation,
1965
2019
  kept: !allocation.managed,
@@ -1973,11 +2027,10 @@ export class BridgeService {
1973
2027
  const entry = await pool.forPath(pointer.projectId, repository.repoRoot);
1974
2028
  if (!entry && !pointer.worktreeGeneration)
1975
2029
  return; // existing unmanaged tasks retain their legacy reservation
2030
+ if (entry && entry.externalTaskId !== pointer.taskSlug)
2031
+ throw refuse(`This directory is allocated to task ${entry.externalTaskId}. Resume this task from its own worktree, or reconcile the pool to release the directory.`, 'worktree.reconcile');
1976
2032
  const owned = this.poolAllocations.get(repository.repoRoot);
1977
- if (!entry ||
1978
- !owned ||
1979
- owned.generation !== pointer.worktreeGeneration ||
1980
- entry.externalTaskId !== pointer.taskSlug)
2033
+ if (!entry || !owned || owned.generation !== pointer.worktreeGeneration)
1981
2034
  throw refuse('Resume this task to acquire its worktree before writing or delivering.', 'session.resume');
1982
2035
  await pool.assertOwnership(pointer.projectId, repository.repoRoot, owned.generation);
1983
2036
  }
@@ -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__'
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { mkdir, readFile, readdir } from 'node:fs/promises';
3
3
  import { homedir } from 'node:os';
4
- import { dirname, join, resolve } from 'node:path';
4
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
5
5
  import * as z from 'zod/v4';
6
6
  import { GitInspector } from '../git/git-inspector.js';
7
7
  import { atomicWrite, assertManagedPath, canonicalPath, ensureManagedDirectory, pathExists, readJson, removeFile, safeSegment, } from '../utilities/files.js';
@@ -37,11 +37,29 @@ const allocationSchema = z.object({
37
37
  pendingDelivery: z.boolean(),
38
38
  recoveredWithoutReservation: z.boolean().optional(),
39
39
  reason: z.string().optional(),
40
+ root: z.string().optional(),
41
+ layout: z.union([z.literal(1), z.literal(2)]).optional(),
40
42
  });
41
43
  const registrySchema = z.object({
42
44
  schemaVersion: z.literal(1),
43
45
  projects: z.record(z.string(), z.object({ folder: z.string().regex(/^[a-zA-Z0-9._-]+$/), entries: z.array(allocationSchema) })),
44
46
  });
47
+ export function managedBase(root, layout) {
48
+ return layout === 1
49
+ ? join(root, 'engineeringmemory')
50
+ : join(root, 'engineering_memory', 'worktrees');
51
+ }
52
+ function projectDirectory(root, folder, layout) {
53
+ return layout === 1
54
+ ? join(root, 'engineeringmemory', folder, 'worktrees')
55
+ : join(root, 'engineering_memory', 'worktrees', folder);
56
+ }
57
+ function worktreeName(folder, slot, layout) {
58
+ return folder + (layout === 1 ? '-worktree' : '_worktree') + slot;
59
+ }
60
+ function managedPath(root, folder, slot, layout) {
61
+ return join(projectDirectory(root, folder, layout), worktreeName(folder, slot, layout));
62
+ }
45
63
  export class WorktreePool {
46
64
  stateRoot;
47
65
  ownerId;
@@ -53,6 +71,9 @@ export class WorktreePool {
53
71
  startTimes = new Map();
54
72
  ownStart;
55
73
  documents;
74
+ legacyDocuments;
75
+ overrideDocuments;
76
+ overrideProblem;
56
77
  before = new Map();
57
78
  pendingWork;
58
79
  constructor(stateRoot, options = {}) {
@@ -63,6 +84,7 @@ export class WorktreePool {
63
84
  this.now = options.now ?? Date.now;
64
85
  this.alive = options.processAlive ?? processAlive;
65
86
  this.documents = options.documentsRoot;
87
+ this.legacyDocuments = options.legacyDocumentsRoot ?? options.documentsRoot;
66
88
  this.pendingWork = options.pendingWork ?? (async () => false);
67
89
  this.registryPath = join(stateRoot, 'worktree-pool.json');
68
90
  }
@@ -79,8 +101,12 @@ export class WorktreePool {
79
101
  const commonDir = await this.commonDirectory(input.repoRoot);
80
102
  return this.transaction(async (registry) => {
81
103
  if (!registry.projects[input.projectId]) {
82
- const managedRoot = key(join(await this.documentsDirectory(), 'engineeringmemory'));
83
- const orphan = (await this.gitPaths(input.repoRoot)).find((p) => key(p.path).startsWith(managedRoot + (process.platform === 'win32' ? '\\' : '/')));
104
+ const separator = process.platform === 'win32' ? '\\' : '/';
105
+ const bases = [];
106
+ for (const root of await this.scanRoots())
107
+ for (const layout of [1, 2])
108
+ bases.push(key(managedBase(root, layout)) + separator);
109
+ const orphan = (await this.gitPaths(input.repoRoot)).find((p) => bases.some((base) => key(p.path).startsWith(base)));
84
110
  if (orphan)
85
111
  throw refuse('Existing managed Git worktrees have no project index. Run worktree.reconcile before allocating more directories.');
86
112
  }
@@ -107,7 +133,7 @@ export class WorktreePool {
107
133
  if (paths.some((p) => p.branch === entry.branch && key(p.path) !== key(entry.repoRoot)))
108
134
  throw refuse('The recorded branch is checked out at another location. Reconcile the moved worktree before resuming.');
109
135
  if (paths.some((p) => key(p.path) === key(entry.repoRoot))) {
110
- await assertManagedPath(await this.documentsDirectory(), entry.repoRoot, true);
136
+ await assertManagedPath(entry.root ?? (await this.legacyDocumentsDirectory()), entry.repoRoot, true);
111
137
  if (await pathExists(entry.repoRoot))
112
138
  throw refuse('The missing worktree reappeared; reconcile before recovery.');
113
139
  await this.command(input.repoRoot, ['worktree', 'remove', entry.repoRoot]);
@@ -151,8 +177,10 @@ export class WorktreePool {
151
177
  if (checkout)
152
178
  throw refuse('This branch already has a checkout. Resume its task instead of creating or resetting the branch.');
153
179
  let reusable;
180
+ const activeRoot = key(await this.documentsDirectory());
181
+ const legacyRoot = await this.legacyDocumentsDirectory();
154
182
  for (const candidate of project.entries
155
- .filter((e) => e.managed && e.commonDir === commonDir)
183
+ .filter((e) => e.managed && e.commonDir === commonDir && key(e.root ?? legacyRoot) === activeRoot)
156
184
  .sort((a, b) => a.slot - b.slot)) {
157
185
  if (candidate.pendingDelivery ||
158
186
  ['creating', 'quarantined'].includes(candidate.phase) ||
@@ -168,7 +196,7 @@ export class WorktreePool {
168
196
  if (entry) {
169
197
  const old = structuredClone(entry);
170
198
  await this.releaseReservation(old);
171
- const next = await this.entry(input, commonDir, old.repoRoot, old.slot, true);
199
+ const next = await this.entry(input, commonDir, old.repoRoot, old.slot, true, old.root ?? (await this.legacyDocumentsDirectory()), old.layout ?? 1);
172
200
  project.entries[project.entries.indexOf(entry)] = next;
173
201
  entry = next;
174
202
  await this.save(registry);
@@ -179,14 +207,15 @@ export class WorktreePool {
179
207
  if (project.entries.filter((e) => e.managed).length >= policy.maxWorktrees)
180
208
  throw refuse('The managed worktree limit is full. worktree.list explains protected directories; finish delivery or safely release a clean task. No files were deleted.');
181
209
  let slot = 1;
182
- const root = join(await this.documentsDirectory(), 'engineeringmemory', project.folder, 'worktrees');
183
- await ensureManagedDirectory(await this.documentsDirectory(), root);
210
+ const active = await this.documentsDirectory();
211
+ const root = projectDirectory(active, project.folder, 2);
212
+ await ensureManagedDirectory(active, root);
184
213
  while (project.entries.some((e) => e.slot === slot) ||
185
- (await pathExists(join(root, project.folder + '-worktree' + slot))))
214
+ (await pathExists(join(root, worktreeName(project.folder, slot, 2)))))
186
215
  slot++;
187
- const path = join(root, project.folder + '-worktree' + slot);
216
+ const path = join(root, worktreeName(project.folder, slot, 2));
188
217
  await assertManagedPath(root, path, true);
189
- entry = await this.entry(input, commonDir, path, slot, true);
218
+ entry = await this.entry(input, commonDir, path, slot, true, await this.documentsDirectory(), 2);
190
219
  project.entries.push(entry);
191
220
  await this.save(registry);
192
221
  await this.git.createWorktree(input.repoRoot, input.branch, path, input.baseCommit);
@@ -326,24 +355,19 @@ export class WorktreePool {
326
355
  let project = registry.projects[projectId];
327
356
  const locations = await this.gitPaths(repoRoot);
328
357
  const candidates = [];
329
- const managedRoot = join(await this.documentsDirectory(), 'engineeringmemory');
358
+ const managedRoots = await this.scanRoots();
330
359
  for (const location of locations) {
331
360
  if (project?.entries.some((entry) => key(entry.repoRoot) === key(location.path)))
332
361
  continue;
333
362
  if (!(await pathExists(location.path)))
334
363
  continue;
335
364
  const parent = dirname(location.path);
336
- const folder = dirname(parent).split(/[\\/]/).pop();
337
365
  const name = location.path.split(/[\\/]/).pop();
338
- if (key(parent) !== key(join(managedRoot, folder, 'worktrees')))
339
- continue;
340
- const prefix = folder + '-worktree';
341
- const slot = Number(name.slice(prefix.length));
342
- if (!name.startsWith(prefix) ||
343
- !Number.isSafeInteger(slot) ||
344
- slot < 1 ||
345
- name !== prefix + slot)
366
+ const placement = this.placement(managedRoots, parent, name);
367
+ if (!placement)
346
368
  continue;
369
+ const { documents, folder, slot, layout } = placement;
370
+ const managedRoot = managedBase(documents, layout);
347
371
  try {
348
372
  await assertManagedPath(managedRoot, location.path, false);
349
373
  if ((await this.commonDirectory(location.path)) !== commonDir)
@@ -357,7 +381,7 @@ export class WorktreePool {
357
381
  continue;
358
382
  if (Object.entries(registry.projects).some(([id, value]) => id !== projectId && key(value.folder) === key(folder)))
359
383
  continue;
360
- candidates.push({ path: location.path, folder, slot });
384
+ candidates.push({ path: location.path, folder, slot, root: documents, layout });
361
385
  }
362
386
  if (!project) {
363
387
  const folders = [...new Set(candidates.map((candidate) => candidate.folder))];
@@ -405,6 +429,8 @@ export class WorktreePool {
405
429
  repoRoot: await canonicalPath(location.path),
406
430
  slot: location.slot,
407
431
  managed: true,
432
+ root: resolve(location.root),
433
+ layout: location.layout,
408
434
  externalTaskId: reservation?.decision.externalTaskId ??
409
435
  'unregistered-' + sha256(key(location.path)).slice(0, 20),
410
436
  taskId: reservation?.decision.taskId,
@@ -451,9 +477,11 @@ export class WorktreePool {
451
477
  });
452
478
  return discoverOnly ? [] : this.list(projectId, policy);
453
479
  }
454
- async entry(input, commonDir, repoRoot, slot, managed) {
480
+ async entry(input, commonDir, repoRoot, slot, managed, root, layout) {
455
481
  const generation = randomUUID();
456
482
  return {
483
+ ...(root ? { root: resolve(root) } : {}),
484
+ ...(layout ? { layout } : {}),
457
485
  projectId: input.projectId,
458
486
  repoFingerprint: input.repoFingerprint,
459
487
  commonDir,
@@ -626,9 +654,9 @@ export class WorktreePool {
626
654
  .slice(0, 70);
627
655
  if (/^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i.test(folder))
628
656
  folder = 'project-' + folder;
629
- const root = join(await this.documentsDirectory(), 'engineeringmemory');
630
657
  if (Object.values(registry.projects).some((p) => key(p.folder) === key(folder)) ||
631
- (await pathExists(join(root, folder))))
658
+ (await pathExists(projectDirectory(await this.documentsDirectory(), folder, 2))) ||
659
+ (await pathExists(join(managedBase(await this.legacyDocumentsDirectory(), 1), folder))))
632
660
  folder += '-' + sha256(projectId).slice(0, 8);
633
661
  if (Object.values(registry.projects).some((p) => key(p.folder) === key(folder)))
634
662
  throw refuse('The managed project folder identity conflicts with another project.');
@@ -697,9 +725,87 @@ export class WorktreePool {
697
725
  return null;
698
726
  }
699
727
  }
728
+ async managedRoot() {
729
+ const root = managedBase(await this.documentsDirectory(), 2);
730
+ return {
731
+ root,
732
+ override: join(this.stateRoot, 'worktree-root.json'),
733
+ synced: /onedrive|dropbox|icloud|mobile documents|google\s?drive/i.test(root),
734
+ ...(this.overrideProblem ? { problem: this.overrideProblem } : {}),
735
+ };
736
+ }
700
737
  async documentsDirectory() {
738
+ if (this.overrideDocuments)
739
+ return this.overrideDocuments;
740
+ const overridePath = join(this.stateRoot, 'worktree-root.json');
741
+ this.overrideProblem = undefined;
742
+ let override = null;
743
+ try {
744
+ override = await readJson(overridePath);
745
+ }
746
+ catch (error) {
747
+ this.overrideProblem = `${overridePath} could not be read and is ignored: ${error instanceof Error ? error.message : String(error)}`;
748
+ }
749
+ if (override && typeof override.root === 'string' && isAbsolute(override.root)) {
750
+ try {
751
+ await mkdir(override.root, { recursive: true }).catch((error) => {
752
+ if (!['EEXIST', 'EPERM'].includes(error.code ?? ''))
753
+ throw error;
754
+ });
755
+ this.overrideDocuments = resolve(override.root);
756
+ return this.overrideDocuments;
757
+ }
758
+ catch (error) {
759
+ this.overrideProblem = `${overridePath} names ${override.root}, which could not be created and is ignored: ${error instanceof Error ? error.message : String(error)}`;
760
+ }
761
+ }
762
+ else if (override) {
763
+ this.overrideProblem = `${overridePath} must contain {"root": "<absolute directory>"} and is ignored.`;
764
+ }
765
+ return this.defaultDocumentsDirectory();
766
+ }
767
+ async defaultDocumentsDirectory() {
701
768
  if (this.documents)
702
769
  return resolve(this.documents);
770
+ this.documents =
771
+ process.platform === 'win32' ? (process.env.SystemDrive ?? 'C:') + '\\' : homedir();
772
+ return resolve(this.documents);
773
+ }
774
+ async scanRoots() {
775
+ return [
776
+ ...new Map([
777
+ await this.documentsDirectory(),
778
+ await this.defaultDocumentsDirectory(),
779
+ await this.legacyDocumentsDirectory(),
780
+ ].map((root) => [key(root), root])).values(),
781
+ ];
782
+ }
783
+ placement(roots, parent, name) {
784
+ for (const documents of roots) {
785
+ const parentName = parent.split(/[\\/]/).pop() ?? '';
786
+ const grandParent = dirname(parent);
787
+ const legacyFolder = grandParent.split(/[\\/]/).pop() ?? '';
788
+ const attempts = [
789
+ { folder: parentName, layout: 2 },
790
+ { folder: legacyFolder, layout: 1 },
791
+ ];
792
+ for (const { folder, layout } of attempts) {
793
+ if (!folder || key(parent) !== key(projectDirectory(documents, folder, layout)))
794
+ continue;
795
+ const prefix = folder + (layout === 1 ? '-worktree' : '_worktree');
796
+ const slot = Number(name.slice(prefix.length));
797
+ if (name.startsWith(prefix) &&
798
+ Number.isSafeInteger(slot) &&
799
+ slot >= 1 &&
800
+ name === prefix + slot)
801
+ return { documents, folder, slot, layout };
802
+ }
803
+ }
804
+ return null;
805
+ }
806
+ async legacyDocumentsDirectory() {
807
+ if (this.legacyDocuments)
808
+ return resolve(this.legacyDocuments);
703
809
  if (process.platform === 'win32') {
704
810
  const result = await this.runner.run('powershell.exe', [
705
811
  '-NoProfile',
@@ -709,19 +815,18 @@ export class WorktreePool {
709
815
  ], {});
710
816
  if (result.exitCode !== 0 || !result.stdout.trim())
711
817
  throw refuse('The Windows Documents folder could not be resolved.');
712
- this.documents = result.stdout.trim();
818
+ this.legacyDocuments = result.stdout.trim();
713
819
  }
714
820
  else if (process.platform === 'linux') {
715
821
  const result = await this.runner.run('xdg-user-dir', ['DOCUMENTS'], {}).catch(() => null);
716
- this.documents =
822
+ this.legacyDocuments =
717
823
  result?.exitCode === 0 && result.stdout.trim()
718
824
  ? result.stdout.trim()
719
825
  : join(homedir(), 'Documents');
720
826
  }
721
827
  else
722
- this.documents = join(homedir(), 'Documents');
723
- await mkdir(this.documents, { recursive: true });
724
- return resolve(this.documents);
828
+ this.legacyDocuments = join(homedir(), 'Documents');
829
+ return resolve(this.legacyDocuments);
725
830
  }
726
831
  async transaction(work) {
727
832
  await mkdir(this.stateRoot, { recursive: true });
@@ -764,8 +869,10 @@ export class WorktreePool {
764
869
  throw refuse('The registry contains conflicting task ownership. Restore a validated backup with worktree.reconcile.');
765
870
  paths.add(key(entry.repoRoot));
766
871
  if (entry.managed) {
767
- const documents = await this.documentsDirectory();
768
- const expected = join(documents, 'engineeringmemory', project.folder, 'worktrees', project.folder + '-worktree' + entry.slot);
872
+ const documents = entry.root ?? (await this.legacyDocumentsDirectory());
873
+ const expected = entry.slot
874
+ ? managedPath(documents, project.folder, entry.slot, entry.layout ?? 1)
875
+ : '';
769
876
  if (!entry.slot || key(entry.repoRoot) !== key(expected))
770
877
  throw refuse('A managed worktree path does not match its project Documents directory. Reconcile before continuing.');
771
878
  await assertManagedPath(documents, entry.repoRoot, true);
@@ -118,7 +118,10 @@ export async function assertManagedPath(root, target, allowMissingTarget) {
118
118
  export async function ensureManagedDirectory(root, directory) {
119
119
  const resolvedRoot = resolve(root);
120
120
  const resolvedDirectory = ensureWithinRoot(resolvedRoot, directory);
121
- await mkdir(resolvedRoot, { recursive: true });
121
+ await mkdir(resolvedRoot, { recursive: true }).catch((error) => {
122
+ if (!isNodeError(error) || !['EEXIST', 'EPERM'].includes(error.code ?? ''))
123
+ throw error;
124
+ });
122
125
  const rootStat = await lstat(resolvedRoot);
123
126
  if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
124
127
  throw new Error(`Managed root must be a real directory: ${resolvedRoot}`);
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.
@@ -14,7 +14,7 @@ Before a new write or scaffold task, read `session.entry` and the project Git pr
14
14
 
15
15
  Call `task.branch` with the stable `externalTaskId` and `repoRoot`. Its native forms show configured branch names, the current commit, another branch, and the explicit keep-current option. New branches always use a separate managed worktree. Configured remote bases are fetched and their exact commits pinned; after a fetch failure select an explicit local source through a new native decision, never silently use a cached branch. Existing task decisions survive retries and restarts. Resume their recorded branch rather than recreating or resetting it.
16
16
 
17
- Use the returned `repoRoot` for **every** file read/write, terminal, context, validation and Git/delivery operation. The user-local pool is shared by Codex and Claude. Managed directories live in the OS Documents folder under `engineeringmemory/<stable-project-folder>/worktrees/<folder>-worktreeN`; do not supply arbitrary `worktreePath` values or create ad-hoc siblings. Independent clones cannot reuse each other's worktrees. `worktree.list` explains which slots are active, inactive but protected, or safely reusable. The versioned backend policy defaults to 50 directories per project/computer, 30-second heartbeats and 10-minute inactivity. Protected inactive directories still count toward the limit. Only a global admin changes `worktree.set_policy`; it is not an environment setting.
17
+ Use the returned `repoRoot` for **every** file read/write, terminal, context, validation and Git/delivery operation. The user-local pool is shared by Codex and Claude. Managed directories live under `engineering_memory/worktrees/<stable-project-folder>/<folder>_worktreeN`, at the root of the system drive on Windows (`C:\engineering_memory\worktrees\...`) and in the home directory elsewhere, or under the absolute directory named by `worktree-root.json` in the user-level API state directory when that file exists; `worktree.list` reports the effective root, and worktrees created by earlier clients under the Documents folder keep working where they are. Do not supply arbitrary `worktreePath` values or create ad-hoc siblings. Independent clones cannot reuse each other's worktrees. `worktree.list` explains which slots are active, inactive but protected, or safely reusable. The versioned backend policy defaults to 50 directories per project/computer, 30-second heartbeats and 10-minute inactivity. Protected inactive directories still count toward the limit. Only a global admin changes `worktree.set_policy`; it is not an environment setting.
18
18
 
19
19
  Renew `task.heartbeat` using the exact returned task, path and ownership generation during actual work and at approximately the cached heartbeat interval during long local commands. The bridge renews while its own task operation is running. Never run a perpetual heartbeat for an idle chat or MCP process. Inactivity never authorizes takeover of a live owner or deletion of files. Before writing after interruption, use `session.resume`; follow any returned worktree redirect and resume there. A stale generation cannot renew, release, verify or commit another owner's work.
20
20
 
@@ -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.