engineering-memory 1.11.20 → 1.11.22

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 (31) hide show
  1. package/bin/engineering-memory.mjs +14 -24
  2. package/install/localization.generated.mjs +18 -0
  3. package/package.json +1 -1
  4. package/runtime/build.json +1 -1
  5. package/runtime/dist/src/auth/browser-auth.js +15 -4
  6. package/runtime/dist/src/config.js +1 -0
  7. package/runtime/dist/src/git/git-inspector.js +10 -0
  8. package/runtime/dist/src/localization/catalogue.generated.js +786 -0
  9. package/runtime/dist/src/mcp/delivery-tools.js +15 -48
  10. package/runtime/dist/src/mcp/onboarding-tools.js +289 -618
  11. package/runtime/dist/src/mcp/questionnaire-tools.js +23 -26
  12. package/runtime/dist/src/mcp/status-meaning-tools.js +12 -86
  13. package/runtime/dist/src/mcp/status-remap-tools.js +252 -0
  14. package/runtime/dist/src/mcp/tool-annotations.js +1 -0
  15. package/runtime/dist/src/mcp/tool-definitions.js +33 -55
  16. package/runtime/dist/src/mcp/workflow-tools.js +17 -108
  17. package/runtime/dist/src/mcp/worktree-tools.js +168 -250
  18. package/runtime/dist/src/runtime/bridge-service.js +123 -94
  19. package/runtime/dist/src/runtime/create-bridge-service.js +4 -2
  20. package/runtime/dist/src/runtime/language-store.js +6 -0
  21. package/runtime/dist/src/runtime/release-notes.js +6 -6
  22. package/runtime/dist/src/runtime/release-report.js +30 -30
  23. package/runtime/dist/src/runtime/task-start.js +75 -73
  24. package/runtime/dist/src/runtime/texts.js +135 -0
  25. package/runtime/dist/src/runtime/worktree-pool.js +30 -5
  26. package/runtime/dist/src/runtime/worktree-preparation.js +3 -2
  27. package/runtime/dist/src/utilities/local-mutex.js +5 -2
  28. package/runtime/dist/src/utilities/process.js +7 -0
  29. package/skill/references/lifecycle.md +22 -4
  30. package/skill/references/project-onboarding.md +5 -2
  31. package/skill/references/questionnaires.md +3 -1
@@ -1,4 +1,6 @@
1
+ import * as z from 'zod/v4';
1
2
  import { sha256, stableStringify } from '../utilities/hash.js';
