engineering-memory 1.11.19 → 1.11.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/engineering-memory.mjs +14 -24
- package/install/localization.generated.mjs +18 -0
- package/package.json +1 -1
- package/runtime/build.json +1 -1
- package/runtime/dist/src/auth/browser-auth.js +15 -4
- package/runtime/dist/src/config.js +1 -0
- package/runtime/dist/src/git/git-inspector.js +10 -0
- package/runtime/dist/src/localization/catalogue.generated.js +786 -0
- package/runtime/dist/src/mcp/delivery-tools.js +15 -48
- package/runtime/dist/src/mcp/onboarding-tools.js +289 -618
- package/runtime/dist/src/mcp/questionnaire-tools.js +25 -26
- package/runtime/dist/src/mcp/status-meaning-tools.js +12 -86
- package/runtime/dist/src/mcp/status-remap-tools.js +252 -0
- package/runtime/dist/src/mcp/tool-annotations.js +1 -0
- package/runtime/dist/src/mcp/tool-definitions.js +33 -55
- package/runtime/dist/src/mcp/workflow-tools.js +17 -108
- package/runtime/dist/src/mcp/worktree-tools.js +168 -250
- package/runtime/dist/src/runtime/api-client.js +8 -2
- package/runtime/dist/src/runtime/bridge-service.js +100 -94
- package/runtime/dist/src/runtime/create-bridge-service.js +4 -2
- package/runtime/dist/src/runtime/language-store.js +6 -0
- package/runtime/dist/src/runtime/release-notes.js +9 -7
- package/runtime/dist/src/runtime/release-report.js +30 -30
- package/runtime/dist/src/runtime/task-start.js +75 -73
- package/runtime/dist/src/runtime/texts.js +135 -0
- package/runtime/dist/src/runtime/worktree-pool.js +30 -5
- package/runtime/dist/src/runtime/worktree-preparation.js +3 -2
- package/runtime/dist/src/utilities/process.js +7 -0
- package/skill/references/lifecycle.md +21 -3
- package/skill/references/project-onboarding.md +5 -2
- package/skill/references/questionnaires.md +5 -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 {
|
|
77
|
-
const
|
|
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:
|
|
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
|
-
? (
|
|
109
|
-
:
|
|
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
|
-
?
|
|
114
|
-
? 'Çekmeden, mevcut commit’ten.'
|
|
115
|
-
: 'Current commit; no fetch.'
|
|
142
|
+
? copy.base.localDetail
|
|
116
143
|
: option.id === 'other'
|
|
117
|
-
?
|
|
118
|
-
|
|
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:
|
|
154
|
+
message: copy.location.message,
|
|
136
155
|
...(holder
|
|
137
|
-
? {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
:
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
?
|
|
151
|
-
? 'Bu klasörde yeni dal'
|
|
152
|
-
: 'New branch in this folder'
|
|
168
|
+
? copy.location.here
|
|
153
169
|
: option.id === 'keep_current'
|
|
154
|
-
?
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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:
|
|
184
|
+
message: copy.message,
|
|
182
185
|
context: keepOnly
|
|
183
|
-
?
|
|
184
|
-
|
|
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
|
|
469
|
+
function withheldReason(folder, keepOnly) {
|
|
474
470
|
if (keepOnly || folder.managed || (!folder.heldBy && folder.clean))
|
|
475
471
|
return null;
|
|
476
|
-
|
|
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) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[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
|
|
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,
|
|
@@ -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) => {
|
|
@@ -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
|
-
|
|
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.
|
|
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
|
|
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.
|
|
222
|
-
|
|
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.
|
|
@@ -22,6 +22,8 @@ When the task mode calls for a user answer, use the host's native questionnaire
|
|
|
22
22
|
|
|
23
23
|
## Required decisions stay pending
|
|
24
24
|
|
|
25
|
+
The owned task-mode selector presents the question, its task-local scope and the consequences beside each option. Keep this routine choice short; complex decisions still need their context and example. Standard MCP exposes feedback as a separate optional field, and some Codex versions render that field and the choice on separate pages. This is one durable question, not two decisions. Do not claim an inline choice/free-input layout is supported by that renderer. Prefer the host's combined native control only where the host permits it; do not substitute asynchronous input for a required answer or bypass a tool's mode restrictions. Retain free feedback without adding a duplicate Other option or treating a comment as consent.
|
|
26
|
+
|
|
25
27
|
Use `questionnaire.ask` for a required decision. It records the question before opening a native MCP form. Use a stable question identifier belonging to the current task and decision, so a retry returns to the same question instead of creating another one. A new decision needs its own identifier; an answer to an earlier proposal, branch or delivery does not approve a later one.
|
|
26
28
|
|
|
27
29
|
Pass `repoRoot`, `questionnaireId`, `message` and two to twelve `options`, each with an `id` and `label`. Identifiers use letters, digits, underscores or hyphens.
|
|
@@ -353,7 +355,9 @@ Saving a profile does not authorize filesystem initialization.
|
|
|
353
355
|
For memory.sync_start, pass language: tr in a Turkish conversation and language: en in an English
|
|
354
356
|
conversation. Use the conversation language, never the computer locale. A call without language uses
|
|
355
357
|
the language remembered from an earlier call, and English when none is known. The bridge's own forms
|
|
356
|
-
|
|
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
|
|
357
361
|
the separate approval needed for permanent memory. Full commit and request hashes stay in the durable
|
|
358
362
|
approval binding, not in the visible question. Generic questionnaire.ask messages and option labels
|
|
359
363
|
are written by the agent in the same language; pass language for the native help and field labels.
|