engineering-memory 1.11.5 → 1.11.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dispatcher/sections.mjs +1 -1
- package/package.json +29 -29
- package/runtime/dist/src/mcp/worktree-tools.js +67 -4
- package/runtime/dist/src/runtime/bridge-service.js +24 -3
- package/runtime/dist/src/runtime/worktree-pool.js +94 -36
- package/skill/references/lifecycle.md +40 -0
- package/skill/references/questionnaires.md +13 -0
package/dispatcher/sections.mjs
CHANGED
|
@@ -19,7 +19,7 @@ Never answer a question about a bound repository from the working tree alone. Wh
|
|
|
19
19
|
|
|
20
20
|
After compaction, a new chat, interruption, or handoff, call \`session.resume\` before continuing.
|
|
21
21
|
|
|
22
|
-
Before a new write/scaffold task, use \`task.branch\` for native base selection and
|
|
22
|
+
Before a new write/scaffold task, inspect \`worktree.list\` first, then use \`task.branch\` for native base selection and pool allocation. A separate worktree does not necessarily mean a new directory: reuse a safely available slot before creating another. Never invent the next numbered path. Reconcile protected legacy entries through the native recovery flow; clean files alone do not establish that the old agent and delivery have finished. Run all commands from its returned \`repoRoot\`. Renew \`task.heartbeat\` during actual work, use \`task.pause\` on handoff, and resume before writing again. Inactivity never permits force checkout or cleanup. Keep closed worktrees until authorized delivery completes. Ask only unanswered shared branch preferences when \`session.entry\` says the user can manage them; otherwise choose a task-specific base.
|
|
23
23
|
|
|
24
24
|
Do not edit until the skill lifecycle has completed discovery, its checkpoint, and \`context.prepare_change\`. Do not claim completion until \`task.verify\` succeeds.
|
|
25
25
|
|
package/package.json
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "engineering-memory",
|
|
3
|
-
"version": "1.11.
|
|
4
|
-
"description": "Installs the Engineering Memory skill and its local MCP bridge. Sign in after installing; your organization and project are resolved from your account.",
|
|
5
|
-
"license": "UNLICENSED",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"engines": {
|
|
8
|
-
"node": ">=20"
|
|
9
|
-
},
|
|
10
|
-
"bin": {
|
|
11
|
-
"engineering-memory": "bin/engineering-memory.mjs"
|
|
12
|
-
},
|
|
13
|
-
"files": [
|
|
14
|
-
"bin",
|
|
15
|
-
"dispatcher",
|
|
16
|
-
"lib",
|
|
17
|
-
"install",
|
|
18
|
-
"skill",
|
|
19
|
-
"runtime"
|
|
20
|
-
],
|
|
21
|
-
"dependencies": {
|
|
22
|
-
"@modelcontextprotocol/server": "2.0.0",
|
|
23
|
-
"minimatch": "10.2.6",
|
|
24
|
-
"zod": "4.4.3"
|
|
25
|
-
},
|
|
26
|
-
"engineeringMemory": {
|
|
27
|
-
"apiUrl": "https://coral-app-zqmj6.ondigitalocean.app"
|
|
28
|
-
}
|
|
29
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "engineering-memory",
|
|
3
|
+
"version": "1.11.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
|
+
}
|
|
@@ -338,13 +338,76 @@ export function registerWorktreeTools(server, service) {
|
|
|
338
338
|
delivered: z.boolean().optional(),
|
|
339
339
|
});
|
|
340
340
|
server.registerTool('worktree.list', {
|
|
341
|
-
description: '
|
|
341
|
+
description: 'Discover existing managed Git checkouts and list activity and protection reasons before allocating another folder. Unregistered legacy checkouts remain protected until explicitly reconciled.',
|
|
342
342
|
inputSchema: locationSchema,
|
|
343
343
|
}, async (input) => output(await service.worktreeOperation('list', input)));
|
|
344
344
|
server.registerTool('worktree.reconcile', {
|
|
345
|
-
description: 'Reconcile Git with the local registry
|
|
346
|
-
inputSchema:
|
|
347
|
-
|
|
345
|
+
description: 'Reconcile Git with the local registry, including unregistered legacy checkouts. Use releaseUnowned only on a selected unreserved recovered checkout after prior work has finished; a native source-bound confirmation and fresh safety checks precede reuse. Never delete files or reset branches.',
|
|
346
|
+
inputSchema: z.strictObject({
|
|
347
|
+
repoRoot: repo,
|
|
348
|
+
releaseUnowned: z.boolean().optional(),
|
|
349
|
+
decisionAttempt: z.number().int().min(0).default(0),
|
|
350
|
+
language: z.enum(['tr', 'en']).default('en'),
|
|
351
|
+
}),
|
|
352
|
+
}, async (input, context) => {
|
|
353
|
+
const inspection = await service.worktreeOperation('reconcile', input);
|
|
354
|
+
if (!inspection.ok || !input.releaseUnowned)
|
|
355
|
+
return output(inspection);
|
|
356
|
+
const data = inspection.data;
|
|
357
|
+
const entry = data.selected;
|
|
358
|
+
if (!entry?.recoveredWithoutReservation || !entry.baseCommit)
|
|
359
|
+
return output({
|
|
360
|
+
ok: false,
|
|
361
|
+
error: {
|
|
362
|
+
kind: 'bridge_error',
|
|
363
|
+
retryable: false,
|
|
364
|
+
recovery: 'worktree.reconcile',
|
|
365
|
+
message: 'Select an unregistered, unreserved checkout from worktree.list. Existing task ownership cannot be released by this recovery.',
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
const digest = sha256(stableStringify({
|
|
369
|
+
projectId: entry.projectId,
|
|
370
|
+
repoRoot: entry.repoRoot,
|
|
371
|
+
generation: entry.generation,
|
|
372
|
+
sourceCommit: entry.baseCommit,
|
|
373
|
+
decisionAttempt: input.decisionAttempt,
|
|
374
|
+
}));
|
|
375
|
+
const questionnaireId = 'worktree-release-unowned-' + digest;
|
|
376
|
+
const form = await askQuestionnaire(server, service, {
|
|
377
|
+
repoRoot: input.repoRoot,
|
|
378
|
+
questionnaireId,
|
|
379
|
+
language: input.language,
|
|
380
|
+
allowFreeText: false,
|
|
381
|
+
message: input.language === 'tr'
|
|
382
|
+
? 'Bu eski çalışma klasörünün önceki sahiplik kaydı yok. Önceki ajan burada artık çalışmıyor ve teslim tamamlandıysa, temiz klasörü yeniden kullanılabilir yapalım mı? Dosyalar ve branch silinmeyecek.'
|
|
383
|
+
: 'This legacy checkout has no prior ownership record. If the previous agent has stopped and delivery is complete, make the clean checkout reusable? No files or branches will be deleted.',
|
|
384
|
+
options: [
|
|
385
|
+
{
|
|
386
|
+
id: 'release_unowned',
|
|
387
|
+
label: input.language === 'tr'
|
|
388
|
+
? 'Önceki iş bitti; güvenli kontrollerden sonra kullanılabilir yap'
|
|
389
|
+
: 'Prior work finished; release after safety checks',
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
id: 'preserve',
|
|
393
|
+
label: input.language === 'tr' ? 'Korumaya devam et' : 'Keep protected',
|
|
394
|
+
},
|
|
395
|
+
],
|
|
396
|
+
}, context);
|
|
397
|
+
const decision = await service.questionnaireResume({
|
|
398
|
+
repoRoot: input.repoRoot,
|
|
399
|
+
questionnaireId,
|
|
400
|
+
});
|
|
401
|
+
if (decision.status !== 'answered' || decision.answer?.choice !== 'release_unowned')
|
|
402
|
+
return form;
|
|
403
|
+
return output(await service.worktreeOperation('release_unowned', {
|
|
404
|
+
repoRoot: input.repoRoot,
|
|
405
|
+
generation: entry.generation,
|
|
406
|
+
sourceCommit: entry.baseCommit,
|
|
407
|
+
decisionAttempt: input.decisionAttempt,
|
|
408
|
+
decisionId: questionnaireId,
|
|
409
|
+
}));
|
|
410
|
+
});
|
|
348
411
|
server.registerTool('task.heartbeat', {
|
|
349
412
|
description: 'Renew this exact task ownership during real activity. Never run an idle timer to keep an abandoned task active.',
|
|
350
413
|
inputSchema: ownershipSchema,
|
|
@@ -1870,12 +1870,33 @@ export class BridgeService {
|
|
|
1870
1870
|
const pool = this.dependencies.worktreePool;
|
|
1871
1871
|
if (!projectId || !pool)
|
|
1872
1872
|
throw refuse('Resolve the project with a pool-capable client.', 'project.resolve');
|
|
1873
|
-
if (operation === 'list')
|
|
1874
|
-
return asJsonValue({ items: await pool.list(projectId, await this.poolPolicy()) });
|
|
1875
|
-
if (operation === 'reconcile')
|
|
1873
|
+
if (operation === 'list' || operation === 'reconcile')
|
|
1876
1874
|
return asJsonValue({
|
|
1877
1875
|
items: await pool.reconcile(projectId, repository.repoRoot, await this.poolPolicy()),
|
|
1876
|
+
selected: await pool.forPath(projectId, repository.repoRoot),
|
|
1878
1877
|
});
|
|
1878
|
+
if (operation === 'release_unowned') {
|
|
1879
|
+
const entry = await pool.forPath(projectId, repository.repoRoot);
|
|
1880
|
+
if (!entry || !input.sourceCommit || !input.generation || !input.decisionId)
|
|
1881
|
+
throw refuse('Inspect the recovered checkout with worktree.reconcile before confirming its release.', 'worktree.reconcile');
|
|
1882
|
+
const digest = sha256(stableStringify({
|
|
1883
|
+
projectId,
|
|
1884
|
+
repoRoot: entry.repoRoot,
|
|
1885
|
+
generation: input.generation,
|
|
1886
|
+
sourceCommit: input.sourceCommit,
|
|
1887
|
+
decisionAttempt: input.decisionAttempt ?? 0,
|
|
1888
|
+
}));
|
|
1889
|
+
if (input.decisionId !== 'worktree-release-unowned-' + digest)
|
|
1890
|
+
throw refuse('The availability decision does not match this checkout and source. Repeat worktree.reconcile.', 'worktree.reconcile');
|
|
1891
|
+
const decision = await this.questionnaireResume({
|
|
1892
|
+
repoRoot: repository.repoRoot,
|
|
1893
|
+
questionnaireId: input.decisionId,
|
|
1894
|
+
});
|
|
1895
|
+
if (decision.status !== 'answered' || decision.answer?.choice !== 'release_unowned')
|
|
1896
|
+
throw refuse('Confirm that prior work and delivery have finished in the native worktree.reconcile form.', 'worktree.reconcile');
|
|
1897
|
+
await pool.releaseUnowned(projectId, repository.repoRoot, input.generation, input.sourceCommit);
|
|
1898
|
+
return asJsonValue({ operation, complete: true, repoRoot: repository.repoRoot });
|
|
1899
|
+
}
|
|
1879
1900
|
const entry = await pool.forPath(projectId, repository.repoRoot);
|
|
1880
1901
|
if (!entry ||
|
|
1881
1902
|
entry.externalTaskId !== input.externalTaskId ||
|
|
@@ -35,6 +35,7 @@ const allocationSchema = z.object({
|
|
|
35
35
|
lastActivityAt: z.number().finite(),
|
|
36
36
|
phase: z.enum(['creating', 'working', 'paused', 'delivery', 'released', 'quarantined']),
|
|
37
37
|
pendingDelivery: z.boolean(),
|
|
38
|
+
recoveredWithoutReservation: z.boolean().optional(),
|
|
38
39
|
reason: z.string().optional(),
|
|
39
40
|
});
|
|
40
41
|
const registrySchema = z.object({
|
|
@@ -74,6 +75,7 @@ export class WorktreePool {
|
|
|
74
75
|
throw new BridgeRecoveryError('Choose a valid literal Git branch name of at most 255 characters. Retry task.branch with the corrected name; no allocation was recorded.', 'task.branch');
|
|
75
76
|
}
|
|
76
77
|
}
|
|
78
|
+
await this.reconcile(input.projectId, input.repoRoot, policy, true);
|
|
77
79
|
const commonDir = await this.commonDirectory(input.repoRoot);
|
|
78
80
|
return this.transaction(async (registry) => {
|
|
79
81
|
if (!registry.projects[input.projectId]) {
|
|
@@ -292,33 +294,83 @@ export class WorktreePool {
|
|
|
292
294
|
await this.save(registry);
|
|
293
295
|
});
|
|
294
296
|
}
|
|
295
|
-
async
|
|
297
|
+
async releaseUnowned(projectId, repoRoot, generation, sourceCommit) {
|
|
298
|
+
await this.transaction(async (registry) => {
|
|
299
|
+
const entry = registry.projects[projectId]?.entries.find((e) => key(e.repoRoot) === key(repoRoot));
|
|
300
|
+
if (!entry ||
|
|
301
|
+
entry.generation !== generation ||
|
|
302
|
+
!entry.recoveredWithoutReservation ||
|
|
303
|
+
entry.phase !== 'quarantined' ||
|
|
304
|
+
entry.owner ||
|
|
305
|
+
entry.taskId ||
|
|
306
|
+
(await this.git.branchStore(repoRoot).read()))
|
|
307
|
+
throw refuse('This is not the same unowned recovered checkout. List and reconcile its current ownership before release.');
|
|
308
|
+
if ((await this.git.sourceIdentity(repoRoot))?.sourceCommit !== sourceCommit ||
|
|
309
|
+
entry.baseCommit !== sourceCommit)
|
|
310
|
+
throw refuse('The recovered checkout source changed. Inspect it again before confirming availability.');
|
|
311
|
+
const reasons = await this.fileProtection(entry);
|
|
312
|
+
if (reasons.length)
|
|
313
|
+
throw refuse('The recovered checkout is protected: ' + reasons.join(', '));
|
|
314
|
+
entry.phase = 'released';
|
|
315
|
+
entry.pendingDelivery = false;
|
|
316
|
+
entry.recoveredWithoutReservation = false;
|
|
317
|
+
entry.generation = randomUUID();
|
|
318
|
+
delete entry.reason;
|
|
319
|
+
await this.save(registry);
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
async reconcile(projectId, repoRoot, policy, discoverOnly = false) {
|
|
296
323
|
await this.restoreRegistry();
|
|
297
324
|
const commonDir = await this.commonDirectory(repoRoot);
|
|
298
325
|
await this.transaction(async (registry) => {
|
|
299
326
|
let project = registry.projects[projectId];
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
327
|
+
const locations = await this.gitPaths(repoRoot);
|
|
328
|
+
const candidates = [];
|
|
329
|
+
const managedRoot = join(await this.documentsDirectory(), 'engineeringmemory');
|
|
330
|
+
for (const location of locations) {
|
|
331
|
+
if (project?.entries.some((entry) => key(entry.repoRoot) === key(location.path)))
|
|
332
|
+
continue;
|
|
333
|
+
if (!(await pathExists(location.path)))
|
|
334
|
+
continue;
|
|
335
|
+
const parent = dirname(location.path);
|
|
336
|
+
const folder = dirname(parent).split(/[\\/]/).pop();
|
|
337
|
+
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)
|
|
346
|
+
continue;
|
|
347
|
+
try {
|
|
348
|
+
await assertManagedPath(managedRoot, location.path, false);
|
|
349
|
+
if ((await this.commonDirectory(location.path)) !== commonDir)
|
|
311
350
|
continue;
|
|
312
|
-
registry.projects[projectId] = { folder, entries: [] };
|
|
313
|
-
break;
|
|
314
351
|
}
|
|
352
|
+
catch {
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
const reservation = await this.git.branchStore(location.path).read();
|
|
356
|
+
if (reservation && reservation.decision.projectId !== projectId)
|
|
357
|
+
continue;
|
|
358
|
+
if (Object.entries(registry.projects).some(([id, value]) => id !== projectId && key(value.folder) === key(folder)))
|
|
359
|
+
continue;
|
|
360
|
+
candidates.push({ path: location.path, folder, slot });
|
|
361
|
+
}
|
|
362
|
+
if (!project) {
|
|
363
|
+
const folders = [...new Set(candidates.map((candidate) => candidate.folder))];
|
|
364
|
+
if (folders.length > 1)
|
|
365
|
+
throw refuse('Several managed folders belong to this clone. Restore the project index to identify its original folder before allocating a directory.');
|
|
366
|
+
if (folders.length === 1)
|
|
367
|
+
registry.projects[projectId] = { folder: folders[0], entries: [] };
|
|
315
368
|
}
|
|
316
369
|
const recoveredProject = registry.projects[projectId];
|
|
317
370
|
if (!recoveredProject)
|
|
318
371
|
return;
|
|
319
372
|
project = recoveredProject;
|
|
320
|
-
const
|
|
321
|
-
for (const entry of project.entries.filter((e) => e.commonDir === commonDir)) {
|
|
373
|
+
for (const entry of project.entries.filter((e) => !discoverOnly && e.commonDir === commonDir)) {
|
|
322
374
|
if (entry.owner && entry.owner.id !== this.ownerId && (await this.ownerAlive(entry.owner)))
|
|
323
375
|
continue;
|
|
324
376
|
if (await pathExists(entry.repoRoot))
|
|
@@ -337,48 +389,54 @@ export class WorktreePool {
|
|
|
337
389
|
}
|
|
338
390
|
}
|
|
339
391
|
}
|
|
340
|
-
for (const location of
|
|
392
|
+
for (const location of candidates.filter((candidate) => key(candidate.folder) === key(project.folder))) {
|
|
341
393
|
if (project.entries.some((e) => key(e.repoRoot) === key(location.path)))
|
|
342
394
|
continue;
|
|
343
|
-
const parent = dirname(location.path);
|
|
344
|
-
if (dirname(parent)
|
|
345
|
-
.split(/[\\\\/]/)
|
|
346
|
-
.pop() !== project.folder ||
|
|
347
|
-
parent.split(/[\\\\/]/).pop() !== 'worktrees')
|
|
348
|
-
continue;
|
|
349
395
|
const reservation = await this.git.branchStore(location.path).read();
|
|
350
|
-
if (reservation
|
|
396
|
+
if (reservation && reservation.decision.projectId !== projectId)
|
|
351
397
|
continue;
|
|
352
398
|
const source = await this.git.sourceIdentity(location.path);
|
|
353
|
-
|
|
354
|
-
.split(/[\\\\/]/)
|
|
355
|
-
.pop()
|
|
356
|
-
?.match(/-worktree([0-9]+)$/)?.[1];
|
|
357
|
-
if (!source || !slotText)
|
|
399
|
+
if (!source)
|
|
358
400
|
continue;
|
|
359
401
|
project.entries.push({
|
|
360
402
|
projectId,
|
|
361
403
|
repoFingerprint: await this.git.fingerprint(repoRoot),
|
|
362
404
|
commonDir,
|
|
363
405
|
repoRoot: await canonicalPath(location.path),
|
|
364
|
-
slot:
|
|
406
|
+
slot: location.slot,
|
|
365
407
|
managed: true,
|
|
366
|
-
externalTaskId: reservation
|
|
367
|
-
|
|
368
|
-
|
|
408
|
+
externalTaskId: reservation?.decision.externalTaskId ??
|
|
409
|
+
'unregistered-' + sha256(key(location.path)).slice(0, 20),
|
|
410
|
+
taskId: reservation?.decision.taskId,
|
|
411
|
+
branch: reservation?.decision.branch ?? (await this.git.currentBranch(location.path)),
|
|
412
|
+
recoveredWithoutReservation: !reservation,
|
|
369
413
|
baseCommit: source.sourceCommit,
|
|
370
414
|
owner: null,
|
|
371
415
|
generation: randomUUID(),
|
|
372
416
|
lastActivityAt: 0,
|
|
373
417
|
phase: 'quarantined',
|
|
374
418
|
pendingDelivery: true,
|
|
375
|
-
reason:
|
|
419
|
+
reason: reservation
|
|
420
|
+
? 'Recovered a Git reservation missing from the registry. Restore its original task source and delivery state before reuse.'
|
|
421
|
+
: 'Unregistered checkout: prior ownership and delivery are unknown. After prior work has finished, use worktree.reconcile with releaseUnowned through its native confirmation.',
|
|
376
422
|
});
|
|
377
423
|
}
|
|
378
|
-
for (const entry of project.entries.filter((e) => e.commonDir === commonDir)) {
|
|
424
|
+
for (const entry of project.entries.filter((e) => !discoverOnly && e.commonDir === commonDir)) {
|
|
379
425
|
if (entry.owner && (await this.ownerAlive(entry.owner)) && entry.owner.id !== this.ownerId)
|
|
380
426
|
continue;
|
|
381
427
|
try {
|
|
428
|
+
if (entry.recoveredWithoutReservation &&
|
|
429
|
+
!entry.owner &&
|
|
430
|
+
!entry.taskId &&
|
|
431
|
+
!(await this.git.branchStore(entry.repoRoot).read())) {
|
|
432
|
+
const source = await this.git.sourceIdentity(entry.repoRoot);
|
|
433
|
+
const branch = await this.git.currentBranch(entry.repoRoot);
|
|
434
|
+
if (source && (source.sourceCommit !== entry.baseCommit || branch !== entry.branch)) {
|
|
435
|
+
entry.baseCommit = source.sourceCommit;
|
|
436
|
+
entry.branch = branch;
|
|
437
|
+
entry.generation = randomUUID();
|
|
438
|
+
}
|
|
439
|
+
}
|
|
382
440
|
await this.repairCreating(entry);
|
|
383
441
|
if (await pathExists(entry.repoRoot))
|
|
384
442
|
await this.assertIdentity(entry);
|
|
@@ -391,7 +449,7 @@ export class WorktreePool {
|
|
|
391
449
|
}
|
|
392
450
|
await this.save(registry);
|
|
393
451
|
});
|
|
394
|
-
return this.list(projectId, policy);
|
|
452
|
+
return discoverOnly ? [] : this.list(projectId, policy);
|
|
395
453
|
}
|
|
396
454
|
async entry(input, commonDir, repoRoot, slot, managed) {
|
|
397
455
|
const generation = randomUUID();
|
|
@@ -254,3 +254,43 @@ version is not automatically raised; projects opt in by starting a source-bound
|
|
|
254
254
|
## Offline source context
|
|
255
255
|
|
|
256
256
|
An unexpired cached context pack may be used for implementation. The bridge records checkpoints in its outbox. Permanent proposal review, strict verification, task close, and Git commit require the backend and synchronization of the relevant task deliveries. An unrelated task or repository queue does not block this task; account switching and logout still require all deliveries to be resolved. Commit receipts are retained per task and worktree and still require an online attestation for the selected closed task and exact staged content.
|
|
257
|
+
|
|
258
|
+
## Reuse before allocating another directory
|
|
259
|
+
|
|
260
|
+
Before starting a write task, call `worktree.list` with the bound repository. It reconciles managed
|
|
261
|
+
Git checkouts with local records. Then let `task.branch` choose the smallest safe slot; do not guess
|
|
262
|
+
a new worktree number or create a directory yourself. Allocation also performs discovery when the
|
|
263
|
+
caller omitted the listing. Old unregistered managed checkouts count toward the limit and remain
|
|
264
|
+
protected because their previous ownership and delivery are unknown. A clean checkout alone is
|
|
265
|
+
insufficient proof of availability.
|
|
266
|
+
|
|
267
|
+
When prior work has finished, call `worktree.reconcile` on that checkout with `releaseUnowned:true`
|
|
268
|
+
and the conversation language. Its native question binds the decision to the checkout, source and
|
|
269
|
+
ownership generation. Cancellation leaves it protected. Git reservation, dirty files, unfinished
|
|
270
|
+
Git operations and pending work are checked again before release. A changed source requires a new
|
|
271
|
+
inspection and answer. This recovery never deletes files or branches or interrupts another agent.
|
|
272
|
+
|
|
273
|
+
If the running client predates the pool tools, inspect `git worktree list` and the existing task
|
|
274
|
+
reservations before any explicit allocation. Do not treat an unavailable tool or an empty registry
|
|
275
|
+
as an empty filesystem. Preserve uncertain checkouts and report the client limitation.
|
|
276
|
+
|
|
277
|
+
## Network and generation evidence
|
|
278
|
+
|
|
279
|
+
`network` means tests of the changed network/contract behavior, not a connection to a live provider.
|
|
280
|
+
Use local contract tests and controlled responses for the applicable success, errors, timeout,
|
|
281
|
+
retry and cancellation behavior. Report the real runner command, for example
|
|
282
|
+
`mvn -B -Dtest=CardDepositServiceTest test` or `./gradlew test --tests CardDepositServiceTest`;
|
|
283
|
+
the class name need not contain a special keyword. Report `network` only when that suite actually
|
|
284
|
+
covers the changed contract. A command match cannot establish behavioral coverage by itself.
|
|
285
|
+
Source searches, static analyzers, `echo` and a successful `curl` request do not replace these tests.
|
|
286
|
+
Never rename a command or invent a passing result to satisfy a category.
|
|
287
|
+
|
|
288
|
+
Handwritten files under `service`/`services` do not automatically require a separate network category,
|
|
289
|
+
and handwritten `model`/`models` files do not automatically require code generation. Explicit
|
|
290
|
+
network paths and generated `.g.dart`/`.freezed.dart` files retain their gates. On code tasks,
|
|
291
|
+
additional applicable `network` and `codegen` evidence is accepted; all submitted evidence must pass.
|
|
292
|
+
Read-only validation requirements are unchanged. Test commands that explicitly skip tests cannot
|
|
293
|
+
serve as test or network evidence. A live environment is a separate check when the task requires it.
|
|
294
|
+
|
|
295
|
+
If the user later reconsiders a preserved legacy checkout, repeat `worktree.reconcile` with a new
|
|
296
|
+
`decisionAttempt`; never reinterpret the previous preserve answer as approval.
|
|
@@ -262,3 +262,16 @@ withdrawing it. Language changes never broaden the approved source, mode or prev
|
|
|
262
262
|
Shared preferences use separate native decisions for development, production and test. Explicitly absent production/test values are answers. Members without canManage receive a task-specific branch choice, never an administrator question they cannot apply. The same task's accepted decision stays valid across Codex/Claude handoff after pause; return to session.resume and the recorded repoRoot.
|
|
263
263
|
|
|
264
264
|
For task.branch and project.set_git_preferences, retries keep the same decisionAttempt (omit it on the first attempt). When the user explicitly reconsiders an answered deferral, use a new positive decisionAttempt to open a fresh native decision. Never reinterpret the old deferral as consent.
|
|
265
|
+
|
|
266
|
+
## Legacy worktree availability
|
|
267
|
+
|
|
268
|
+
Use `worktree.list` before deciding another directory is needed. An unregistered legacy checkout
|
|
269
|
+
has unknown prior ownership and delivery, even if Git reports clean. `worktree.reconcile` with
|
|
270
|
+
`releaseUnowned:true` and `language` opens the native availability question for that exact checkout,
|
|
271
|
+
source commit and generation. The user confirms prior work and delivery have finished; current Git
|
|
272
|
+
safety checks still decide whether reuse is possible. Cancellation or choosing to preserve does not
|
|
273
|
+
release it. Resume the same form via the same operation. A changed source/generation needs a fresh
|
|
274
|
+
decision, and an old answer cannot release a newly owned task.
|
|
275
|
+
|
|
276
|
+
If the user later reconsiders a preserved legacy checkout, repeat `worktree.reconcile` with a new
|
|
277
|
+
`decisionAttempt`; never reinterpret the previous preserve answer as approval.
|