3
+ import { copies, format } from './texts.js';
2
4
  const startCopy = {
3
5
  en: {
4
6
  start: (task) => `${task}: how should this task start?`,
@@ -39,6 +41,34 @@ const startCopy = {
39
41
  typedName: 'Adı aşağıya yaz. Zaten var olan bir ad kabul edilmez, form yeniden sorulur.',
40
42
  },
41
43
  };
44
+ const text = z.string().min(1);
45
+ const option = z.strictObject({ label: text, description: text });
46
+ const startWording = z.strictObject({
47
+ message: text,
48
+ keepContext: text,
49
+ newBranch: text,
50
+ example: text,
51
+ stay: text,
52
+ base: z.strictObject({
53
+ message: text,
54
+ local: text,
55
+ localDetail: text,
56
+ other: text,
57
+ otherDetail: text,
58
+ fetchDetail: text,
59
+ field: text,
60
+ }),
61
+ location: z.strictObject({
62
+ message: text,
63
+ holder: text,
64
+ busy: text,
65
+ dirty: text,
66
+ worktree: option,
67
+ here: option,
68
+ keep: option,
69
+ defer: option,
70
+ }),
71
+ });
42
72
  export function suggestedBranchNames(externalTaskId) {
43
73
  const slug = externalTaskId
44
74
  .replace(/[^A-Za-z0-9._-]+/g, '-')
@@ -73,14 +103,15 @@ export function configuredBase(preferences, role) {
73
103
  : null;
74
104
  }
75
105
  export function taskStartDefinition(facts) {
76
- const { previous, ...legacy } = legacyTaskStartDefinition(facts);
77
- const tr = facts.language === 'tr';
106
+ const { language, copy } = copies(startWording, 'taskStart', facts.language)[0];
107
+ const { previous, ...legacy } = legacyTaskStartDefinition({ ...facts, language });
78
108
  const binding = { ...legacy.binding };
79
109
  const keepOnly = facts.keepCurrent === true || !facts.currentCommit;
80
110
  if (!keepOnly) {
81
111
  binding.name = facts.name ?? facts.suggestedName;
82
112
  delete binding.suggestedName;
83
113
  }
114
+ const branch = facts.currentBranch ?? 'HEAD';
84
115
  const seen = new Set();
85
116
  const questions = legacy
86
117
  .questions.filter((question) => question.id !== 'name')
@@ -88,7 +119,7 @@ export function taskStartDefinition(facts) {
88
119
  if (question.id === 'base')
89
120
  return {
90
121
  ...question,
91
- message: tr ? 'Hangi daldan başlayalım?' : 'Which starting branch?',
122
+ message: copy.base.message,
92
123
  options: question.options
93
124
  .filter((option) => {
94
125
  const base = binding.bases?.[option.id];
@@ -105,92 +136,57 @@ export function taskStartDefinition(facts) {
105
136
  label: base?.kind === 'remote'
106
137
  ? sourceLabel(base, facts.currentBranch)
107
138
  : option.id === 'current'
108
- ? (tr ? 'Yerel: ' : 'Local: ') + (facts.currentBranch ?? 'HEAD')
109
- : tr
110
- ? 'origin üzerinde başka dal'
111
- : 'Another branch on origin',
139
+ ? format(copy.base.local, language, { branch })
140
+ : copy.base.other,
112
141
  description: option.id === 'current'
113
- ? tr
114
- ? 'Çekmeden, mevcut commit’ten.'
115
- : 'Current commit; no fetch.'
142
+ ? copy.base.localDetail
116
143
  : option.id === 'other'
117
- ? tr
118
- ? 'Alttaki alana yaz.'
119
- : 'Enter it below.'
120
- : tr
121
- ? 'Güncel hali çekilir.'
122
- : 'Fetch the latest commit.',
144
+ ? copy.base.otherDetail
145
+ : copy.base.fetchDetail,
123
146
  };
124
147
  }),
125
- textField: {
126
- ...question.textField,
127
- title: tr
128
- ? 'origin dalı (yalnızca başka dal için)'
129
- : 'Branch on origin (only for another branch)',
130
- },
148
+ textField: { ...question.textField, title: copy.base.field },
131
149
  };
132
150
  const holder = binding.holder?.externalTaskId;
151
+ const withheld = withheldReason(facts.folder, keepOnly);
133
152
  return {
134
153
  ...question,
135
- message: tr ? 'Nerede çalışalım?' : 'Where should we work?',
154
+ message: copy.location.message,
136
155
  ...(holder
137
- ? {
138
- context: tr
139
- ? `Bu klasördeki ${holder} görevi burada başlarsan ayrı worktree’ye geçer; işi korunur.`
140
- : `Starting here moves ${holder} to a separate worktree; its work is kept.`,
141
- }
142
- : {}),
143
- options: question.options.map((option) => ({
144
- ...option,
145
- label: option.id === 'worktree'
146
- ? tr
147
- ? 'Ayrı worktree'
148
- : 'Separate worktree'
156
+ ? { context: format(copy.location.holder, language, { task: holder }) }
157
+ : withheld
158
+ ? {
159
+ context: format(copy.location[withheld], language, {
160
+ task: facts.folder.heldBy ?? '',
161
+ }),
162
+ }
163
+ : {}),
164
+ options: question.options.map((option) => {
165
+ const shown = option.id === 'worktree'
166
+ ? copy.location.worktree
149
167
  : option.id === 'here'
150
- ? tr
151
- ? 'Bu klasörde yeni dal'
152
- : 'New branch in this folder'
168
+ ? copy.location.here
153
169
  : option.id === 'keep_current'
154
- ? tr
155
- ? 'Mevcut dalda kal'
156
- : 'Stay on the current branch'
157
- : tr
158
- ? 'Şimdilik başlatma'
159
- : 'Not now',
160
- description: option.id === 'worktree'
161
- ? tr
162
- ? 'Bu klasör değişmez.'
163
- : 'Leave this folder unchanged.'
164
- : option.id === 'here'
165
- ? tr
166
- ? 'Klasör yeni dala geçer.'
167
- : 'Switch this folder to the new branch.'
168
- : option.id === 'keep_current'
169
- ? tr
170
- ? `${facts.currentBranch ?? 'HEAD'} üzerinde devam et.`
171
- : `Continue on ${facts.currentBranch ?? 'HEAD'}.`
172
- : tr
173
- ? 'Hiçbir şey oluşturulmaz.'
174
- : 'Create nothing.',
175
- })),
170
+ ? copy.location.keep
171
+ : copy.location.defer;
172
+ return {
173
+ ...option,
174
+ label: shown.label,
175
+ description: format(shown.description, language, { branch }),
176
+ };
177
+ }),
176
178
  };
177
179
  });
178
180
  questions.sort((left, right) => Number(right.id === 'base') - Number(left.id === 'base'));
181
+ const current = sourceLabel({ kind: 'current', ...(facts.currentCommit ? { commit: facts.currentCommit } : {}) }, facts.currentBranch);
179
182
  return {
180
183
  ...legacy,
181
- message: tr ? 'Göreve başla' : 'Start task',
184
+ message: copy.message,
182
185
  context: keepOnly
183
- ? tr
184
- ? 'Mevcut dalda devam edilecek.'
185
- : 'Continue on the current branch.'
186
- : (tr ? 'Yeni dal: ' : 'New branch: ') +
187
- binding.name +
186
+ ? copy.keepContext
187
+ : format(copy.newBranch, language, { name: binding.name }) +
188
188
  (binding.base ? ' · ' + sourceLabel(binding.base, facts.currentBranch) : ''),
189
- example: keepOnly
190
- ? legacy.example
191
- : tr
192
- ? 'Ayrı worktree, işi ayrı klasörde tutar.'
193
- : 'A worktree keeps this task in its own folder.',
189
+ example: keepOnly ? format(copy.stay, language, { source: current }) : copy.example,
194
190
  questions,
195
191
  binding: JSON.parse(JSON.stringify(binding)),
196
192
  previous: [previous, legacy],
@@ -470,10 +466,16 @@ function readableTime(value, tr) {
470
466
  return null;
471
467
  return (tr ? date.split('-').reverse().join('.') : date) + ' ' + time + ' UTC';
472
468
  }
473
- function withheld(folder, keepOnly, tr) {
469
+ function withheldReason(folder, keepOnly) {
474
470
  if (keepOnly || folder.managed || (!folder.heldBy && folder.clean))
475
471
  return null;
476
- if (folder.heldBy)
472
+ return folder.heldBy ? 'busy' : 'dirty';
473
+ }
474
+ function withheld(folder, keepOnly, tr) {
475
+ const reason = withheldReason(folder, keepOnly);
476
+ if (!reason)
477
+ return null;
478
+ if (reason === 'busy')
477
479
  return tr
478
480
  ? folder.heldBy +
479
481
  ' bu klasörde başka bir oturumda hâlâ çalışıyor; yeni branch burada açılamaz. O oturumda duraklatılırsa bu klasör de seçilebilir.'
@@ -0,0 +1,135 @@
1
+ import * as z from 'zod/v4';
2
+ import { endpoints } from '../config.js';
3
+ import { bundledDirections, bundledTexts } from '../localization/catalogue.generated.js';
4
+ const fetched = {};
5
+ const runtimeTexts = z.object({
6
+ language: z.string(),
7
+ direction: z.enum(['ltr', 'rtl']),
8
+ texts: z.array(z.object({ key: z.string(), text: z.string() })),
9
+ });
10
+ const savedTexts = z.object({ current: runtimeTexts, previous: runtimeTexts.optional() });
11
+ export async function fetchLanguage(client, store, language) {
12
+ const stored = savedTexts.safeParse(await store.fetchedTexts(language));
13
+ let saved = stored.success ? stored.data : undefined;
14
+ const response = await client
15
+ .request(`${endpoints.runtimeTexts}?${new URLSearchParams({ language })}`, {
16
+ authenticated: false,
17
+ networkTimeoutMs: 2000,
18
+ })
19
+ .catch(() => null);
20
+ const fresh = runtimeTexts.safeParse(response?.data);
21
+ if (fresh.success && JSON.stringify(fresh.data) !== JSON.stringify(saved?.current)) {
22
+ saved = { current: fresh.data, previous: saved?.current };
23
+ await store.rememberFetchedTexts(language, saved);
24
+ }
25
+ if (!saved?.current.texts.length)
26
+ return;
27
+ fetched[saved.current.language] = {
28
+ direction: saved.current.direction,
29
+ versions: [saved.current, ...(saved.previous ? [saved.previous] : [])].map((version) => Object.fromEntries(version.texts
30
+ .filter(({ key }) => bundledTexts.en[key] !== undefined)
31
+ .map(({ key, text }) => [key, text]))),
32
+ };
33
+ }
34
+ export function texts(prefix, language, version = 0) {
35
+ const own = (code) => bundledTexts[code] ?? fetched[code]?.versions[version];
36
+ const catalogue = { ...own(language.split('-')[0]), ...own(language) };
37
+ const copy = {};
38
+ for (const [key, text] of Object.entries(catalogue)) {
39
+ if (!key.startsWith(prefix + '.'))
40
+ continue;
41
+ const path = key.slice(prefix.length + 1).split('.');
42
+ let node = copy;
43
+ for (const part of path.slice(0, -1))
44
+ node = (node[part] ??= {});
45
+ node[path.at(-1)] = text;
46
+ }
47
+ return copy;
48
+ }
49
+ export function format(template, language, values) {
50
+ let text = '';
51
+ for (let index = 0; index < template.length; index++) {
52
+ if (template[index] !== '{') {
53
+ text += template[index];
54
+ continue;
55
+ }
56
+ const end = closing(template, index);
57
+ const inner = template.slice(index + 1, end);
58
+ const comma = inner.indexOf(',');
59
+ const name = (comma < 0 ? inner : inner.slice(0, comma)).trim();
60
+ if (values[name] === undefined)
61
+ throw new Error(`No value for {${name}} in ${template}`);
62
+ if (comma < 0)
63
+ text += String(values[name]);
64
+ else {
65
+ const kind = inner.indexOf(',', comma + 1);
66
+ if (inner.slice(comma + 1, kind).trim() !== 'plural')
67
+ throw new Error(`Only plural formats are supported: ${template}`);
68
+ const count = Number(values[name]);
69
+ const chosen = pluralCase(inner.slice(kind + 1), language, count);
70
+ text += format(chosen.replaceAll('#', String(values[name])), language, values);
71
+ }
72
+ index = end;
73
+ }
74
+ return text;
75
+ }
76
+ function pluralCase(cases, language, count) {
77
+ const found = new Map();
78
+ for (let index = 0; index < cases.length;) {
79
+ const open = cases.indexOf('{', index);
80
+ if (open < 0)
81
+ break;
82
+ const end = closing(cases, open);
83
+ found.set(cases.slice(index, open).trim(), cases.slice(open + 1, end));
84
+ index = end + 1;
85
+ }
86
+ const chosen = found.get('=' + count) ??
87
+ found.get(new Intl.PluralRules(language).select(count)) ??
88
+ found.get('other');
89
+ if (chosen === undefined)
90
+ throw new Error(`A plural format needs an other case: ${cases}`);
91
+ return chosen;
92
+ }
93
+ function closing(text, open) {
94
+ let depth = 0;
95
+ for (let index = open; index < text.length; index++) {
96
+ if (text[index] === '{')
97
+ depth++;
98
+ if (text[index] === '}' && --depth === 0)
99
+ return index;
100
+ }
101
+ throw new Error(`Unbalanced braces in ${text}`);
102
+ }
103
+ export function catalogueLanguages() {
104
+ return [...Object.keys(bundledTexts), ...Object.keys(fetched)];
105
+ }
106
+ export function escapeHtml(value) {
107
+ return value.replace(/[&<>"']/g, (character) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[character]);
108
+ }
109
+ export function textDirection(language) {
110
+ const primary = language.split('-')[0];
111
+ return (bundledDirections[language] ??
112
+ fetched[language]?.direction ??
113
+ bundledDirections[primary] ??
114
+ fetched[primary]?.direction ??
115
+ 'ltr');
116
+ }
117
+ export function copies(wording, prefix, language = 'en') {
118
+ const complete = (code, version = 0) => {
119
+ const copy = wording.safeParse(texts(prefix, code, version));
120
+ return copy.success ? [{ language: code, copy: copy.data }] : [];
121
+ };
122
+ const known = catalogueLanguages().find((code) => code.toLowerCase() === language.toLowerCase()) ??
123
+ language.split('-')[0];
124
+ const shown = complete(known)[0] ?? complete('en')[0];
125
+ if (!shown)
126
+ throw new Error(`The English ${prefix} texts do not match the wording the code reads.`);
127
+ return [
128
+ shown,
129
+ ...catalogueLanguages()
130
+ .filter((code) => code !== shown.language)
131
+ .flatMap((code) => complete(code)),
132
+ ...Object.entries(fetched).flatMap(([code, { versions }]) => versions.slice(1).flatMap((_, index) => complete(code, index + 1))),
133
+ ];
134
+ }
135
+ //# sourceMappingURL=texts.js.map
@@ -7,7 +7,7 @@ import { GitInspector } from '../git/git-inspector.js';
7
7
  import { atomicWrite, assertManagedPath, canonicalPath, ensureManagedDirectory, pathExists, readJson, removeFile, safeSegment, } from '../utilities/files.js';
8
8
  import { sha256 } from '../utilities/hash.js';
9
9
  import { localMutex } from '../utilities/local-mutex.js';
10
- import { NativeCommandRunner } from '../utilities/process.js';
10
+ import { NativeCommandRunner, windowsSystemTool } from '../utilities/process.js';
11
11
  import { BridgeRecoveryError } from './recovery-error.js';
12
12
  import { planWorktreeFiles, ignoredRuntimeFiles, createWorktreeStage, stageWorktreeFiles, publishWorktreeFiles, discardStagedWorktreeFiles, } from './worktree-preparation.js';
13
13
  import { WorktreeFileIssue, WorktreeFileStatus, } from './worktree-readiness-types.js';
@@ -43,6 +43,8 @@ const allocationSchema = z.object({
43
43
  root: z.string().optional(),
44
44
  layout: z.union([z.literal(1), z.literal(2)]).optional(),
45
45
  deliveryOutcome: z.enum(['delivered', 'cancelled']).optional(),
46
+ deliveredCommit: z.string().optional(),
47
+ deliveredRef: z.string().optional(),
46
48
  cancelledAt: z.string().optional(),
47
49
  preservedRef: z.string().optional(),
48
50
  sourceRepoRoot: z.string().optional(),
@@ -251,6 +253,7 @@ export class WorktreePool {
251
253
  await this.save(registry);
252
254
  return structuredClone(entry);
253
255
  }
256
+ await this.settleDelivered(registry, project.entries);
254
257
  let reusable;
255
258
  const activeRoot = key(await this.documentsDirectory());
256
259
  const legacyRoot = await this.legacyDocumentsDirectory();
@@ -463,8 +466,10 @@ export class WorktreePool {
463
466
  }
464
467
  async list(projectId, policy) {
465
468
  return this.transaction(async (registry) => {
469
+ const entries = registry.projects[projectId]?.entries ?? [];
470
+ await this.settleDelivered(registry, entries);
466
471
  const result = [];
467
- for (const entry of registry.projects[projectId]?.entries ?? [])
472
+ for (const entry of entries)
468
473
  result.push(await this.view(entry, policy));
469
474
  return result;
470
475
  });
@@ -945,6 +950,26 @@ export class WorktreePool {
945
950
  };
946
951
  return entry;
947
952
  }
953
+ async settleDelivered(registry, entries) {
954
+ for (const entry of entries) {
955
+ if (!entry.managed ||
956
+ !entry.pendingDelivery ||
957
+ ['creating', 'quarantined', 'released'].includes(entry.phase) ||
958
+ (entry.owner && !this.owns(entry) && (await this.ownerAlive(entry.owner))) ||
959
+ (await this.fileProtection(entry)).length)
960
+ continue;
961
+ const head = (await this.git.sourceIdentity(entry.repoRoot))?.sourceCommit;
962
+ if (!head || head === entry.baseCommit)
963
+ continue;
964
+ const ref = await this.git.remoteRefContaining(entry.repoRoot, head);
965
+ if (!ref)
966
+ continue;
967
+ entry.deliveryOutcome = 'delivered';
968
+ entry.deliveredCommit = head;
969
+ entry.deliveredRef = ref;
970
+ await this.releaseEntry(registry, entry);
971
+ }
972
+ }
948
973
  async releaseEntry(registry, entry) {
949
974
  await this.releaseReservation(entry);
950
975
  if (entry.managed) {
@@ -1220,7 +1245,7 @@ export class WorktreePool {
1220
1245
  const found = new Map();
1221
1246
  try {
1222
1247
  if (process.platform === 'win32') {
1223
- const result = await this.runner.run('powershell.exe', [
1248
+ const result = await this.runner.run(windowsSystemTool('WindowsPowerShell', 'v1.0', 'powershell.exe'), [
1224
1249
  '-NoProfile',
1225
1250
  '-NonInteractive',
1226
1251
  '-Command',
@@ -1262,7 +1287,7 @@ export class WorktreePool {
1262
1287
  return stat.slice(stat.lastIndexOf(')') + 2).split(' ')[19] ?? null;
1263
1288
  }
1264
1289
  const result = process.platform === 'win32'
1265
- ? await this.runner.run('powershell.exe', [
1290
+ ? await this.runner.run(windowsSystemTool('WindowsPowerShell', 'v1.0', 'powershell.exe'), [
1266
1291
  '-NoProfile',
1267
1292
  '-NonInteractive',
1268
1293
  '-Command',
@@ -1359,7 +1384,7 @@ export class WorktreePool {
1359
1384
  if (this.legacyDocuments)
1360
1385
  return resolve(this.legacyDocuments);
1361
1386
  if (process.platform === 'win32') {
1362
- const result = await this.runner.run('powershell.exe', [
1387
+ const result = await this.runner.run(windowsSystemTool('WindowsPowerShell', 'v1.0', 'powershell.exe'), [
1363
1388
  '-NoProfile',
1364
1389
  '-NonInteractive',
1365
1390
  '-Command',
@@ -7,6 +7,7 @@ import { assertManagedPath, ensureManagedDirectory } from '../utilities/files.js
7
7
  import { WorktreeFileIssue, WorktreeFileStatus, } from './worktree-readiness-types.js';
8
8
  import { parseGradleSigning } from './worktree-gradle.js';
9
9
  import { sha256, stableStringify } from '../utilities/hash.js';
10
+ import { windowsSystemTool } from '../utilities/process.js';
10
11
  export async function ignoredRuntimeFiles(sourceRoot, mainRoot, repoRoot) {
11
12
  const sources = [];
12
13
  for (const root of [...new Set([sourceRoot, mainRoot])]) {
@@ -824,7 +825,7 @@ async function assertStage(stage, missing = false) {
824
825
  export async function stageWorktreeFiles(stage) {
825
826
  const directory = (await assertStage(stage));
826
827
  if (process.platform === 'win32') {
827
- const account = await execute('whoami.exe', ['/user', '/fo', 'csv', '/nh'], {
828
+ const account = await execute(windowsSystemTool('whoami.exe'), ['/user', '/fo', 'csv', '/nh'], {
828
829
  timeout: 10000,
829
830
  windowsHide: true,
830
831
  maxBuffer: 8192,
@@ -832,7 +833,7 @@ export async function stageWorktreeFiles(stage) {
832
833
  const sid = /S-1-5-\d+(?:-\d+)+/.exec(account.stdout)?.[0];
833
834
  if (!sid)
834
835
  throw new Error('Local file access could not be restricted');
835
- await execute('icacls.exe', [directory, '/inheritance:r', '/grant:r', '*' + sid + ':(OI)(CI)F'], {
836
+ await execute(windowsSystemTool('icacls.exe'), [directory, '/inheritance:r', '/grant:r', '*' + sid + ':(OI)(CI)F'], {
836
837
  timeout: 10000,
837
838
  windowsHide: true,
838
839
  maxBuffer: 8192,
@@ -3,7 +3,10 @@ import { performance } from 'node:perf_hooks';
3
3
  import { sha256 } from './hash.js';
4
4
  import { BridgeRecoveryError } from '../runtime/recovery-error.js';
5
5
  export async function localMutex(identity, work, timeoutMs = 60000) {
6
- const port = 20000 + (parseInt(sha256(identity).slice(0, 8), 16) % 30000);
6
+ const hash = sha256(identity);
7
+ const address = process.platform === 'win32'
8
+ ? { path: String.raw `\\.\pipe\engineering-memory-` + hash }
9
+ : { host: '127.0.0.1', port: 20000 + (parseInt(hash.slice(0, 8), 16) % 30000) };
7
10
  const deadline = performance.now() + timeoutMs;
8
11
  let server;
9
12
  for (;;) {
@@ -11,7 +14,7 @@ export async function localMutex(identity, work, timeoutMs = 60000) {
11
14
  try {
12
15
  await new Promise((resolve, reject) => {
13
16
  server.once('error', reject);
14
- server.listen({ host: '127.0.0.1', port, exclusive: true }, () => {
17
+ server.listen({ ...address, exclusive: true }, () => {
15
18
  server.removeListener('error', reject);
16
19
  resolve();
17
20
  });
@@ -1,4 +1,11 @@
1
1
  import { spawn } from 'node:child_process';
2
+ import { win32 } from 'node:path';
3
+ export function windowsSystemTool(...path) {
4
+ const windowsDirectory = process.env.SystemRoot ?? process.env.WINDIR;
5
+ if (!windowsDirectory || !win32.isAbsolute(windowsDirectory))
6
+ throw new Error('A trusted absolute Windows system directory is required');
7
+ return win32.join(windowsDirectory, 'System32', ...path);
8
+ }
2
9
  export class NativeCommandRunner {
3
10
  async run(command, args, options = {}) {
4
11
  return await new Promise((resolvePromise, reject) => {
@@ -40,7 +40,7 @@ Renew `task.heartbeat` using the exact returned task, path and ownership generat
40
40
 
41
41
  Use `task.pause` when work stops or is handed to another client. It preserves task state and files, invalidates the old owner, and allows safe clean directories to be reused. It is not `task.abandon`. Resume reacquires the original directory when possible or uses the retained task branch in another safe directory. `worktree.reconcile` recovers interrupted allocation and validated local ownership records against Git. Never edit the registry, force checkout, stash/reset/clean, remove branches, kill a client or delete a worktree to bypass a refusal.
42
42
 
43
- `task.close` retains a managed checkout while changes or delivery remain pending. After delivery, release with `deliveryOutcome:'delivered'` and the current generation. When the user decides not to deliver, call `worktree.release` with `deliveryOutcome:'cancelled'`; the product asks what happens to the folder and saves any changes to a ref. Never run `git restore` or `git clean` yourself to free a worktree, and never report a delivery that did not happen. Dirty/staged/untracked files, pending outbox/intents and unfinished Git operations prevent release. An old release cannot affect a reused directory. User-owned existing directories are never added to automatic cleanup. When the user wants to work in the main folder, they answer that in `task.branch`'s start form; when an open task holds the folder, that option names the task that would move out. A closed or abandoned task stops holding the folder by itself; a task still running in another client keeps it until it is paused there. Never edit or delete the reservation ref by hand.
43
+ `task.close` retains a managed checkout while changes or delivery remain pending. After delivery, release with `deliveryOutcome:'delivered'` and the current generation. Once the task's commits are on a remote branch the pool settles the folder by itself; `worktree.release` then answers `alreadyReleased`, `session.resume` of the closed task answers `folderReleased`, and nothing further is needed. When the user decides not to deliver, call `worktree.release` with `deliveryOutcome:'cancelled'`; the product asks what happens to the folder and saves any changes to a ref. Never run `git restore` or `git clean` yourself to free a worktree, and never report a delivery that did not happen. Dirty/staged/untracked files, pending outbox/intents and unfinished Git operations prevent release. An old release cannot affect a reused directory. User-owned existing directories are never added to automatic cleanup. When the user wants to work in the main folder, they answer that in `task.branch`'s start form; when an open task holds the folder, that option names the task that would move out. A closed or abandoned task stops holding the folder by itself; a task still running in another client keeps it until it is paused there. Never edit or delete the reservation ref by hand.
44
44
 
45
45
  Read-only tasks allocate no worktree. If the user authorizes writing, settle `task.branch` for that same externalTaskId, use the allocated repoRoot, then call `context.prepare_change` with `transitionToWrite`. Its pinned source must still match; choosing a different source requires a new task, never rewriting the original task's source identity. Existing lease, discipline, source-memory, verify and commit gates remain mandatory.
46
46
 
@@ -371,9 +371,10 @@ identity. Autonomous must still provide reasoned questionnaire.decide responses;
371
371
  settings are critical in Approve for me. A changed snapshot requires rereading and reviewing, never
372
372
  substituting new versions. Honor explicit deferral and reconsider only when authorized.
373
373
 
374
- For a language other than Turkish or English, supply every copy field in the conversation language.
374
+ Copy comes from the product texts when they cover the conversation language. When the tool refuses
375
+ for missing copy, supply every copy field in the conversation language.
375
376
  Do not open custom parallel questions or force a separate Other option. Use the optional description
376
- flow below separately from role setup. Counted work remapping remains a later unit.
377
+ flow below separately from role setup. Moving existing work to other stages is its own flow below.
377
378
 
378
379
  ## Optional stage descriptions
379
380
 
@@ -386,4 +387,21 @@ The existing editable question can collect explicit text; supplied longer author
386
387
  reviewed in full through pages. Only the final review saves the chosen field. Clear requires the
387
388
  same review, while keep/skip and unchanged text write nothing. Do not substitute fresh versions,
388
389
  reuse a different task's approval or silently proceed after deferral. Mode-governed native or
389
- reasoned delegated receipts still apply. Complete copy is required outside Turkish/English.
390
+ reasoned delegated receipts still apply. Complete copy is required when the product texts lack the
391
+ conversation language.
392
+
393
+ ## Moving work between stages
394
+
395
+ When the user retires or merges stages and wants their work to continue elsewhere, read every
396
+ work_item.workflow page at one snapshot. Each row carries `activeItems` and `archivedItems`, the
397
+ open and archived work in that stage. Ask the user where each stage's work goes; never pick a
398
+ target from a stage name. Then call work_item.remap_statuses with the resumed task, that exact
399
+ snapshot, the from/to pairs they chose, a short reason and the conversation language.
400
+
401
+ A source may be an archived stage; a target must be active; a stage cannot be both. The tool shows
402
+ every group with its counts and the roles still aimed at a source stage, and moves all of a
403
+ source's work, open and archived, in one operation. Stage definitions, order, archive state and
404
+ roles stay as they are; change an aimed role separately with work_item.setup_workflow. Approval is
405
+ bound to the counts it showed: when they change, the question is asked again, and a change at the
406
+ moment of writing moves nothing. It is critical under the task mode. An empty source needs no
407
+ question. Keep the same requestKey to resume and honor deferral as in the flows above.
@@ -218,8 +218,11 @@ A deferred individual field or final profile saves no onboarding choice. Retryin
218
218
  ## Inspection confirmation language
219
219
 
220
220
  For memory.sync_start, pass language: tr in a Turkish conversation and language: en in an English
221
- conversation. Use the conversation language, never the computer locale. Legacy calls without language
222
- use English. The form shows a short source reference, scope, the treatment of uncommitted changes and
221
+ conversation. Use the conversation language, never the computer locale. A call without language uses
222
+ the language remembered from an earlier call, and English when none is known. The bridge's own forms
223
+ carry Turkish and English, fetch any other language the product texts have, and open in English when
224
+ that language lacks a form's text. The form shows a short source reference, scope, the treatment of
225
+ uncommitted changes and
223
226
  the separate approval needed for permanent memory. Full commit and request hashes stay in the durable
224
227
  approval binding, not in the visible question. Generic questionnaire.ask messages and option labels
225
228
  are written by the agent in the same language; pass language for the native help and field labels.
@@ -355,7 +355,9 @@ Saving a profile does not authorize filesystem initialization.
355
355
  For memory.sync_start, pass language: tr in a Turkish conversation and language: en in an English
356
356
  conversation. Use the conversation language, never the computer locale. A call without language uses
357
357
  the language remembered from an earlier call, and English when none is known. The bridge's own forms
358
- have Turkish and English text; any other language opens them in English. The form shows a short source reference, scope, the treatment of uncommitted changes and
358
+ carry Turkish and English, fetch any other language the product texts have, and open in English when
359
+ that language lacks a form's text. The form shows a short source reference, scope, the treatment of
360
+ uncommitted changes and
359
361
  the separate approval needed for permanent memory. Full commit and request hashes stay in the durable
360
362
  approval binding, not in the visible question. Generic questionnaire.ask messages and option labels
361
363
  are written by the agent in the same language; pass language for the native help and field labels.