engineering-memory 1.8.0 → 1.9.0
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 +2 -0
- package/package.json +1 -1
- package/runtime/dist/src/git/git-inspector.js +91 -0
- package/runtime/dist/src/git/pre-commit.js +16 -0
- package/runtime/dist/src/git/verification-gate.js +14 -1
- package/runtime/dist/src/mcp/tool-definitions.js +11 -0
- package/runtime/dist/src/runtime/api-client.js +1 -0
- package/runtime/dist/src/runtime/bridge-service.js +180 -0
- package/runtime/dist/src/runtime/task-branch-store.js +111 -0
- package/skill/SKILL.md +2 -0
- package/skill/references/lifecycle.md +4 -2
- package/skill/references/questionnaires.md +2 -2
- package/bin/entry-point.test.mjs +0 -29
package/dispatcher/sections.mjs
CHANGED
|
@@ -21,6 +21,8 @@ After compaction, a new chat, interruption, or handoff, call \`session.resume\`
|
|
|
21
21
|
|
|
22
22
|
Do not edit until the skill lifecycle has completed discovery, its checkpoint, and \`context.prepare_change\`. Do not claim completion until \`task.verify\` succeeds.
|
|
23
23
|
|
|
24
|
+
Use the host's native questionnaire for every question to the user, including implementation choices, names, clarification, branch/worktree decisions and delivery. In Codex use request_user_input when available; in Claude use AskUserQuestion. 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. Existing answers remain valid through retries and handoffs.
|
|
25
|
+
|
|
24
26
|
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.`;
|
|
25
27
|
|
|
26
28
|
export const dispatcherSections = Object.freeze({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "engineering-memory",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
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
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -3,6 +3,7 @@ import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
|
3
3
|
import { assertManagedPath, canonicalPath } from '../utilities/files.js';
|
|
4
4
|
import { sha256, stableStringify } from '../utilities/hash.js';
|
|
5
5
|
import { NativeCommandRunner } from '../utilities/process.js';
|
|
6
|
+
import { TaskBranchStore } from '../runtime/task-branch-store.js';
|
|
6
7
|
export const temporaryScaffoldingMarker = 'ENGINEERING-MEMORY-TEMPORARY';
|
|
7
8
|
export const temporaryScaffoldingPattern = `${temporaryScaffoldingMarker}:`;
|
|
8
9
|
export class GitInspector {
|
|
@@ -33,6 +34,96 @@ export class GitInspector {
|
|
|
33
34
|
}
|
|
34
35
|
return sha256(identity);
|
|
35
36
|
}
|
|
37
|
+
branchStore(repoRoot) {
|
|
38
|
+
return new TaskBranchStore(repoRoot, this.runner);
|
|
39
|
+
}
|
|
40
|
+
async validateBranch(repoRoot, name) {
|
|
41
|
+
const result = await this.runner.run('git', ['check-ref-format', '--branch', name], {
|
|
42
|
+
cwd: repoRoot,
|
|
43
|
+
});
|
|
44
|
+
if (name.length > 255 || result.exitCode !== 0 || result.stdout.trim() !== name)
|
|
45
|
+
throw new Error('Choose a valid literal Git branch name of at most 255 characters');
|
|
46
|
+
}
|
|
47
|
+
async createWorktree(repoRoot, name, target) {
|
|
48
|
+
await this.validateBranch(repoRoot, name);
|
|
49
|
+
const path = resolve(repoRoot, target);
|
|
50
|
+
const present = await lstat(path).then(() => true, (error) => {
|
|
51
|
+
if (error.code === 'ENOENT')
|
|
52
|
+
return false;
|
|
53
|
+
throw error;
|
|
54
|
+
});
|
|
55
|
+
if (present) {
|
|
56
|
+
const root = await this.findRoot(path);
|
|
57
|
+
const common = await this.gitValue(root, [
|
|
58
|
+
'rev-parse',
|
|
59
|
+
'--path-format=absolute',
|
|
60
|
+
'--git-common-dir',
|
|
61
|
+
]);
|
|
62
|
+
const sourceCommon = await this.gitValue(repoRoot, [
|
|
63
|
+
'rev-parse',
|
|
64
|
+
'--path-format=absolute',
|
|
65
|
+
'--git-common-dir',
|
|
66
|
+
]);
|
|
67
|
+
if (root === (await canonicalPath(path)) &&
|
|
68
|
+
common === sourceCommon &&
|
|
69
|
+
(await this.currentBranch(root)) === name)
|
|
70
|
+
return root;
|
|
71
|
+
throw new Error('The worktree directory already belongs to another repository or branch. Choose a different directory');
|
|
72
|
+
}
|
|
73
|
+
const exists = await this.branchExists(repoRoot, name);
|
|
74
|
+
const args = exists
|
|
75
|
+
? ['worktree', 'add', '--', path, name]
|
|
76
|
+
: ['worktree', 'add', '-b', name, '--', path];
|
|
77
|
+
const result = await this.runner.run('git', args, { cwd: repoRoot });
|
|
78
|
+
if (result.exitCode !== 0)
|
|
79
|
+
throw new Error(`Git could not create the worktree: ${result.stderr.trim()}`);
|
|
80
|
+
return await this.findRoot(path);
|
|
81
|
+
}
|
|
82
|
+
async currentBranch(repoRoot) {
|
|
83
|
+
return await this.gitValue(repoRoot, ['symbolic-ref', '--short', '-q', 'HEAD']);
|
|
84
|
+
}
|
|
85
|
+
async defaultBranch(repoRoot) {
|
|
86
|
+
const ref = await this.gitValue(repoRoot, [
|
|
87
|
+
'symbolic-ref',
|
|
88
|
+
'--short',
|
|
89
|
+
'-q',
|
|
90
|
+
'refs/remotes/origin/HEAD',
|
|
91
|
+
]);
|
|
92
|
+
return ref ? ref.replace(/^origin\//, '') : null;
|
|
93
|
+
}
|
|
94
|
+
async protectedBranchNames(repoRoot) {
|
|
95
|
+
const names = new Set(['main', 'master', 'develop', 'prod']);
|
|
96
|
+
const fallback = await this.defaultBranch(repoRoot);
|
|
97
|
+
if (fallback)
|
|
98
|
+
names.add(fallback);
|
|
99
|
+
return names;
|
|
100
|
+
}
|
|
101
|
+
async createBranch(repoRoot, name) {
|
|
102
|
+
await this.validateBranch(repoRoot, name);
|
|
103
|
+
const result = await this.runner.run('git', ['switch', '-c', name], { cwd: repoRoot });
|
|
104
|
+
if (result.exitCode !== 0) {
|
|
105
|
+
throw new Error(`Git could not create the branch ${name}: ${result.stderr.trim() || 'unknown error'}`);
|
|
106
|
+
}
|
|
107
|
+
const branch = await this.currentBranch(repoRoot);
|
|
108
|
+
if (branch !== name) {
|
|
109
|
+
throw new Error(`Git created ${name} but HEAD is on ${branch ?? 'no branch'}`);
|
|
110
|
+
}
|
|
111
|
+
return branch;
|
|
112
|
+
}
|
|
113
|
+
async branchExists(repoRoot, name) {
|
|
114
|
+
const result = await this.runner.run('git', ['rev-parse', '--verify', '--quiet', `refs/heads/${name}`], {
|
|
115
|
+
cwd: repoRoot,
|
|
116
|
+
});
|
|
117
|
+
return result.exitCode === 0;
|
|
118
|
+
}
|
|
119
|
+
async switchBranch(repoRoot, name) {
|
|
120
|
+
await this.validateBranch(repoRoot, name);
|
|
121
|
+
const result = await this.runner.run('git', ['switch', '--', name], { cwd: repoRoot });
|
|
122
|
+
if (result.exitCode !== 0) {
|
|
123
|
+
throw new Error(`Git could not switch to the branch ${name}: ${result.stderr.trim() || 'unknown error'}`);
|
|
124
|
+
}
|
|
125
|
+
return name;
|
|
126
|
+
}
|
|
36
127
|
async mainWorktree(repoRoot) {
|
|
37
128
|
const commonDirectory = await this.gitValue(repoRoot, [
|
|
38
129
|
'rev-parse',
|
|
@@ -35,8 +35,24 @@ async function run() {
|
|
|
35
35
|
})).data);
|
|
36
36
|
const result = await gate.verify(repoRoot);
|
|
37
37
|
process.stderr.write(`${JSON.stringify(result)}\n`);
|
|
38
|
+
const advice = describe(result);
|
|
39
|
+
if (advice)
|
|
40
|
+
process.stderr.write(`${advice}\n`);
|
|
38
41
|
process.exitCode = result.allowed ? 0 : 1;
|
|
39
42
|
}
|
|
43
|
+
function describe(result) {
|
|
44
|
+
const [expected, actual] = result.details ?? [];
|
|
45
|
+
switch (result.reason) {
|
|
46
|
+
case 'branch_mismatch':
|
|
47
|
+
return `The task selected ${expected} but HEAD is on ${actual}. Run: git switch ${expected}`;
|
|
48
|
+
case 'branch_detached':
|
|
49
|
+
return `The task selected ${expected} but HEAD is detached. Run: git switch ${expected}`;
|
|
50
|
+
case 'online_commit_gate_rejected':
|
|
51
|
+
return `The backend refused the commit: ${expected ?? 'no reason given'}`;
|
|
52
|
+
default:
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
40
56
|
void run().catch(() => {
|
|
41
57
|
process.stderr.write('Engineering Memory Git gate failed\n');
|
|
42
58
|
process.exitCode = 1;
|
|
@@ -32,6 +32,7 @@ export class VerificationGate {
|
|
|
32
32
|
taskClosed: input.taskClosed ?? false,
|
|
33
33
|
taskVersion: input.taskVersion ?? null,
|
|
34
34
|
taskChanges,
|
|
35
|
+
branch: input.branch ?? null,
|
|
35
36
|
}, this.root);
|
|
36
37
|
}
|
|
37
38
|
async verify(repoRoot) {
|
|
@@ -79,6 +80,16 @@ export class VerificationGate {
|
|
|
79
80
|
taskId: receipt.taskId,
|
|
80
81
|
};
|
|
81
82
|
}
|
|
83
|
+
const branch = await this.git.currentBranch(repoRoot);
|
|
84
|
+
if (receipt.branch && branch !== receipt.branch) {
|
|
85
|
+
return {
|
|
86
|
+
allowed: false,
|
|
87
|
+
reason: branch ? 'branch_mismatch' : 'branch_detached',
|
|
88
|
+
diffHash: manifest.diffHash,
|
|
89
|
+
taskId: receipt.taskId,
|
|
90
|
+
details: [receipt.branch, branch ?? 'HEAD'],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
82
93
|
if (receipt.diffHash !== manifest.diffHash) {
|
|
83
94
|
return {
|
|
84
95
|
allowed: false,
|
|
@@ -118,6 +129,7 @@ export class VerificationGate {
|
|
|
118
129
|
repoFingerprint: receipt.repoFingerprint,
|
|
119
130
|
diffHash: manifest.diffHash,
|
|
120
131
|
expectedTaskVersion: receipt.taskVersion,
|
|
132
|
+
branch: branch ?? undefined,
|
|
121
133
|
});
|
|
122
134
|
const result = asObject(attestation);
|
|
123
135
|
if (result?.allowed !== true ||
|
|
@@ -132,12 +144,13 @@ export class VerificationGate {
|
|
|
132
144
|
};
|
|
133
145
|
}
|
|
134
146
|
}
|
|
135
|
-
catch {
|
|
147
|
+
catch (error) {
|
|
136
148
|
return {
|
|
137
149
|
allowed: false,
|
|
138
150
|
reason: 'online_commit_gate_rejected',
|
|
139
151
|
diffHash: manifest.diffHash,
|
|
140
152
|
taskId: receipt.taskId,
|
|
153
|
+
details: [error instanceof Error ? error.message : String(error)],
|
|
141
154
|
};
|
|
142
155
|
}
|
|
143
156
|
return {
|
|
@@ -66,6 +66,7 @@ export const engineeringMemoryToolNames = [
|
|
|
66
66
|
'task.verify',
|
|
67
67
|
'task.close',
|
|
68
68
|
'task.abandon',
|
|
69
|
+
'task.branch',
|
|
69
70
|
'architecture.plan',
|
|
70
71
|
'architecture.module',
|
|
71
72
|
'architecture.record_application',
|
|
@@ -428,6 +429,16 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
428
429
|
reason: z.string().min(1).max(2000),
|
|
429
430
|
}),
|
|
430
431
|
}, async (input) => toolResult(await service.taskAbandon(input)));
|
|
432
|
+
server.registerTool('task.branch', {
|
|
433
|
+
description: 'Record the native questionnaire answer for the exact externalTaskId before any new write/scaffold task or read-only-to-write transition. Choose name or keepCurrent. For another task in a busy checkout supply worktreePath and name; use the returned repoRoot for all subsequent commands. Existing branches may be selected without resetting them. Decisions persist across retries and restarts.',
|
|
434
|
+
inputSchema: z.object({
|
|
435
|
+
repoRoot: optionalRepoRoot,
|
|
436
|
+
externalTaskId: z.string().min(2).max(160),
|
|
437
|
+
worktreePath: z.string().min(1).optional(),
|
|
438
|
+
name: z.string().min(1).max(255).optional(),
|
|
439
|
+
keepCurrent: z.boolean().optional(),
|
|
440
|
+
}),
|
|
441
|
+
}, async (input) => toolResult(await service.taskBranch(input)));
|
|
431
442
|
server.registerTool('organization.create', {
|
|
432
443
|
description: 'Create an organization the user named, with the short identifier they typed. Offer this underneath the organizations they already belong to; never invent the identifier from the name.',
|
|
433
444
|
inputSchema: z.object({
|
|
@@ -3,6 +3,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { minimatch } from 'minimatch';
|
|
5
5
|
import { endpoints } from '../config.js';
|
|
6
|
+
import { RepositoryResolver } from '../project/repository.js';
|
|
6
7
|
import { sha256, stableStringify } from '../utilities/hash.js';
|
|
7
8
|
import { ApiResponseError, BackendUnavailableError, } from './api-client.js';
|
|
8
9
|
import { assertSafeToPersist, normalizeRepositoryPaths } from './offline-outbox.js';
|
|
@@ -90,6 +91,11 @@ export class BridgeService {
|
|
|
90
91
|
if (!projectId) {
|
|
91
92
|
throw refuse('This repository has a marker that names no project, so there is nothing to bootstrap against.', 'project.resolve');
|
|
92
93
|
}
|
|
94
|
+
const bootstrapMode = persistedBootstrap.mode ?? 'write';
|
|
95
|
+
const decision = bootstrapMode === 'read_only'
|
|
96
|
+
? null
|
|
97
|
+
: await this.requireBranchDecision(repository, input.externalTaskId);
|
|
98
|
+
const branch = decision?.branch;
|
|
93
99
|
const checkpointId = deterministicUuid('session.bootstrap', projectId, repository.repoFingerprint, input.externalTaskId);
|
|
94
100
|
const response = await this.dependencies.client.request(endpoints.sessionBootstrap, {
|
|
95
101
|
method: 'POST',
|
|
@@ -106,6 +112,7 @@ export class BridgeService {
|
|
|
106
112
|
...(input.mode === 'read_only'
|
|
107
113
|
? { baselineDiffHash: repository.git.worktreeHash }
|
|
108
114
|
: {}),
|
|
115
|
+
...(decision ? { branch } : {}),
|
|
109
116
|
checkpointIdempotencyKey: checkpointId,
|
|
110
117
|
knownRevisions: persistedBootstrap.knownRevisions,
|
|
111
118
|
}),
|
|
@@ -127,11 +134,20 @@ export class BridgeService {
|
|
|
127
134
|
!isIsoTimestamp(checkpointPayload.createdAt)) {
|
|
128
135
|
throw refuse('The backend opened the session without the checkpoint the task is anchored to.', 'session.bootstrap');
|
|
129
136
|
}
|
|
137
|
+
if (decision)
|
|
138
|
+
await this.dependencies.repositories.git
|
|
139
|
+
.branchStore(repository.repoRoot)
|
|
140
|
+
.bind(projectId, input.externalTaskId, task.id);
|
|
130
141
|
const pointer = {
|
|
131
142
|
repoFingerprint: repository.repoFingerprint,
|
|
132
143
|
projectId,
|
|
133
144
|
taskId: task.id,
|
|
134
145
|
taskSlug: input.externalTaskId,
|
|
146
|
+
worktreeId: sha256(repository.repoRoot),
|
|
147
|
+
...(decision
|
|
148
|
+
? { branch: typeof task.branch === 'string' ? task.branch : decision.branch }
|
|
149
|
+
: {}),
|
|
150
|
+
mode: normalizeTaskMode(task.mode),
|
|
135
151
|
sessionId: session.id,
|
|
136
152
|
lastSequence: numericSequence(task.lastSequence),
|
|
137
153
|
taskVersion: numericTaskVersion(task.lockVersion),
|
|
@@ -256,6 +272,12 @@ export class BridgeService {
|
|
|
256
272
|
backendSession.id !== sessionId) {
|
|
257
273
|
throw refuse('The backend resumed the session without the task it belongs to.', 'session.resume');
|
|
258
274
|
}
|
|
275
|
+
await this.assertTaskWorktree(repository, backendTask, pointer);
|
|
276
|
+
if (backendTask.status === 'closed' || backendTask.status === 'abandoned') {
|
|
277
|
+
await this.dependencies.repositories.git
|
|
278
|
+
.branchStore(repository.repoRoot)
|
|
279
|
+
.release(backendTask.id);
|
|
280
|
+
}
|
|
259
281
|
let localJournal = await this.dependencies.journal.load(projectId, taskSlug);
|
|
260
282
|
let pendingOutbox = await this.dependencies.outbox.list();
|
|
261
283
|
const responseSource = this.dependencies.client.getResponseSource(response);
|
|
@@ -365,6 +387,13 @@ export class BridgeService {
|
|
|
365
387
|
projectId,
|
|
366
388
|
taskId: backendTask.id,
|
|
367
389
|
taskSlug,
|
|
390
|
+
worktreeId: pointer?.worktreeId ?? sha256(repository.repoRoot),
|
|
391
|
+
...(typeof backendTask.branch === 'string'
|
|
392
|
+
? { branch: backendTask.branch }
|
|
393
|
+
: pointer?.branch !== undefined
|
|
394
|
+
? { branch: pointer.branch }
|
|
395
|
+
: {}),
|
|
396
|
+
mode: normalizeTaskMode(backendTask.mode),
|
|
368
397
|
sessionId,
|
|
369
398
|
lastSequence: resumedLastSequence,
|
|
370
399
|
taskVersion: resumedTaskVersion,
|
|
@@ -443,6 +472,10 @@ export class BridgeService {
|
|
|
443
472
|
...input.changedPaths,
|
|
444
473
|
]);
|
|
445
474
|
const baselineDiffHash = activeBaseline?.diffHash ?? repository.git.diffHash;
|
|
475
|
+
const decision = pointer &&
|
|
476
|
+
(input.transitionToWrite || (pointer.branch !== undefined && pointer.mode !== 'read_only'))
|
|
477
|
+
? await this.requireBranchDecision(repository, pointer.taskSlug)
|
|
478
|
+
: null;
|
|
446
479
|
let transitionTaskVersion;
|
|
447
480
|
if (input.transitionToWrite) {
|
|
448
481
|
if (!pointer || pointer.sessionId !== input.sessionId) {
|
|
@@ -455,6 +488,7 @@ export class BridgeService {
|
|
|
455
488
|
sessionId: input.sessionId,
|
|
456
489
|
changedPaths,
|
|
457
490
|
baselineDiffHash,
|
|
491
|
+
...(decision ? { branch: decision.branch } : {}),
|
|
458
492
|
...(input.transitionToWrite
|
|
459
493
|
? { transitionToWrite: true, expectedTaskVersion: transitionTaskVersion }
|
|
460
494
|
: {}),
|
|
@@ -501,6 +535,17 @@ export class BridgeService {
|
|
|
501
535
|
: undefined);
|
|
502
536
|
const preparedPointer = await this.dependencies.activeContexts.loadForSession(repository.repoFingerprint, input.sessionId);
|
|
503
537
|
if (preparedPointer) {
|
|
538
|
+
if (decision) {
|
|
539
|
+
await this.dependencies.repositories.git
|
|
540
|
+
.branchStore(repository.repoRoot)
|
|
541
|
+
.bind(preparedPointer.projectId, preparedPointer.taskSlug, preparedPointer.taskId);
|
|
542
|
+
await this.dependencies.activeContexts.save({
|
|
543
|
+
...preparedPointer,
|
|
544
|
+
worktreeId: sha256(repository.repoRoot),
|
|
545
|
+
branch: typeof preparedTask?.branch === 'string' ? preparedTask.branch : decision.branch,
|
|
546
|
+
mode: 'write',
|
|
547
|
+
});
|
|
548
|
+
}
|
|
504
549
|
const preparedData = objectValue(response.data);
|
|
505
550
|
await this.seedResumeSnapshot(preparedPointer, {
|
|
506
551
|
...objectOrEmpty(response.data),
|
|
@@ -1010,6 +1055,7 @@ export class BridgeService {
|
|
|
1010
1055
|
});
|
|
1011
1056
|
}
|
|
1012
1057
|
const taskMode = normalizeTaskMode(snapshotTask.mode);
|
|
1058
|
+
await this.assertTaskWorktree(repository, snapshotTask, pointer);
|
|
1013
1059
|
const resourceDiscoveryPolicy = readResourceDiscoveryPolicy(snapshot, activeLease);
|
|
1014
1060
|
const validations = normalizePersistentInput(input.validations, repository.repoRoot);
|
|
1015
1061
|
if (taskMode === 'read_only') {
|
|
@@ -1072,6 +1118,11 @@ export class BridgeService {
|
|
|
1072
1118
|
changedPaths: taskMode === 'read_only' ? [] : taskChangedPaths,
|
|
1073
1119
|
pathChanges: pathChangeManifest,
|
|
1074
1120
|
diffHash: repository.git.diffHash,
|
|
1121
|
+
...(taskMode !== 'read_only'
|
|
1122
|
+
? {
|
|
1123
|
+
branch: await this.dependencies.repositories.git.currentBranch(repository.repoRoot),
|
|
1124
|
+
}
|
|
1125
|
+
: {}),
|
|
1075
1126
|
...(taskMode === 'read_only' ? { worktreeHash: repository.git.worktreeHash } : {}),
|
|
1076
1127
|
validations,
|
|
1077
1128
|
pendingOutboxCount,
|
|
@@ -1105,6 +1156,9 @@ export class BridgeService {
|
|
|
1105
1156
|
repoFingerprint: repository.repoFingerprint,
|
|
1106
1157
|
repoRoot: repository.repoRoot,
|
|
1107
1158
|
diffHash: repository.git.diffHash,
|
|
1159
|
+
branch: typeof snapshotTask.branch === 'string'
|
|
1160
|
+
? snapshotTask.branch
|
|
1161
|
+
: (pointer.branch ?? null),
|
|
1108
1162
|
taskVersion,
|
|
1109
1163
|
taskChanges,
|
|
1110
1164
|
});
|
|
@@ -1148,11 +1202,17 @@ export class BridgeService {
|
|
|
1148
1202
|
const taskChanges = pointer.changeBaseline
|
|
1149
1203
|
? manifestDelta(pointer.changeBaseline.changedPaths, repository.git.changedPaths)
|
|
1150
1204
|
: [];
|
|
1205
|
+
await this.assertTaskWorktree(repository, task, pointer);
|
|
1151
1206
|
const closeBody = {
|
|
1152
1207
|
taskId: input.taskId,
|
|
1153
1208
|
sessionId: pointer.sessionId,
|
|
1154
1209
|
expectedTaskVersion: numericTaskVersion(task.lockVersion),
|
|
1155
1210
|
diffHash: repository.git.diffHash,
|
|
1211
|
+
...(normalizeTaskMode(task.mode) !== 'read_only'
|
|
1212
|
+
? {
|
|
1213
|
+
branch: await this.dependencies.repositories.git.currentBranch(repository.repoRoot),
|
|
1214
|
+
}
|
|
1215
|
+
: {}),
|
|
1156
1216
|
};
|
|
1157
1217
|
await this.dependencies.activeContexts.setCloseIntent(repository.repoFingerprint, {
|
|
1158
1218
|
body: closeBody,
|
|
@@ -1162,6 +1222,111 @@ export class BridgeService {
|
|
|
1162
1222
|
});
|
|
1163
1223
|
});
|
|
1164
1224
|
}
|
|
1225
|
+
async taskBranch(input) {
|
|
1226
|
+
return await this.execute(async () => {
|
|
1227
|
+
const repository = await this.dependencies.repositories.resolve(input.repoRoot ?? process.cwd());
|
|
1228
|
+
const authentication = await this.dependencies.browserAuth.ensureAuthenticated();
|
|
1229
|
+
if (authentication)
|
|
1230
|
+
return asJsonValue({ authentication });
|
|
1231
|
+
if (!repository.projectId)
|
|
1232
|
+
throw refuse('Resolve the project before choosing its task branch.', 'project.resolve');
|
|
1233
|
+
if (!input.externalTaskId ||
|
|
1234
|
+
input.externalTaskId.length > 160 ||
|
|
1235
|
+
/[\r\n]/.test(input.externalTaskId))
|
|
1236
|
+
throw refuse('Pass the exact externalTaskId of the task whose branch question was answered.', 'task.branch');
|
|
1237
|
+
if (Boolean(input.name) === (input.keepCurrent === true) ||
|
|
1238
|
+
(input.worktreePath && !input.name))
|
|
1239
|
+
throw refuse('Choose exactly one branch name or keepCurrent. A separate worktree also needs a branch name.', 'task.branch');
|
|
1240
|
+
assertSafeToPersist(cleanJson({ externalTaskId: input.externalTaskId, name: input.name }));
|
|
1241
|
+
const git = this.dependencies.repositories.git;
|
|
1242
|
+
let target = repository;
|
|
1243
|
+
try {
|
|
1244
|
+
if (input.name)
|
|
1245
|
+
await git.validateBranch(repository.repoRoot, input.name);
|
|
1246
|
+
if (input.worktreePath && input.name) {
|
|
1247
|
+
const targetRoot = await git.createWorktree(repository.repoRoot, input.name, input.worktreePath);
|
|
1248
|
+
target = await this.dependencies.repositories.resolve(targetRoot);
|
|
1249
|
+
if (target.projectId && target.projectId !== repository.projectId)
|
|
1250
|
+
throw refuse('The target worktree belongs to a different Engineering Memory project. Choose another worktreePath.', 'task.branch');
|
|
1251
|
+
if (!target.markerPath) {
|
|
1252
|
+
await new RepositoryResolver(git, repository.schemaVersion ?? 2).writeMarker(targetRoot, repository.projectId);
|
|
1253
|
+
target = await this.dependencies.repositories.resolve(targetRoot);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
const branch = input.name ?? (await git.currentBranch(target.repoRoot));
|
|
1257
|
+
const store = git.branchStore(target.repoRoot);
|
|
1258
|
+
await store.reserve({
|
|
1259
|
+
projectId: repository.projectId,
|
|
1260
|
+
externalTaskId: input.externalTaskId,
|
|
1261
|
+
branch,
|
|
1262
|
+
});
|
|
1263
|
+
if (input.name && (await git.currentBranch(target.repoRoot)) !== input.name) {
|
|
1264
|
+
try {
|
|
1265
|
+
if (await git.branchExists(target.repoRoot, input.name))
|
|
1266
|
+
await git.switchBranch(target.repoRoot, input.name);
|
|
1267
|
+
else
|
|
1268
|
+
await git.createBranch(target.repoRoot, input.name);
|
|
1269
|
+
}
|
|
1270
|
+
catch (error) {
|
|
1271
|
+
if ((await git.currentBranch(target.repoRoot)) !== input.name) {
|
|
1272
|
+
await store.cancelUnopened(repository.projectId, input.externalTaskId);
|
|
1273
|
+
throw error;
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
return asJsonValue({
|
|
1278
|
+
branch,
|
|
1279
|
+
kept: input.keepCurrent === true,
|
|
1280
|
+
repoRoot: target.repoRoot,
|
|
1281
|
+
externalTaskId: input.externalTaskId,
|
|
1282
|
+
nextAction: 'Use this repoRoot for session.bootstrap and every subsequent command for this task. The decision survives retries and restarts.',
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
catch (error) {
|
|
1286
|
+
if (error instanceof BridgeRecoveryError)
|
|
1287
|
+
throw error;
|
|
1288
|
+
throw refuse(`${error instanceof Error ? error.message : String(error)} Retry task.branch for the selected task; use worktreePath to leave a busy or dirty checkout intact.`, 'task.branch');
|
|
1289
|
+
}
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
async requireBranchDecision(repository, externalTaskId) {
|
|
1293
|
+
const git = this.dependencies.repositories.git;
|
|
1294
|
+
const store = git.branchStore(repository.repoRoot);
|
|
1295
|
+
let record = await store.read();
|
|
1296
|
+
if (!record) {
|
|
1297
|
+
const pointer = await this.dependencies.activeContexts.loadForSlug(repository.repoFingerprint, externalTaskId);
|
|
1298
|
+
if (pointer?.worktreeId === sha256(repository.repoRoot) &&
|
|
1299
|
+
pointer.mode !== 'read_only' &&
|
|
1300
|
+
pointer.branch !== undefined) {
|
|
1301
|
+
await store.reserve({
|
|
1302
|
+
projectId: pointer.projectId,
|
|
1303
|
+
externalTaskId,
|
|
1304
|
+
branch: pointer.branch,
|
|
1305
|
+
taskId: pointer.taskId,
|
|
1306
|
+
});
|
|
1307
|
+
record = await store.read();
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
if (!record)
|
|
1311
|
+
throw refuse(`Ask the branch question through the native questionnaire for task ${externalTaskId}, then call task.branch with that externalTaskId and a name or keepCurrent. For parallel work choose a separate worktreePath.`, 'task.branch');
|
|
1312
|
+
if (record.decision.projectId !== repository.projectId ||
|
|
1313
|
+
record.decision.externalTaskId !== externalTaskId)
|
|
1314
|
+
throw refuse(`This worktree belongs to task ${record.decision.externalTaskId}. Use task.branch with worktreePath for ${externalTaskId}, then bootstrap in the returned directory.`, 'task.branch');
|
|
1315
|
+
const branch = await git.currentBranch(repository.repoRoot);
|
|
1316
|
+
if (branch !== record.decision.branch)
|
|
1317
|
+
throw refuse(`Task ${externalTaskId} selected ${record.decision.branch ?? 'detached HEAD'}, but this worktree is on ${branch ?? 'detached HEAD'}. Return to the selected branch with task.branch.`, 'task.branch');
|
|
1318
|
+
return record.decision;
|
|
1319
|
+
}
|
|
1320
|
+
async assertTaskWorktree(repository, task, pointer) {
|
|
1321
|
+
if (normalizeTaskMode(task.mode) === 'read_only')
|
|
1322
|
+
return;
|
|
1323
|
+
if (pointer?.worktreeId && pointer.worktreeId !== sha256(repository.repoRoot))
|
|
1324
|
+
throw refuse('This task belongs to a different worktree. Resume and run its commands in the worktree selected for this task.', 'session.resume');
|
|
1325
|
+
const recorded = typeof task.branch === 'string' ? task.branch : pointer?.branch;
|
|
1326
|
+
const current = await this.dependencies.repositories.git.currentBranch(repository.repoRoot);
|
|
1327
|
+
if (recorded && current !== recorded)
|
|
1328
|
+
throw refuse(`This task selected ${recorded}. Call task.branch with externalTaskId ${pointer?.taskSlug ?? String(task.externalTaskId)} and name ${recorded} before continuing.`, 'task.branch');
|
|
1329
|
+
}
|
|
1165
1330
|
async taskAbandon(input) {
|
|
1166
1331
|
return await this.execute(async () => {
|
|
1167
1332
|
return await this.taskExclusive(input.taskId, async () => {
|
|
@@ -1174,6 +1339,9 @@ export class BridgeService {
|
|
|
1174
1339
|
if (!abandoned || abandoned.abandoned !== true) {
|
|
1175
1340
|
throw refuse('The backend did not record the abandonment, so the task is still open.', 'session.resume');
|
|
1176
1341
|
}
|
|
1342
|
+
await this.dependencies.repositories.git
|
|
1343
|
+
.branchStore(repository.repoRoot)
|
|
1344
|
+
.release(input.taskId);
|
|
1177
1345
|
const forgotten = await this.dependencies.activeContexts.forget(repository.repoFingerprint, input.taskId);
|
|
1178
1346
|
await this.dependencies.gate.invalidateTask(input.taskId);
|
|
1179
1347
|
return asJsonValue({
|
|
@@ -2072,6 +2240,7 @@ export class BridgeService {
|
|
|
2072
2240
|
repoFingerprint: pointer.repoFingerprint,
|
|
2073
2241
|
repoRoot: repository.repoRoot,
|
|
2074
2242
|
diffHash: repository.git.diffHash,
|
|
2243
|
+
branch: typeof backendTask?.branch === 'string' ? backendTask.branch : (pointer.branch ?? null),
|
|
2075
2244
|
taskVersion,
|
|
2076
2245
|
taskChanges: intent.taskChanges,
|
|
2077
2246
|
});
|
|
@@ -2100,6 +2269,9 @@ export class BridgeService {
|
|
|
2100
2269
|
}
|
|
2101
2270
|
async deliverCloseIntent(repository, body, taskChanges, recoveredAfterResponseLoss) {
|
|
2102
2271
|
const touchedContract = taskChanges.some((change) => /\.controller\.[tj]s$/.test(change.path));
|
|
2272
|
+
const currentBranch = await this.dependencies.repositories.git.currentBranch(repository.repoRoot);
|
|
2273
|
+
if (typeof body.branch === 'string' && body.branch !== currentBranch)
|
|
2274
|
+
throw refuse(`Return to the branch ${body.branch} recorded in this close request before retrying.`, 'task.branch');
|
|
2103
2275
|
let response;
|
|
2104
2276
|
try {
|
|
2105
2277
|
response = await this.dependencies.client.request(endpoints.taskClose, {
|
|
@@ -2125,11 +2297,19 @@ export class BridgeService {
|
|
|
2125
2297
|
repoFingerprint: repository.repoFingerprint,
|
|
2126
2298
|
repoRoot: repository.repoRoot,
|
|
2127
2299
|
diffHash: String(body.diffHash),
|
|
2300
|
+
branch: typeof closedTask.branch === 'string'
|
|
2301
|
+
? closedTask.branch
|
|
2302
|
+
: typeof body.branch === 'string'
|
|
2303
|
+
? body.branch
|
|
2304
|
+
: null,
|
|
2128
2305
|
taskClosed: true,
|
|
2129
2306
|
taskVersion: closedTaskVersion,
|
|
2130
2307
|
taskChanges,
|
|
2131
2308
|
});
|
|
2132
2309
|
await this.dependencies.activeContexts.clearCloseIntent(repository.repoFingerprint, String(body.taskId));
|
|
2310
|
+
await this.dependencies.repositories.git
|
|
2311
|
+
.branchStore(repository.repoRoot)
|
|
2312
|
+
.release(String(body.taskId));
|
|
2133
2313
|
return asJsonValue({
|
|
2134
2314
|
...closedTask,
|
|
2135
2315
|
repository: publicRepository(repository),
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { NativeCommandRunner } from '../utilities/process.js';
|
|
2
|
+
import { assertSafeToPersist } from './offline-outbox.js';
|
|
3
|
+
import { BridgeRecoveryError } from './recovery-error.js';
|
|
4
|
+
const decisionRef = 'refs/worktree/engineering-memory-task';
|
|
5
|
+
export class TaskBranchStore {
|
|
6
|
+
repoRoot;
|
|
7
|
+
runner;
|
|
8
|
+
constructor(repoRoot, runner = new NativeCommandRunner()) {
|
|
9
|
+
this.repoRoot = repoRoot;
|
|
10
|
+
this.runner = runner;
|
|
11
|
+
}
|
|
12
|
+
async read() {
|
|
13
|
+
const reference = await this.runner.run('git', ['rev-parse', '--verify', '--quiet', decisionRef], { cwd: this.repoRoot });
|
|
14
|
+
if (reference.exitCode === 1)
|
|
15
|
+
return null;
|
|
16
|
+
if (reference.exitCode !== 0)
|
|
17
|
+
throw new BridgeRecoveryError('Git could not read the worktree task reservation. Retry the branch operation.', 'task.branch');
|
|
18
|
+
const oid = reference.stdout.trim();
|
|
19
|
+
const blob = await this.runner.run('git', ['cat-file', 'blob', oid], { cwd: this.repoRoot });
|
|
20
|
+
if (blob.exitCode !== 0)
|
|
21
|
+
throw new BridgeRecoveryError('Git could not read the recorded branch decision. Retry the branch operation.', 'task.branch');
|
|
22
|
+
let decision;
|
|
23
|
+
try {
|
|
24
|
+
decision = JSON.parse(blob.stdout);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
throw new BridgeRecoveryError('The worktree branch record is invalid. Use a separate worktree to continue this task.', 'task.branch');
|
|
28
|
+
}
|
|
29
|
+
if (!decision)
|
|
30
|
+
throw new BridgeRecoveryError('The worktree branch record is invalid. Use a separate worktree to continue this task.', 'task.branch');
|
|
31
|
+
if (typeof decision.projectId !== 'string' ||
|
|
32
|
+
typeof decision.externalTaskId !== 'string' ||
|
|
33
|
+
!(decision.branch === null || typeof decision.branch === 'string')) {
|
|
34
|
+
throw new BridgeRecoveryError('The worktree branch record is invalid. Use a separate worktree to continue this task.', 'task.branch');
|
|
35
|
+
}
|
|
36
|
+
return { oid, decision };
|
|
37
|
+
}
|
|
38
|
+
async reserve(decision) {
|
|
39
|
+
assertSafeToPersist({ ...decision });
|
|
40
|
+
const existing = await this.read();
|
|
41
|
+
if (existing) {
|
|
42
|
+
this.assertSame(existing.decision, decision);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const oid = await this.hash(decision);
|
|
46
|
+
const written = await this.runner.run('git', ['update-ref', '--no-deref', decisionRef, oid, '0'.repeat(oid.length)], { cwd: this.repoRoot });
|
|
47
|
+
if (written.exitCode !== 0) {
|
|
48
|
+
const winner = await this.read();
|
|
49
|
+
if (winner) {
|
|
50
|
+
this.assertSame(winner.decision, decision);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
throw new BridgeRecoveryError('Another branch operation is in progress. Retry the same task.branch operation.', 'task.branch');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async bind(projectId, externalTaskId, taskId) {
|
|
57
|
+
const current = await this.read();
|
|
58
|
+
if (!current ||
|
|
59
|
+
current.decision.projectId !== projectId ||
|
|
60
|
+
current.decision.externalTaskId !== externalTaskId)
|
|
61
|
+
throw new BridgeRecoveryError('The worktree reservation changed while opening the task. Resume it before editing.', 'session.resume');
|
|
62
|
+
if (current.decision.taskId === taskId)
|
|
63
|
+
return;
|
|
64
|
+
const oid = await this.hash({ ...current.decision, taskId });
|
|
65
|
+
const result = await this.runner.run('git', ['update-ref', '--no-deref', decisionRef, oid, current.oid], { cwd: this.repoRoot });
|
|
66
|
+
if (result.exitCode !== 0) {
|
|
67
|
+
const latest = await this.read();
|
|
68
|
+
if (latest?.decision.taskId !== taskId)
|
|
69
|
+
throw new BridgeRecoveryError('The worktree reservation changed while opening the task. Resume it before editing.', 'session.resume');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async cancelUnopened(projectId, externalTaskId) {
|
|
73
|
+
const current = await this.read();
|
|
74
|
+
if (!current ||
|
|
75
|
+
current.decision.taskId ||
|
|
76
|
+
current.decision.projectId !== projectId ||
|
|
77
|
+
current.decision.externalTaskId !== externalTaskId)
|
|
78
|
+
return;
|
|
79
|
+
const result = await this.runner.run('git', ['update-ref', '--no-deref', '-d', decisionRef, current.oid], { cwd: this.repoRoot });
|
|
80
|
+
if (result.exitCode !== 0 && (await this.read())?.oid === current.oid)
|
|
81
|
+
throw new BridgeRecoveryError('The failed branch operation could not release its reservation. Retry task.branch with the same task identifier.', 'task.branch');
|
|
82
|
+
}
|
|
83
|
+
async release(taskId) {
|
|
84
|
+
const current = await this.read();
|
|
85
|
+
if (!current || current.decision.taskId !== taskId)
|
|
86
|
+
return;
|
|
87
|
+
const result = await this.runner.run('git', ['update-ref', '--no-deref', '-d', decisionRef, current.oid], { cwd: this.repoRoot });
|
|
88
|
+
if (result.exitCode !== 0 && (await this.read())?.decision.taskId === taskId) {
|
|
89
|
+
throw new BridgeRecoveryError('The task has finished but its worktree reservation could not be released. Resume the task to retry cleanup.', 'session.resume');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
assertSame(current, requested) {
|
|
93
|
+
if (current.projectId !== requested.projectId ||
|
|
94
|
+
current.externalTaskId !== requested.externalTaskId) {
|
|
95
|
+
throw new BridgeRecoveryError(`This worktree belongs to task ${current.externalTaskId}. Resume that task, or call task.branch with worktreePath and a branch name for the new task.`, 'task.branch');
|
|
96
|
+
}
|
|
97
|
+
if (current.branch !== requested.branch) {
|
|
98
|
+
throw new BridgeRecoveryError(`This task selected ${current.branch ?? 'detached HEAD'}. Return to that branch before continuing.`, 'task.branch');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async hash(decision) {
|
|
102
|
+
const result = await this.runner.run('git', ['hash-object', '-w', '--stdin'], {
|
|
103
|
+
cwd: this.repoRoot,
|
|
104
|
+
input: JSON.stringify(decision),
|
|
105
|
+
});
|
|
106
|
+
if (result.exitCode !== 0)
|
|
107
|
+
throw new BridgeRecoveryError('Git could not save the branch decision. Retry the same branch operation.', 'task.branch');
|
|
108
|
+
return result.stdout.trim();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=task-branch-store.js.map
|
package/skill/SKILL.md
CHANGED
|
@@ -11,6 +11,8 @@ Read [lifecycle.md](references/lifecycle.md) before acting in a bound repository
|
|
|
11
11
|
|
|
12
12
|
Mandatory behavior:
|
|
13
13
|
|
|
14
|
+
Use the host's native questionnaire for every question to the user, including implementation choices, names, clarification, branch/worktree decisions and delivery. In Codex use request_user_input when available; in Claude use AskUserQuestion. 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. Existing answers remain valid through retries and handoffs.
|
|
15
|
+
|
|
14
16
|
1. Locate `.engineering-memory/project.json` from the working directory toward the repository root. When it exists, the project's knowledge is in the backend and not in the working tree, so answer nothing about the project before bootstrapping — a file search that finds no design link, no screen record and no rule is reporting what the repository lacks, not what the project knows.
|
|
15
17
|
2. Call `session.entry` before answering anything in a repository, and act on what it reports before the message itself: sign in when it says so, ask for organization and project when nothing has been decided, and stay completely silent about Engineering Memory in a repository where the user switched it off. Record every one of those answers with `session.set_decision`, and only ever from something the user actually said.
|
|
16
18
|
3. Before planning or editing, call `session.bootstrap`. After compaction, a new chat, interruption, or handoff, call `session.resume` first.
|
|
@@ -10,9 +10,11 @@ An archived project is recoverable state, not a missing or conflicting binding.
|
|
|
10
10
|
|
|
11
11
|
For a bound repository, call `session.bootstrap` before producing a plan or changing files. Supply the current repository root, project ID, task ID or stable local task slug, objective, task kind, task mode, and current Git diff hash. Use `read_only` for review, diagnosis, planning, or reporting without write authority; use `scaffold` when the task applies organization architecture templates to a new project; use `write` when the request authorizes repository changes. The bridge records the read-only Git diff hash as the immutable task baseline, including a pre-existing dirty worktree. Use the returned task ID, task version, context session ID, pinned revisions, project profile, engineering rules, prior task documents, quality gates, and current deviations.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Every new write or scaffold task settles its branch through the native questionnaire before it opens, including when HEAD is already on a feature branch. Offer a task branch or an explicit choice to stay on the current branch. Call `task.branch` with the exact `externalTaskId` and either `name` or `keepCurrent`, then call `session.bootstrap` in its returned `repoRoot`. An existing branch name checks it out. Creating a branch remains optional; recording the user's choice does not. A read-only task needs no branch decision, but `context.prepare_change` with `transitionToWrite` requires one before editing.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
The choice belongs to that task and worktree, survives retry and client restart, and cannot be consumed by a different task. Reuse an existing answer rather than asking again. A named branch stays fixed through prepare, verify, close and commit; closing never replaces it. Return with `task.branch` using the same task slug and recorded branch when a gate reports a mismatch. Legacy tasks without a recorded branch and tasks explicitly kept on detached HEAD remain compatible; they are not restricted to a named branch. Older clients remain supported during the minimum-version compatibility window.
|
|
16
|
+
|
|
17
|
+
Parallel development needs separate working directories. If another task owns the current worktree, offer a separate worktree through the questionnaire. Call `task.branch` with `externalTaskId`, `name`, and `worktreePath`; run bootstrap and all subsequent file, test and Git commands in its returned `repoRoot`. This creates or reuses an appropriate Git worktree and preserves the repository's project marker schema. Never switch another task's working directory. Reservations are shared by clients using the same worktree; close or abandon releases that task's reservation. An interrupted close can release it through resume.
|
|
16
18
|
|
|
17
19
|
A repository holds as many tasks as the people working in it. Never treat somebody else's unfinished task as a reason this one cannot proceed: no task waits on another task's review, reconciliation, verification or close, and nothing that is already verified or closed is undone by what happens elsewhere. When `session.resume` reports more than one live task for this repository, it lists them and the right move is to ask the user which one this is, never to guess and never to adopt the one that happens to be most recent.
|
|
18
20
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Native Questionnaires
|
|
2
2
|
|
|
3
|
-
Use Codex
|
|
3
|
+
Use the host's native questionnaire for every question to the user, including implementation choices, names, clarification, branch/worktree decisions and delivery. In Codex use request_user_input when available; in Claude use AskUserQuestion. 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. Existing answers remain valid through retries and handoffs. Web pages are limited to sign in, sign up, initial password change, and email verification.
|
|
4
4
|
|
|
5
5
|
## Authentication
|
|
6
6
|
|
|
@@ -62,7 +62,7 @@ Ask for the registered email and intended role. Show owner, maintainer, member,
|
|
|
62
62
|
|
|
63
63
|
## Branch
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
Ask once for every new write or scaffold task, on any branch, and before a read-only task transitions to writing. Offer a new or existing task branch, or explicitly continuing on the current branch; use the naming convention from the returned rules. When another task owns the worktree, offer a separate directory and branch for parallel work. Pass the exact `externalTaskId` and either `name` or `keepCurrent` to `task.branch`; for a separate worktree also supply `worktreePath`. Continue in the returned `repoRoot`. Retries and handoffs reuse the stored choice. Read-only analysis does not create a branch or consume another task's answer.
|
|
66
66
|
|
|
67
67
|
## Flow Entry and Exit
|
|
68
68
|
|
package/bin/entry-point.test.mjs
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { readFile } from 'node:fs/promises';
|
|
3
|
-
import test from 'node:test';
|
|
4
|
-
|
|
5
|
-
const entryPoint = new URL('./engineering-memory.mjs', import.meta.url);
|
|
6
|
-
const manifest = new URL('../package.json', import.meta.url);
|
|
7
|
-
|
|
8
|
-
test('the published entry point tells the installer which version it is', async () => {
|
|
9
|
-
const source = await readFile(entryPoint, 'utf8');
|
|
10
|
-
|
|
11
|
-
assert.match(
|
|
12
|
-
source,
|
|
13
|
-
/'--client-version',\s*await publishedVersion\(\)/,
|
|
14
|
-
'The installer is never told the version, so every install reports itself as unknown and no update is ever offered',
|
|
15
|
-
);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
test('the published entry point tells the installer which backend it was built for', async () => {
|
|
19
|
-
const source = await readFile(entryPoint, 'utf8');
|
|
20
|
-
|
|
21
|
-
assert.match(source, /'--api-url',\s*await publishedApiUrl\(\)/);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
test('the manifest carries a version the entry point can stamp', async () => {
|
|
25
|
-
const declared = JSON.parse(await readFile(manifest, 'utf8'));
|
|
26
|
-
|
|
27
|
-
assert.match(String(declared.version), /^\d+\.\d+\.\d+$/);
|
|
28
|
-
assert.equal(declared.name, 'engineering-memory');
|
|
29
|
-
});
|