engineering-memory 1.9.0 → 1.10.1
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/entry-point.test.mjs +29 -0
- package/dispatcher/sections.mjs +2 -2
- package/package.json +1 -1
- package/runtime/dist/src/config.js +6 -0
- package/runtime/dist/src/git/verification-gate.js +55 -14
- package/runtime/dist/src/index.js +1 -1
- package/runtime/dist/src/mcp/tool-definitions.js +154 -4
- package/runtime/dist/src/project/repository.js +69 -32
- package/runtime/dist/src/runtime/api-client.js +1 -0
- package/runtime/dist/src/runtime/bridge-service.js +115 -30
- package/runtime/dist/src/runtime/offline-outbox.js +6 -0
- package/skill/SKILL.md +3 -3
- package/skill/references/lifecycle.md +15 -16
- package/skill/references/questionnaires.md +4 -4
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
});
|
package/dispatcher/sections.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const shared = `This section is managed by the Engineering Memory installer.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Repository bindings live in the user-level Engineering Memory state directory, not in the working tree. Do not require or create a repository settings file. Always discover the binding with \`session.entry\`; the absence of \`.engineering-memory/project.json\` does not mean the repository is unbound.
|
|
4
4
|
|
|
5
5
|
Call \`session.entry\` before you answer anything in a repository, whatever the user asked for. It reports whether they are signed in, whether this repository is bound, what they decided about it last time, and the one thing to do now. Act on that before the message itself:
|
|
6
6
|
|
|
@@ -13,7 +13,7 @@ Call \`session.entry\` before you answer anything in a repository, whatever the
|
|
|
13
13
|
|
|
14
14
|
The user may say at any point that they want a different organization, a different project, Engineering Memory switched off, or switched back on. Recognise what they mean rather than matching words — the request arrives in whatever language and phrasing they happen to use — and act on it. Changing the organization always asks for the project again.
|
|
15
15
|
|
|
16
|
-
When
|
|
16
|
+
When session.entry reports a bound repository, load the personal \`engineering-memory\` skill and use the \`engineering-memory\` MCP server before you answer. Call \`session.bootstrap\` after the entry check — not only before planning or editing, but before telling the user anything about this project: its design source, its screens, its rules, its history, what it does or does not have.
|
|
17
17
|
|
|
18
18
|
Never answer a question about a bound repository from the working tree alone. What the repository holds is not what the project knows: the design file, the screen records, the engineering rules and the history of every earlier task live in Engineering Memory, and a file search will report them missing. If you are about to say that something is not recorded anywhere, that is the moment you have not asked yet.
|
|
19
19
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "engineering-memory",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.1",
|
|
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",
|
|
@@ -50,6 +50,7 @@ export const endpoints = {
|
|
|
50
50
|
taskScaffoldApplication: '/tasks/scaffold-application',
|
|
51
51
|
organizationList: '/organizations',
|
|
52
52
|
organizationCreate: '/organizations',
|
|
53
|
+
organizationUpdate: (organizationId) => `/organizations/${organizationId}`,
|
|
53
54
|
organizationMemberList: (organizationId) => `/organizations/${organizationId}/members`,
|
|
54
55
|
organizationMemberUpsert: (organizationId) => `/organizations/${organizationId}/members`,
|
|
55
56
|
taskVerify: '/runtime/task/verify',
|
|
@@ -60,6 +61,11 @@ export const endpoints = {
|
|
|
60
61
|
projectList: '/projects',
|
|
61
62
|
projectResolve: '/projects/resolve',
|
|
62
63
|
projectBind: (projectId) => `/projects/${projectId}/bind`,
|
|
64
|
+
projectUpdate: (projectId) => `/projects/${projectId}`,
|
|
65
|
+
workItemRuns: (projectId, workItemId) => `/projects/${projectId}/work-items/${workItemId}/runs`,
|
|
66
|
+
workItemRestore: (projectId, workItemId) => `/projects/${projectId}/work-items/${workItemId}/restore`,
|
|
67
|
+
workItemTests: (projectId, workItemId) => `/projects/${projectId}/work-items/${workItemId}/tests`,
|
|
68
|
+
workItemPlan: (projectId, workItemId) => `/projects/${projectId}/work-items/${workItemId}/plan`,
|
|
63
69
|
projectArchive: (projectId) => `/projects/${projectId}`,
|
|
64
70
|
projectRestore: (projectId) => `/projects/${projectId}/restore`,
|
|
65
71
|
projectMemberAdd: (projectId) => `/projects/${projectId}/members`,
|
|
@@ -1,16 +1,20 @@
|
|
|
1
|
+
import { sha256 } from '../utilities/hash.js';
|
|
1
2
|
import { readdir } from 'node:fs/promises';
|
|
2
3
|
import { join } from 'node:path';
|
|
4
|
+
import { RepositoryResolver } from '../project/repository.js';
|
|
3
5
|
import { ensureManagedDirectory, readJson, removeFile, safeSegment, writeJson, } from '../utilities/files.js';
|
|
4
6
|
export class VerificationGate {
|
|
5
7
|
git;
|
|
6
8
|
outbox;
|
|
7
9
|
commitGateAttestor;
|
|
8
10
|
root;
|
|
11
|
+
repositories;
|
|
9
12
|
constructor(stateRoot, git, outbox, commitGateAttestor) {
|
|
10
13
|
this.git = git;
|
|
11
14
|
this.outbox = outbox;
|
|
12
15
|
this.commitGateAttestor = commitGateAttestor;
|
|
13
16
|
this.root = join(stateRoot, 'verification-gates');
|
|
17
|
+
this.repositories = new RepositoryResolver(git, 2, stateRoot);
|
|
14
18
|
}
|
|
15
19
|
async record(input) {
|
|
16
20
|
if (input.taskClosed &&
|
|
@@ -19,8 +23,10 @@ export class VerificationGate {
|
|
|
19
23
|
}
|
|
20
24
|
const verifiedAt = new Date();
|
|
21
25
|
const taskChanges = await this.git.taskManifest(input.repoRoot, input.taskChanges);
|
|
22
|
-
await
|
|
23
|
-
|
|
26
|
+
const worktreeId = worktreeIdentity(await this.git.findRoot(input.repoRoot));
|
|
27
|
+
await writeJson(this.pathFor(input.repoFingerprint, worktreeId, input.taskId), {
|
|
28
|
+
schemaVersion: 5,
|
|
29
|
+
worktreeId,
|
|
24
30
|
taskId: input.taskId,
|
|
25
31
|
projectId: input.projectId,
|
|
26
32
|
repoFingerprint: input.repoFingerprint,
|
|
@@ -36,16 +42,12 @@ export class VerificationGate {
|
|
|
36
42
|
}, this.root);
|
|
37
43
|
}
|
|
38
44
|
async verify(repoRoot) {
|
|
39
|
-
const [
|
|
40
|
-
this.
|
|
41
|
-
this.git.manifest(repoRoot),
|
|
45
|
+
const [repository, stagedChanges, scaffolding] = await Promise.all([
|
|
46
|
+
this.repositories.resolve(repoRoot),
|
|
42
47
|
this.git.stagedManifest(repoRoot),
|
|
43
|
-
this.outbox.list(),
|
|
44
48
|
this.git.temporaryScaffolding(repoRoot),
|
|
45
49
|
]);
|
|
46
|
-
|
|
47
|
-
return { allowed: false, reason: 'offline_outbox_pending', diffHash: manifest.diffHash };
|
|
48
|
-
}
|
|
50
|
+
const { repoFingerprint: fingerprint, git: manifest } = repository;
|
|
49
51
|
const staged = scaffolding.filter((match) => match.staged);
|
|
50
52
|
if (staged.length > 0) {
|
|
51
53
|
return {
|
|
@@ -55,10 +57,29 @@ export class VerificationGate {
|
|
|
55
57
|
details: staged.map((match) => `${match.path}:${match.line}`),
|
|
56
58
|
};
|
|
57
59
|
}
|
|
58
|
-
const
|
|
60
|
+
const branch = await this.git.currentBranch(repository.repoRoot);
|
|
61
|
+
const candidates = await this.receiptsFor(fingerprint, worktreeIdentity(repository.repoRoot));
|
|
62
|
+
const relevant = candidates.filter((candidate) => !repository.projectId || candidate.projectId === repository.projectId);
|
|
63
|
+
relevant.sort((a, b) => b.verifiedAt.localeCompare(a.verifiedAt) || a.taskId.localeCompare(b.taskId));
|
|
64
|
+
const matchesBranch = (candidate) => !candidate.branch || candidate.branch === branch;
|
|
65
|
+
const receipt = relevant.find((candidate) => candidate.taskClosed &&
|
|
66
|
+
matchesBranch(candidate) &&
|
|
67
|
+
candidate.diffHash === manifest.diffHash &&
|
|
68
|
+
isTaskManifest(candidate.taskChanges) &&
|
|
69
|
+
stagedChanges.every((entry) => belongsToTask(entry, candidate.taskChanges))) ??
|
|
70
|
+
relevant.find(matchesBranch) ??
|
|
71
|
+
relevant[0];
|
|
59
72
|
if (!receipt) {
|
|
60
73
|
return { allowed: false, reason: 'task_verify_required', diffHash: manifest.diffHash };
|
|
61
74
|
}
|
|
75
|
+
if ((await this.outbox.listForTask(receipt.taskId)).length > 0) {
|
|
76
|
+
return {
|
|
77
|
+
allowed: false,
|
|
78
|
+
reason: 'offline_outbox_pending',
|
|
79
|
+
diffHash: manifest.diffHash,
|
|
80
|
+
taskId: receipt.taskId,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
62
83
|
if (!receipt.taskClosed) {
|
|
63
84
|
return {
|
|
64
85
|
allowed: false,
|
|
@@ -67,7 +88,7 @@ export class VerificationGate {
|
|
|
67
88
|
taskId: receipt.taskId,
|
|
68
89
|
};
|
|
69
90
|
}
|
|
70
|
-
if (receipt.schemaVersion
|
|
91
|
+
if (![4, 5].includes(receipt.schemaVersion) ||
|
|
71
92
|
!receipt.projectId ||
|
|
72
93
|
typeof receipt.taskVersion !== 'number' ||
|
|
73
94
|
!Number.isSafeInteger(receipt.taskVersion) ||
|
|
@@ -80,7 +101,6 @@ export class VerificationGate {
|
|
|
80
101
|
taskId: receipt.taskId,
|
|
81
102
|
};
|
|
82
103
|
}
|
|
83
|
-
const branch = await this.git.currentBranch(repoRoot);
|
|
84
104
|
if (receipt.branch && branch !== receipt.branch) {
|
|
85
105
|
return {
|
|
86
106
|
allowed: false,
|
|
@@ -188,10 +208,31 @@ export class VerificationGate {
|
|
|
188
208
|
await removeFile(join(this.root, entry.name), this.root);
|
|
189
209
|
}
|
|
190
210
|
}
|
|
191
|
-
|
|
192
|
-
|
|
211
|
+
async receiptsFor(repoFingerprint, worktreeId) {
|
|
212
|
+
await ensureManagedDirectory(this.root, this.root);
|
|
213
|
+
const prefix = safeSegment(repoFingerprint);
|
|
214
|
+
const entries = await readdir(this.root, { withFileTypes: true });
|
|
215
|
+
const receipts = [];
|
|
216
|
+
for (const entry of entries) {
|
|
217
|
+
if (entry.name !== `${prefix}.json` &&
|
|
218
|
+
!(entry.name.startsWith(`${prefix}-`) && entry.name.endsWith('.json')))
|
|
219
|
+
continue;
|
|
220
|
+
if (!entry.isFile() || entry.isSymbolicLink())
|
|
221
|
+
throw new Error(`Unsafe verification gate entry: ${entry.name}`);
|
|
222
|
+
const receipt = await readJson(join(this.root, entry.name), this.root);
|
|
223
|
+
if (receipt?.repoFingerprint === repoFingerprint &&
|
|
224
|
+
(entry.name === `${prefix}.json` || receipt.worktreeId === worktreeId))
|
|
225
|
+
receipts.push(receipt);
|
|
226
|
+
}
|
|
227
|
+
return receipts;
|
|
228
|
+
}
|
|
229
|
+
pathFor(repoFingerprint, worktreeId, taskId) {
|
|
230
|
+
return join(this.root, `${safeSegment(repoFingerprint)}-${worktreeId}-${safeSegment(taskId)}.json`);
|
|
193
231
|
}
|
|
194
232
|
}
|
|
233
|
+
function worktreeIdentity(repoRoot) {
|
|
234
|
+
return sha256(process.platform === 'win32' ? repoRoot.toLowerCase() : repoRoot);
|
|
235
|
+
}
|
|
195
236
|
function asObject(value) {
|
|
196
237
|
return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
197
238
|
}
|
|
@@ -51,7 +51,7 @@ export function createBridgeService() {
|
|
|
51
51
|
client,
|
|
52
52
|
credentials,
|
|
53
53
|
browserAuth: new BrowserAuthCoordinator(client, credentials),
|
|
54
|
-
repositories: new RepositoryResolver(git, config.markerSchemaVersion),
|
|
54
|
+
repositories: new RepositoryResolver(git, config.markerSchemaVersion, stateRoot),
|
|
55
55
|
journal: new JournalStore(stateRoot),
|
|
56
56
|
outbox,
|
|
57
57
|
gate,
|
|
@@ -72,6 +72,7 @@ export const engineeringMemoryToolNames = [
|
|
|
72
72
|
'architecture.record_application',
|
|
73
73
|
'organization.list',
|
|
74
74
|
'organization.create',
|
|
75
|
+
'organization.update',
|
|
75
76
|
'organization.member_list',
|
|
76
77
|
'organization.member_upsert',
|
|
77
78
|
'project.setup',
|
|
@@ -81,6 +82,16 @@ export const engineeringMemoryToolNames = [
|
|
|
81
82
|
'project.restore',
|
|
82
83
|
'project.member_add',
|
|
83
84
|
'project.member_list',
|
|
85
|
+
'work_item.create',
|
|
86
|
+
'work_item.update',
|
|
87
|
+
'work_item.archive',
|
|
88
|
+
'work_item.restore',
|
|
89
|
+
'work_item.runs',
|
|
90
|
+
'work_item.record_test',
|
|
91
|
+
'work_item.test_results',
|
|
92
|
+
'work_item.plan',
|
|
93
|
+
'work_item.confirm_plan',
|
|
94
|
+
'project.update',
|
|
84
95
|
'work_item.list',
|
|
85
96
|
'work_item.get',
|
|
86
97
|
'project.clone',
|
|
@@ -456,17 +467,27 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
456
467
|
organizationId: z.string().uuid(),
|
|
457
468
|
email: z.string().email(),
|
|
458
469
|
role: z.enum(['owner', 'member']),
|
|
459
|
-
discipline: z.enum([
|
|
470
|
+
discipline: z.enum([
|
|
471
|
+
'backend',
|
|
472
|
+
'frontend',
|
|
473
|
+
'fullstack',
|
|
474
|
+
'web',
|
|
475
|
+
'mobile',
|
|
476
|
+
'designer',
|
|
477
|
+
'product_manager',
|
|
478
|
+
'tester',
|
|
479
|
+
]),
|
|
460
480
|
}),
|
|
461
481
|
}, async (input) => toolResult(await service.organizationMemberUpsert(input)));
|
|
462
482
|
server.registerTool('project.setup', {
|
|
463
|
-
description: 'Create a policy-ready backend project from the native questionnaire and local repository audit, then
|
|
483
|
+
description: 'Create a policy-ready backend project from the native questionnaire and local repository audit, then store its binding in user-local state.',
|
|
464
484
|
inputSchema: z.object({
|
|
465
485
|
repoRoot: optionalRepoRoot,
|
|
466
486
|
organizationId: z.string().uuid(),
|
|
467
487
|
projectName: z.string().min(2),
|
|
468
488
|
projectSlug: z.string().min(2).optional(),
|
|
469
489
|
framework: z.string().optional(),
|
|
490
|
+
developmentArea: z.enum(['backend', 'web', 'mobile']).nullable().optional(),
|
|
470
491
|
figmaConfig: jsonObject,
|
|
471
492
|
initialProjectProfile: z.object({
|
|
472
493
|
title: z.string().min(2),
|
|
@@ -518,6 +539,14 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
518
539
|
.min(1),
|
|
519
540
|
}),
|
|
520
541
|
}, async (input) => toolResult(await service.architectureRecordApplication(input)));
|
|
542
|
+
server.registerTool('organization.update', {
|
|
543
|
+
description: 'Rename an organization as its administrator, using the last name read to avoid overwriting another rename. This does not change the organization identity or grant a global role.',
|
|
544
|
+
inputSchema: z.object({
|
|
545
|
+
organizationId: z.string().uuid(),
|
|
546
|
+
name: z.string().trim().min(2).max(120),
|
|
547
|
+
expectedName: z.string().min(2).max(120),
|
|
548
|
+
}),
|
|
549
|
+
}, async (input) => toolResult(await service.organizationUpdate(input)));
|
|
521
550
|
server.registerTool('organization.list', {
|
|
522
551
|
description: 'List the organizations the authenticated user belongs to so project setup can inherit an existing engineering core instead of silently creating an empty organization.',
|
|
523
552
|
inputSchema: z.object({}),
|
|
@@ -527,7 +556,7 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
527
556
|
inputSchema: z.object({ includeArchived: z.boolean().optional() }),
|
|
528
557
|
}, async (input) => toolResult(await service.projectList(input)));
|
|
529
558
|
server.registerTool('project.resolve', {
|
|
530
|
-
description: 'Resolve the current repository fingerprint or bind an explicitly selected project, then
|
|
559
|
+
description: 'Resolve the current repository fingerprint or bind an explicitly selected project, then store the user-local binding only after backend authorization succeeds.',
|
|
531
560
|
inputSchema: z.object({
|
|
532
561
|
repoRoot: optionalRepoRoot,
|
|
533
562
|
bind: z.boolean().optional(),
|
|
@@ -554,13 +583,134 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
554
583
|
projectId: z.string().min(1),
|
|
555
584
|
email: z.string().email(),
|
|
556
585
|
role: z.enum(['owner', 'maintainer', 'member', 'reader']),
|
|
557
|
-
discipline: z
|
|
586
|
+
discipline: z
|
|
587
|
+
.enum([
|
|
588
|
+
'backend',
|
|
589
|
+
'frontend',
|
|
590
|
+
'fullstack',
|
|
591
|
+
'web',
|
|
592
|
+
'mobile',
|
|
593
|
+
'designer',
|
|
594
|
+
'product_manager',
|
|
595
|
+
'tester',
|
|
596
|
+
])
|
|
597
|
+
.nullable()
|
|
598
|
+
.optional(),
|
|
558
599
|
}),
|
|
559
600
|
}, async (input) => toolResult(await service.projectMemberAdd(input)));
|
|
560
601
|
server.registerTool('project.member_list', {
|
|
561
602
|
description: 'List the members and roles of a project, including an archived project, so an action that requires an owner can be coordinated with the right person.',
|
|
562
603
|
inputSchema: z.object({ projectId: z.string().uuid() }),
|
|
563
604
|
}, async (input) => toolResult(await service.projectMemberList(input)));
|
|
605
|
+
const workItemLocator = { projectId: z.string().uuid(), workItemId: z.string().uuid() };
|
|
606
|
+
const workItemFields = {
|
|
607
|
+
title: z.string().trim().min(2).max(240).optional(),
|
|
608
|
+
description: z.string().max(20000).nullable().optional(),
|
|
609
|
+
priority: z.enum(['lowest', 'low', 'medium', 'high', 'highest']).optional(),
|
|
610
|
+
assigneeUserId: z.string().uuid().nullable().optional(),
|
|
611
|
+
acceptanceCriteria: z.array(z.string().trim().min(2).max(500)).max(50).optional(),
|
|
612
|
+
figmaUrl: z.string().url().nullable().optional(),
|
|
613
|
+
dueAt: z.string().datetime().nullable().optional(),
|
|
614
|
+
position: z.number().int().min(0).optional(),
|
|
615
|
+
dependencyIds: z.array(z.string().uuid()).max(50).optional(),
|
|
616
|
+
};
|
|
617
|
+
server.registerTool('work_item.create', {
|
|
618
|
+
description: 'Create a requirement in an authorized project. Organization administrators and assigned product managers manage work. Testers may report an unassigned bug with kind bug; no implementation permission is granted.',
|
|
619
|
+
inputSchema: z.object({
|
|
620
|
+
projectId: z.string().uuid(),
|
|
621
|
+
data: z.object({
|
|
622
|
+
...workItemFields,
|
|
623
|
+
title: z.string().trim().min(2).max(240),
|
|
624
|
+
kind: z.enum(['task', 'bug']).optional(),
|
|
625
|
+
}),
|
|
626
|
+
}),
|
|
627
|
+
}, async (input) => toolResult(await service.workItemCreate(input)));
|
|
628
|
+
server.registerTool('work_item.update', {
|
|
629
|
+
description: 'Edit or assign a work item at its current version. Assignees must already be active project members; this does not grant project access.',
|
|
630
|
+
inputSchema: z.object({
|
|
631
|
+
...workItemLocator,
|
|
632
|
+
data: z.object({
|
|
633
|
+
...workItemFields,
|
|
634
|
+
expectedVersion: z.number().int().min(1),
|
|
635
|
+
status: z
|
|
636
|
+
.enum(['backlog', 'ready', 'in_progress', 'in_review', 'done', 'cancelled'])
|
|
637
|
+
.optional(),
|
|
638
|
+
}),
|
|
639
|
+
}),
|
|
640
|
+
}, async (input) => toolResult(await service.workItemUpdate(input)));
|
|
641
|
+
server.registerTool('work_item.archive', {
|
|
642
|
+
description: 'Reversibly remove a work item from the active backlog at its current version, preserving history.',
|
|
643
|
+
inputSchema: z.object({ ...workItemLocator, expectedVersion: z.number().int().min(1) }),
|
|
644
|
+
}, async (input) => toolResult(await service.workItemArchive(input)));
|
|
645
|
+
server.registerTool('work_item.restore', {
|
|
646
|
+
description: 'Restore an archived work item at its current version.',
|
|
647
|
+
inputSchema: z.object({ ...workItemLocator, expectedVersion: z.number().int().min(1) }),
|
|
648
|
+
}, async (input) => toolResult(await service.workItemRestore(input)));
|
|
649
|
+
server.registerTool('work_item.runs', {
|
|
650
|
+
description: 'List execution runs for this requirement in the current and authorized directly linked projects. A closed run in one repository does not prove another repository finished.',
|
|
651
|
+
inputSchema: z.object({
|
|
652
|
+
...workItemLocator,
|
|
653
|
+
offset: z.number().int().min(0).optional(),
|
|
654
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
655
|
+
}),
|
|
656
|
+
}, async (input) => toolResult(await service.workItemRuns(input)));
|
|
657
|
+
server.registerTool('work_item.record_test', {
|
|
658
|
+
description: 'Record immutable, privacy-safe QA evidence for the exact work-item version. Passed does not auto-complete the work item. Reuse the same idempotency key only for an identical retry.',
|
|
659
|
+
inputSchema: z.object({
|
|
660
|
+
...workItemLocator,
|
|
661
|
+
data: z.object({
|
|
662
|
+
expectedVersion: z.number().int().min(1),
|
|
663
|
+
idempotencyKey: z.string().uuid(),
|
|
664
|
+
outcome: z.enum(['passed', 'failed', 'blocked']),
|
|
665
|
+
summary: z.string().trim().min(2).max(4000),
|
|
666
|
+
testedRevision: z.string().min(1).max(160).optional(),
|
|
667
|
+
}),
|
|
668
|
+
}),
|
|
669
|
+
}, async (input) => toolResult(await service.workItemRecordTest(input)));
|
|
670
|
+
server.registerTool('work_item.test_results', {
|
|
671
|
+
description: 'Read the versioned QA evidence for an authorized work item. Older evidence remains attached to the requirement version it tested.',
|
|
672
|
+
inputSchema: z.object({
|
|
673
|
+
...workItemLocator,
|
|
674
|
+
offset: z.number().int().min(0).optional(),
|
|
675
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
676
|
+
}),
|
|
677
|
+
}, async (input) => toolResult(await service.workItemTestResults(input)));
|
|
678
|
+
server.registerTool('work_item.plan', {
|
|
679
|
+
description: 'Inspect authorized current and linked project targets, repository URLs, implementation capabilities and the persisted shared branch before opening work. Check contracts to identify backend/web/mobile impact; capability is not user consent.',
|
|
680
|
+
inputSchema: z.object(workItemLocator),
|
|
681
|
+
}, async (input) => toolResult(await service.workItemPlan(input)));
|
|
682
|
+
server.registerTool('work_item.confirm_plan', {
|
|
683
|
+
description: 'Persist only the project scope and shared branch the user approved, through the native questionnaire or explicit existing instruction. Explain additional backend/frontend work and ask before expanding scope. Existing answers survive retries and handoffs; never fabricate confirmation.',
|
|
684
|
+
inputSchema: z.object({
|
|
685
|
+
...workItemLocator,
|
|
686
|
+
data: z.object({
|
|
687
|
+
expectedVersion: z.number().int().min(1),
|
|
688
|
+
projectIds: z.array(z.string().uuid()).min(1).max(12),
|
|
689
|
+
branch: z
|
|
690
|
+
.string()
|
|
691
|
+
.regex(/^(?=.{1,255}$)[a-zA-Z0-9][a-zA-Z0-9_-]*(?:\/[a-zA-Z0-9][a-zA-Z0-9_-]*)*$/),
|
|
692
|
+
reason: z.string().trim().min(2).max(2000),
|
|
693
|
+
userConfirmed: z.literal(true),
|
|
694
|
+
}),
|
|
695
|
+
}),
|
|
696
|
+
}, async (input) => toolResult(await service.workItemConfirmPlan(input)));
|
|
697
|
+
server.registerTool('project.update', {
|
|
698
|
+
description: 'Update project details or its GitHub/GitLab HTTPS repository URL at the current version. Organization administrators can manage their organization projects without an implementation assignment.',
|
|
699
|
+
inputSchema: z.object({
|
|
700
|
+
projectId: z.string().uuid(),
|
|
701
|
+
data: z.object({
|
|
702
|
+
expectedVersion: z.number().int().min(1),
|
|
703
|
+
name: z.string().min(2).max(160).optional(),
|
|
704
|
+
framework: z.string().min(2).max(80).nullable().optional(),
|
|
705
|
+
developmentArea: z.enum(['backend', 'web', 'mobile']).nullable().optional(),
|
|
706
|
+
repositoryUrl: z.string().url().nullable().optional(),
|
|
707
|
+
workItemPrefix: z
|
|
708
|
+
.string()
|
|
709
|
+
.regex(/^[A-Z][A-Z0-9]{1,11}$/)
|
|
710
|
+
.optional(),
|
|
711
|
+
}),
|
|
712
|
+
}),
|
|
713
|
+
}, async (input) => toolResult(await service.projectUpdate(input)));
|
|
564
714
|
server.registerTool('work_item.list', {
|
|
565
715
|
description: 'List selectable work items for a project before opening an engineering run in this chat.',
|
|
566
716
|
inputSchema: z.object({
|
|
@@ -1,74 +1,111 @@
|
|
|
1
|
-
import { dirname, join, parse
|
|
1
|
+
import { dirname, join, parse } from 'node:path';
|
|
2
2
|
import { canonicalPath, pathExists, readJson, writeJson } from '../utilities/files.js';
|
|
3
|
+
import { sha256 } from '../utilities/hash.js';
|
|
3
4
|
export class RepositoryResolver {
|
|
4
5
|
git;
|
|
5
6
|
markerSchemaVersion;
|
|
6
|
-
|
|
7
|
+
stateRoot;
|
|
8
|
+
constructor(git, markerSchemaVersion, stateRoot) {
|
|
7
9
|
this.git = git;
|
|
8
10
|
this.markerSchemaVersion = markerSchemaVersion;
|
|
11
|
+
this.stateRoot = stateRoot;
|
|
9
12
|
}
|
|
10
13
|
async resolve(startPath, explicitProjectId) {
|
|
11
14
|
const repoRoot = await this.git.findRoot(startPath);
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
const repositoryKey = await this.keyFor(repoRoot);
|
|
16
|
+
const bindingPath = this.pathFor(repositoryKey);
|
|
17
|
+
let binding = await readJson(bindingPath, this.stateRoot);
|
|
18
|
+
let markerPath = null;
|
|
19
|
+
if (binding) {
|
|
20
|
+
this.assertMarker(binding);
|
|
21
|
+
if (binding.repositoryKey !== repositoryKey ||
|
|
22
|
+
!/^[0-9a-f]{64}$/.test(binding.repoFingerprint)) {
|
|
23
|
+
throw new Error('Engineering Memory local project binding is invalid');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
markerPath = await this.findMarker(startPath, repoRoot);
|
|
28
|
+
if (!markerPath) {
|
|
29
|
+
const mainRoot = await this.git.mainWorktree(repoRoot);
|
|
30
|
+
if (mainRoot !== repoRoot)
|
|
31
|
+
markerPath = await this.findMarker(mainRoot, mainRoot);
|
|
32
|
+
}
|
|
33
|
+
if (markerPath) {
|
|
34
|
+
const markerRoot = await this.git.findRoot(dirname(dirname(markerPath)));
|
|
35
|
+
const marker = await readJson(markerPath, markerRoot);
|
|
36
|
+
this.assertMarker(marker);
|
|
37
|
+
binding = {
|
|
38
|
+
projectId: marker.projectId,
|
|
39
|
+
schemaVersion: marker.schemaVersion,
|
|
40
|
+
repositoryKey,
|
|
41
|
+
repoFingerprint: await this.git.fingerprint(repoRoot, marker.schemaVersion),
|
|
42
|
+
};
|
|
43
|
+
await writeJson(bindingPath, binding, this.stateRoot);
|
|
44
|
+
}
|
|
16
45
|
}
|
|
17
|
-
if (explicitProjectId
|
|
18
|
-
|
|
46
|
+
if (explicitProjectId)
|
|
47
|
+
assertProjectId(explicitProjectId);
|
|
48
|
+
if (explicitProjectId && binding && explicitProjectId !== binding.projectId) {
|
|
49
|
+
throw new Error('Explicit project does not match the local repository binding');
|
|
19
50
|
}
|
|
20
51
|
const [repoFingerprint, git] = await Promise.all([
|
|
21
|
-
this.git.fingerprint(repoRoot,
|
|
52
|
+
binding?.repoFingerprint ?? this.git.fingerprint(repoRoot, this.markerSchemaVersion),
|
|
22
53
|
this.git.manifest(repoRoot),
|
|
23
54
|
]);
|
|
24
55
|
return {
|
|
25
56
|
repoRoot,
|
|
26
57
|
markerPath,
|
|
27
|
-
|
|
28
|
-
|
|
58
|
+
bindingPath: binding ? bindingPath : null,
|
|
59
|
+
projectId: explicitProjectId ?? binding?.projectId ?? null,
|
|
60
|
+
schemaVersion: binding?.schemaVersion ?? null,
|
|
29
61
|
repoFingerprint,
|
|
30
62
|
git,
|
|
31
63
|
};
|
|
32
64
|
}
|
|
33
|
-
async
|
|
65
|
+
async writeBinding(repoRoot, projectId) {
|
|
34
66
|
assertProjectId(projectId);
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
|
|
67
|
+
const repository = await this.resolve(repoRoot, projectId);
|
|
68
|
+
const repositoryKey = await this.keyFor(repository.repoRoot);
|
|
69
|
+
const bindingPath = this.pathFor(repositoryKey);
|
|
70
|
+
await writeJson(bindingPath, {
|
|
38
71
|
projectId,
|
|
39
|
-
schemaVersion: this.markerSchemaVersion,
|
|
40
|
-
|
|
41
|
-
|
|
72
|
+
schemaVersion: repository.schemaVersion ?? this.markerSchemaVersion,
|
|
73
|
+
repositoryKey,
|
|
74
|
+
repoFingerprint: repository.repoFingerprint,
|
|
75
|
+
}, this.stateRoot);
|
|
76
|
+
return bindingPath;
|
|
77
|
+
}
|
|
78
|
+
async keyFor(repoRoot) {
|
|
79
|
+
const mainRoot = await this.git.mainWorktree(repoRoot);
|
|
80
|
+
const path = process.platform === 'win32' ? mainRoot.toLowerCase() : mainRoot;
|
|
81
|
+
return sha256(`${path}\n${await this.git.fingerprint(repoRoot, 2)}`);
|
|
82
|
+
}
|
|
83
|
+
pathFor(repositoryKey) {
|
|
84
|
+
return join(this.stateRoot, 'project-bindings', `${repositoryKey}.json`);
|
|
42
85
|
}
|
|
43
86
|
async findMarker(startPath, repoRoot) {
|
|
44
87
|
let current = await canonicalPath(startPath);
|
|
45
88
|
const root = await canonicalPath(repoRoot);
|
|
46
89
|
while (true) {
|
|
47
90
|
const candidate = join(current, '.engineering-memory', 'project.json');
|
|
48
|
-
if (await pathExists(candidate, root))
|
|
91
|
+
if (await pathExists(candidate, root))
|
|
49
92
|
return candidate;
|
|
50
|
-
|
|
51
|
-
if (current === root || current === parse(current).root) {
|
|
93
|
+
if (current === root || current === parse(current).root)
|
|
52
94
|
return null;
|
|
53
|
-
}
|
|
54
95
|
current = dirname(current);
|
|
55
96
|
}
|
|
56
97
|
}
|
|
57
|
-
|
|
58
|
-
const marker = await readJson(path, repoRoot);
|
|
59
|
-
const schemaVersion = marker?.schemaVersion;
|
|
98
|
+
assertMarker(marker) {
|
|
60
99
|
if (!marker ||
|
|
61
100
|
typeof marker.projectId !== 'string' ||
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
throw new Error(`Invalid Engineering Memory project marker: ${resolve(path)}`);
|
|
101
|
+
!Number.isInteger(marker.schemaVersion) ||
|
|
102
|
+
marker.schemaVersion < 1) {
|
|
103
|
+
throw new Error('Invalid Engineering Memory project binding');
|
|
66
104
|
}
|
|
67
|
-
if (schemaVersion > this.markerSchemaVersion) {
|
|
68
|
-
throw new Error(`Unsupported project
|
|
105
|
+
if (marker.schemaVersion > this.markerSchemaVersion) {
|
|
106
|
+
throw new Error(`Unsupported project binding schema: ${marker.schemaVersion}`);
|
|
69
107
|
}
|
|
70
108
|
assertProjectId(marker.projectId);
|
|
71
|
-
return marker;
|
|
72
109
|
}
|
|
73
110
|
}
|
|
74
111
|
function assertProjectId(value) {
|
|
@@ -3,7 +3,6 @@ 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';
|
|
7
6
|
import { sha256, stableStringify } from '../utilities/hash.js';
|
|
8
7
|
import { ApiResponseError, BackendUnavailableError, } from './api-client.js';
|
|
9
8
|
import { assertSafeToPersist, normalizeRepositoryPaths } from './offline-outbox.js';
|
|
@@ -68,7 +67,7 @@ export class BridgeService {
|
|
|
68
67
|
const recoveredDeliveries = await this.recoverJournalOutbox();
|
|
69
68
|
const outbox = await this.flushOutbox();
|
|
70
69
|
let projectId = repository.projectId;
|
|
71
|
-
if (!repository.
|
|
70
|
+
if (!repository.bindingPath) {
|
|
72
71
|
const resolved = await this.dependencies.client.request(endpoints.projectResolve, {
|
|
73
72
|
method: 'POST',
|
|
74
73
|
body: { repoFingerprint: repository.repoFingerprint },
|
|
@@ -89,7 +88,7 @@ export class BridgeService {
|
|
|
89
88
|
});
|
|
90
89
|
}
|
|
91
90
|
if (!projectId) {
|
|
92
|
-
throw refuse('This repository has
|
|
91
|
+
throw refuse('This repository has no selected project. Select its project before bootstrapping.', 'project.resolve');
|
|
93
92
|
}
|
|
94
93
|
const bootstrapMode = persistedBootstrap.mode ?? 'write';
|
|
95
94
|
const decision = bootstrapMode === 'read_only'
|
|
@@ -249,7 +248,7 @@ export class BridgeService {
|
|
|
249
248
|
});
|
|
250
249
|
}
|
|
251
250
|
const recoveredDeliveries = await this.recoverJournalOutbox(projectId, taskSlug);
|
|
252
|
-
const outbox = await this.flushOutbox();
|
|
251
|
+
const outbox = await this.flushOutbox(pointer?.taskId);
|
|
253
252
|
const afterSequence = input.afterSequence ?? 0;
|
|
254
253
|
const response = await this.dependencies.client.request(endpoints.sessionResume, {
|
|
255
254
|
method: 'POST',
|
|
@@ -279,7 +278,7 @@ export class BridgeService {
|
|
|
279
278
|
.release(backendTask.id);
|
|
280
279
|
}
|
|
281
280
|
let localJournal = await this.dependencies.journal.load(projectId, taskSlug);
|
|
282
|
-
let pendingOutbox = await this.dependencies.outbox.
|
|
281
|
+
let pendingOutbox = await this.dependencies.outbox.listForTask(backendTask.id);
|
|
283
282
|
const responseSource = this.dependencies.client.getResponseSource(response);
|
|
284
283
|
const recoveredVerification = responseSource !== 'stale_cache' && pointer
|
|
285
284
|
? await this.retryVerificationRecovery(repository, pointer, backend)
|
|
@@ -307,7 +306,7 @@ export class BridgeService {
|
|
|
307
306
|
documents: backendDocuments,
|
|
308
307
|
});
|
|
309
308
|
blockedJournalEventIds = hydration.blockedEventIds;
|
|
310
|
-
const entries = await this.dependencies.outbox.
|
|
309
|
+
const entries = await this.dependencies.outbox.listForTask(backendTask.id);
|
|
311
310
|
for (const eventId of hydration.synchronizedEventIds) {
|
|
312
311
|
const entry = entries.find((value) => value.journalRef?.eventId === eventId);
|
|
313
312
|
if (entry) {
|
|
@@ -338,7 +337,7 @@ export class BridgeService {
|
|
|
338
337
|
}
|
|
339
338
|
}
|
|
340
339
|
localJournal = await this.dependencies.journal.load(projectId, taskSlug);
|
|
341
|
-
pendingOutbox = await this.dependencies.outbox.
|
|
340
|
+
pendingOutbox = await this.dependencies.outbox.listForTask(backendTask.id);
|
|
342
341
|
hydratedFromBackend = !hadLocalProjection;
|
|
343
342
|
mergedFromBackend = hadLocalProjection;
|
|
344
343
|
}
|
|
@@ -438,10 +437,12 @@ export class BridgeService {
|
|
|
438
437
|
async contextPrepareChange(input) {
|
|
439
438
|
return await this.execute(async () => {
|
|
440
439
|
const repository = await this.dependencies.repositories.resolve(input.repoRoot ?? process.cwd());
|
|
441
|
-
await this.recoverJournalOutbox();
|
|
442
|
-
const outbox = await this.flushOutbox();
|
|
443
|
-
const pendingOutbox = await this.dependencies.outbox.list();
|
|
444
440
|
const pointer = await this.dependencies.activeContexts.loadForSession(repository.repoFingerprint, input.sessionId);
|
|
441
|
+
if (!pointer)
|
|
442
|
+
throw refuse('Resume this task before requesting a change lease.', 'session.resume');
|
|
443
|
+
await this.recoverJournalOutbox(pointer.projectId, pointer.taskSlug);
|
|
444
|
+
const outbox = await this.flushOutbox(pointer.taskId);
|
|
445
|
+
const pendingOutbox = await this.dependencies.outbox.listForTask(pointer.taskId);
|
|
445
446
|
if (pointer?.verificationIntent || pointer?.closeIntent) {
|
|
446
447
|
throw refuse('A verification or close intent from an earlier session is still unsettled, so a new lease would be taken against the wrong state.', 'session.resume');
|
|
447
448
|
}
|
|
@@ -993,8 +994,9 @@ export class BridgeService {
|
|
|
993
994
|
return await this.execute(async () => {
|
|
994
995
|
return await this.taskExclusive(input.taskId, async () => {
|
|
995
996
|
await this.recoverJournalOutbox();
|
|
996
|
-
const outbox = await this.flushOutbox();
|
|
997
|
-
const pendingOutboxCount = (await this.dependencies.outbox.
|
|
997
|
+
const outbox = await this.flushOutbox(input.taskId);
|
|
998
|
+
const pendingOutboxCount = (await this.dependencies.outbox.listForTask(input.taskId))
|
|
999
|
+
.length;
|
|
998
1000
|
const repository = await this.dependencies.repositories.resolve(input.repoRoot ?? process.cwd());
|
|
999
1001
|
if (pendingOutboxCount > 0) {
|
|
1000
1002
|
return asJsonValue({
|
|
@@ -1248,10 +1250,8 @@ export class BridgeService {
|
|
|
1248
1250
|
target = await this.dependencies.repositories.resolve(targetRoot);
|
|
1249
1251
|
if (target.projectId && target.projectId !== repository.projectId)
|
|
1250
1252
|
throw refuse('The target worktree belongs to a different Engineering Memory project. Choose another worktreePath.', 'task.branch');
|
|
1251
|
-
if (!target.
|
|
1252
|
-
|
|
1253
|
-
target = await this.dependencies.repositories.resolve(targetRoot);
|
|
1254
|
-
}
|
|
1253
|
+
if (!target.bindingPath || target.repoFingerprint !== repository.repoFingerprint)
|
|
1254
|
+
throw refuse('The worktree did not inherit the selected local project binding.', 'project.resolve');
|
|
1255
1255
|
}
|
|
1256
1256
|
const branch = input.name ?? (await git.currentBranch(target.repoRoot));
|
|
1257
1257
|
const store = git.branchStore(target.repoRoot);
|
|
@@ -1362,6 +1362,7 @@ export class BridgeService {
|
|
|
1362
1362
|
projectSlug: input.projectSlug ?? slugify(input.projectName),
|
|
1363
1363
|
repoFingerprint: repository.repoFingerprint,
|
|
1364
1364
|
framework: input.framework,
|
|
1365
|
+
developmentArea: input.developmentArea,
|
|
1365
1366
|
figmaConfig: input.figmaConfig,
|
|
1366
1367
|
initialProjectProfile: input.initialProjectProfile,
|
|
1367
1368
|
});
|
|
@@ -1391,8 +1392,8 @@ export class BridgeService {
|
|
|
1391
1392
|
readDiscoveryUnits(policy).length === 0) {
|
|
1392
1393
|
throw refuse('Project setup response is not policy-ready', 'project.setup');
|
|
1393
1394
|
}
|
|
1394
|
-
const
|
|
1395
|
-
return asJsonValue({ ...data, markerPath });
|
|
1395
|
+
const bindingPath = await this.dependencies.repositories.writeBinding(repository.repoRoot, project.id);
|
|
1396
|
+
return asJsonValue({ ...data, marker: null, markerPath: null, bindingPath });
|
|
1396
1397
|
});
|
|
1397
1398
|
}
|
|
1398
1399
|
async projectMemberAdd(input) {
|
|
@@ -1493,6 +1494,77 @@ export class BridgeService {
|
|
|
1493
1494
|
return asJsonValue({ workItem: response.data });
|
|
1494
1495
|
});
|
|
1495
1496
|
}
|
|
1497
|
+
async workItemCreate(input) {
|
|
1498
|
+
return this.execute(async () => {
|
|
1499
|
+
const response = await this.dependencies.client.request(endpoints.workItemList(input.projectId), { method: 'POST', body: cleanJson(input.data) });
|
|
1500
|
+
return asJsonValue(response.data);
|
|
1501
|
+
});
|
|
1502
|
+
}
|
|
1503
|
+
async workItemUpdate(input) {
|
|
1504
|
+
return this.execute(async () => {
|
|
1505
|
+
const response = await this.dependencies.client.request(endpoints.workItemGet(input.projectId, input.workItemId), { method: 'PATCH', body: cleanJson(input.data) });
|
|
1506
|
+
return asJsonValue(response.data);
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1509
|
+
async workItemArchive(input) {
|
|
1510
|
+
return this.execute(async () => {
|
|
1511
|
+
const response = await this.dependencies.client.request(endpoints.workItemGet(input.projectId, input.workItemId), { method: 'DELETE', body: cleanJson({ expectedVersion: input.expectedVersion }) });
|
|
1512
|
+
return asJsonValue(response.data);
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1515
|
+
async workItemRestore(input) {
|
|
1516
|
+
return this.execute(async () => {
|
|
1517
|
+
const response = await this.dependencies.client.request(endpoints.workItemRestore(input.projectId, input.workItemId), { method: 'POST', body: cleanJson({ expectedVersion: input.expectedVersion }) });
|
|
1518
|
+
return asJsonValue(response.data);
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1521
|
+
async workItemRecordTest(input) {
|
|
1522
|
+
return this.execute(async () => {
|
|
1523
|
+
const response = await this.dependencies.client.request(endpoints.workItemTests(input.projectId, input.workItemId), { method: 'POST', body: cleanJson(input.data) });
|
|
1524
|
+
return asJsonValue(response.data);
|
|
1525
|
+
});
|
|
1526
|
+
}
|
|
1527
|
+
async workItemConfirmPlan(input) {
|
|
1528
|
+
return this.execute(async () => {
|
|
1529
|
+
const response = await this.dependencies.client.request(endpoints.workItemPlan(input.projectId, input.workItemId), { method: 'PUT', body: cleanJson(input.data) });
|
|
1530
|
+
return asJsonValue(response.data);
|
|
1531
|
+
});
|
|
1532
|
+
}
|
|
1533
|
+
async projectUpdate(input) {
|
|
1534
|
+
return this.execute(async () => {
|
|
1535
|
+
const response = await this.dependencies.client.request(endpoints.projectUpdate(input.projectId), { method: 'PATCH', body: cleanJson(input.data) });
|
|
1536
|
+
return asJsonValue(response.data);
|
|
1537
|
+
});
|
|
1538
|
+
}
|
|
1539
|
+
async workItemRuns(input) {
|
|
1540
|
+
return this.execute(async () => {
|
|
1541
|
+
const query = new URLSearchParams({
|
|
1542
|
+
offset: String(input.offset ?? 0),
|
|
1543
|
+
limit: String(input.limit ?? 50),
|
|
1544
|
+
});
|
|
1545
|
+
const response = await this.dependencies.client.request(`${endpoints.workItemRuns(input.projectId, input.workItemId)}?${query}`);
|
|
1546
|
+
return asJsonValue(response.data);
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1549
|
+
async workItemTestResults(input) {
|
|
1550
|
+
return this.execute(async () => {
|
|
1551
|
+
const query = new URLSearchParams({
|
|
1552
|
+
offset: String(input.offset ?? 0),
|
|
1553
|
+
limit: String(input.limit ?? 50),
|
|
1554
|
+
});
|
|
1555
|
+
const response = await this.dependencies.client.request(`${endpoints.workItemTests(input.projectId, input.workItemId)}?${query}`);
|
|
1556
|
+
return asJsonValue(response.data);
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
async workItemPlan(input) {
|
|
1560
|
+
return this.execute(async () => {
|
|
1561
|
+
const response = await this.dependencies.client.request(endpoints.workItemPlan(input.projectId, input.workItemId));
|
|
1562
|
+
return asJsonValue({
|
|
1563
|
+
...objectOrEmpty(response.data),
|
|
1564
|
+
nextAction: 'Read the current and authorized linked project contracts. Decide whether web, mobile, backend or multiple projects need changes. If additional scope is needed, explain why and obtain the native questionnaire answer before work_item.confirm_plan. Reuse an already confirmed scope and shared branch; each repository has its own worktree and task lifecycle.',
|
|
1565
|
+
});
|
|
1566
|
+
});
|
|
1567
|
+
}
|
|
1496
1568
|
async architecturePlan(input) {
|
|
1497
1569
|
return await this.execute(async () => {
|
|
1498
1570
|
const body = cleanJson(input);
|
|
@@ -1541,6 +1613,15 @@ export class BridgeService {
|
|
|
1541
1613
|
return asJsonValue({ application: response.data });
|
|
1542
1614
|
});
|
|
1543
1615
|
}
|
|
1616
|
+
async organizationUpdate(input) {
|
|
1617
|
+
return this.execute(async () => {
|
|
1618
|
+
const response = await this.dependencies.client.request(endpoints.organizationUpdate(input.organizationId), {
|
|
1619
|
+
method: 'PATCH',
|
|
1620
|
+
body: cleanJson({ name: input.name, expectedName: input.expectedName }),
|
|
1621
|
+
});
|
|
1622
|
+
return asJsonValue(response.data);
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
1544
1625
|
async organizationCreate(input) {
|
|
1545
1626
|
return await this.execute(async () => {
|
|
1546
1627
|
const response = await this.dependencies.client.request(endpoints.organizationCreate, { method: 'POST', body: cleanJson(input) });
|
|
@@ -1614,17 +1695,14 @@ export class BridgeService {
|
|
|
1614
1695
|
if (!project || project.id !== input.projectId) {
|
|
1615
1696
|
throw refuse('Project bind response does not match the selected project', 'project.resolve');
|
|
1616
1697
|
}
|
|
1617
|
-
const
|
|
1698
|
+
const bindingPath = await this.dependencies.repositories.writeBinding(repository.repoRoot, input.projectId);
|
|
1618
1699
|
return asJsonValue({
|
|
1619
1700
|
resolved: true,
|
|
1620
1701
|
bound: true,
|
|
1621
1702
|
project,
|
|
1622
|
-
markerPath,
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
markerPath,
|
|
1626
|
-
projectId: input.projectId,
|
|
1627
|
-
}),
|
|
1703
|
+
markerPath: null,
|
|
1704
|
+
bindingPath,
|
|
1705
|
+
repository: publicRepository(await this.dependencies.repositories.resolve(repository.repoRoot)),
|
|
1628
1706
|
selectionRequired: false,
|
|
1629
1707
|
});
|
|
1630
1708
|
}
|
|
@@ -1669,11 +1747,11 @@ export class BridgeService {
|
|
|
1669
1747
|
}
|
|
1670
1748
|
const decision = await this.dependencies.repositoryDecisions.load(repository.repoFingerprint);
|
|
1671
1749
|
const authenticated = authentication.authenticated === true;
|
|
1672
|
-
const state = decision?.state
|
|
1750
|
+
const state = decision?.state === 'disabled' ? 'disabled' : repository.bindingPath ? 'bound' : 'none';
|
|
1673
1751
|
const client = await this.clientUpdate(authenticated);
|
|
1674
1752
|
const shipped = objectValue(objectValue(client)?.shippedKnowledge);
|
|
1675
1753
|
const incomplete = shipped && (shipped.failure !== null || Number(shipped.live) < Number(shipped.expected));
|
|
1676
|
-
const projectId = repository.projectId
|
|
1754
|
+
const projectId = repository.projectId;
|
|
1677
1755
|
const liveTasks = authenticated && state === 'bound'
|
|
1678
1756
|
? (await this.dependencies.activeContexts.list(repository.repoFingerprint)).map(describePointer)
|
|
1679
1757
|
: [];
|
|
@@ -2011,11 +2089,17 @@ export class BridgeService {
|
|
|
2011
2089
|
return asJsonValue({ queued: true, outboxId: queued.id, idempotencyKey });
|
|
2012
2090
|
}
|
|
2013
2091
|
}
|
|
2014
|
-
async flushOutbox() {
|
|
2015
|
-
const entries =
|
|
2092
|
+
async flushOutbox(taskId) {
|
|
2093
|
+
const entries = taskId
|
|
2094
|
+
? await this.dependencies.outbox.listForTask(taskId)
|
|
2095
|
+
: await this.dependencies.outbox.list();
|
|
2016
2096
|
const synchronized = [];
|
|
2017
2097
|
const blocked = [];
|
|
2098
|
+
const blockedTasks = new Set();
|
|
2018
2099
|
for (const entry of entries) {
|
|
2100
|
+
const deliveryTaskId = taskIdFromDeliverySafe(entry);
|
|
2101
|
+
if (blockedTasks.has(deliveryTaskId))
|
|
2102
|
+
continue;
|
|
2019
2103
|
try {
|
|
2020
2104
|
const response = await this.dependencies.client.request(entry.path, {
|
|
2021
2105
|
method: entry.method,
|
|
@@ -2041,7 +2125,7 @@ export class BridgeService {
|
|
|
2041
2125
|
await this.dependencies.journal.markDeliveryAttempt(entry.journalRef.projectId, entry.journalRef.taskSlug, entry.journalRef.eventId, errorKind);
|
|
2042
2126
|
}
|
|
2043
2127
|
blocked.push({ id: entry.id, error: errorKind });
|
|
2044
|
-
|
|
2128
|
+
blockedTasks.add(deliveryTaskId);
|
|
2045
2129
|
}
|
|
2046
2130
|
}
|
|
2047
2131
|
return asJsonValue({
|
|
@@ -2543,6 +2627,7 @@ function publicRepository(repository) {
|
|
|
2543
2627
|
return asJsonValue({
|
|
2544
2628
|
repoRoot: repository.repoRoot,
|
|
2545
2629
|
markerPath: repository.markerPath,
|
|
2630
|
+
bindingPath: repository.bindingPath,
|
|
2546
2631
|
projectId: repository.projectId,
|
|
2547
2632
|
schemaVersion: repository.schemaVersion,
|
|
2548
2633
|
repoFingerprint: repository.repoFingerprint,
|
|
@@ -81,6 +81,12 @@ export class OfflineOutbox {
|
|
|
81
81
|
}));
|
|
82
82
|
return values.filter((entry) => entry !== null).sort(compareEntries);
|
|
83
83
|
}
|
|
84
|
+
async listForTask(taskId) {
|
|
85
|
+
return (await this.list()).filter((entry) => {
|
|
86
|
+
const body = entry.body;
|
|
87
|
+
return (body !== null && typeof body === 'object' && !Array.isArray(body) && body.taskId === taskId);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
84
90
|
async markAttempt(id, errorKind) {
|
|
85
91
|
await this.exclusive(id, async () => {
|
|
86
92
|
const path = this.pathFor(id);
|
package/skill/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: engineering-memory
|
|
3
|
-
description: Enforces the Engineering Memory lifecycle for Codex and Claude in repositories bound
|
|
3
|
+
description: Enforces the Engineering Memory lifecycle for Codex and Claude in repositories bound in user-local Engineering Memory state. Use before answering anything about a bound repository, including a question about what it has or where its design lives, and for every planning, implementation, review, diagnosis, rework, Figma, component, screen, service, model, navigation, localization, storage, validation, or Git task in one.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Engineering Memory
|
|
@@ -13,7 +13,7 @@ Mandatory behavior:
|
|
|
13
13
|
|
|
14
14
|
Use the host's native questionnaire for every question to the user, including implementation choices, names, clarification, branch/worktree decisions and delivery. 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
15
|
|
|
16
|
-
1.
|
|
16
|
+
1. Discover the repository binding through `session.entry`. Bindings live in the user-level Engineering Memory state directory, outside the repository and installed runtime. No project settings file is required. For a bound repository, the project's knowledge is in the backend, so answer nothing about it before bootstrapping; the absence of local design files or records says nothing about its stored knowledge.
|
|
17
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.
|
|
18
18
|
3. Before planning or editing, call `session.bootstrap`. After compaction, a new chat, interruption, or handoff, call `session.resume` first.
|
|
19
19
|
4. Open the task in `read_only` mode for review, diagnosis, planning, or reporting that does not authorize writes; use `scaffold` mode only to apply organization architecture templates to a new project; otherwise use `write` mode. Do read-only discovery, then record the discovery checkpoint.
|
|
@@ -27,6 +27,6 @@ Use the host's native questionnaire for every question to the user, including im
|
|
|
27
27
|
12. Never commit, push, publish, deploy, approve a permanent memory revision, or overwrite an existing Git hook without explicit user authorization.
|
|
28
28
|
13. Never store tokens, passwords, client secrets, raw headers, raw payloads, customer data, or PII in tool inputs, journals, memory, logs, or generated documentation.
|
|
29
29
|
|
|
30
|
-
If the repository is unbound, do not silently create or attach a project. Use the native questionnaire workflow. If the user selects task-only skip, do not create a
|
|
30
|
+
If the repository is unbound, do not silently create or attach a project. Use the native questionnaire workflow. If the user selects task-only skip, do not create a binding or memory records.
|
|
31
31
|
|
|
32
32
|
If the backend is unavailable, use only an unexpired bridge-provided cached context. Checkpoints may enter the local outbox, but verification, task close, and commit remain blocked until synchronization succeeds.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
Sign-in decides nothing beyond who the user is. The organization and the project are chosen after it, through the questionnaires in `questionnaires.md`, and both listings end with an option to create a new one. Ask for both whenever this session has not already confirmed them, and ask again the moment the user says they want to change either — changing the organization always means choosing the project again.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Use the binding returned by `session.entry` as the authority. The bridge stores the selected project and repository identity under `~/.engineering-memory/origins/<api-hash>/project-bindings/`, outside the installed runtime and working tree. It imports a legacy `.engineering-memory/project.json` once, preserving its schema and exact backend fingerprint for existing tasks and receipts. It leaves that file unchanged; after migration the file is optional and may be removed through the repository's normal Git workflow. New bindings never create it. Never infer a selection from a directory or remote, and never edit binding records by hand. Separate clones or a changed remote require a new explicit selection; worktrees of the same checkout share the local binding.
|
|
8
8
|
|
|
9
9
|
An archived project is recoverable state, not a missing or conflicting binding. When an owner receives `project.restore`, use the `projectId` and `expectedVersion` in its data and call that operation before retrying. A non-owner receives `project.member_list` instead: list the members, identify a project owner and explain that the owner must restore the exact project before this session can retry. Use `project.list` with `includeArchived: true` when an owner must first select the archived project. Never create or bind a replacement project to escape archive state.
|
|
10
10
|
|
|
@@ -14,7 +14,7 @@ Every new write or scaffold task settles its branch through the native questionn
|
|
|
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
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
|
|
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 uses the same user-local binding and repository identity without writing a project marker. 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.
|
|
18
18
|
|
|
19
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.
|
|
20
20
|
|
|
@@ -43,22 +43,21 @@ A request can be as short as "add the KYC flow from Figma". That is enough, and
|
|
|
43
43
|
|
|
44
44
|
Then propose the `flow_logic` record and stop. The plan is not a message in the chat; it is the proposal, and the user approving it is the approval. Do not write the second screen before that approval exists — a flow's shape replicated across six screens costs six times as much to undo, and verification refuses a task that adds several screens without an approved flow record reconciled to it.
|
|
45
45
|
|
|
46
|
-
### Work that spans
|
|
46
|
+
### Work that spans multiple projects
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
as screens — and the user's discipline covers both, it is one work item and two tasks: one in
|
|
50
|
-
each repository. Verification and the commit gate stay per repository because a commit is.
|
|
48
|
+
Before implementation, load `work_item.plan` for the selected requirement and inspect current and authorized linked API/flow contracts. Decide whether backend, web, mobile or several projects need changes, even if the user mentioned only one interface. Fullstack covers every development area but never grants project membership or permission to expand scope.
|
|
51
49
|
|
|
52
|
-
|
|
53
|
-
moves to the other side, open its task with the same `workItemId` and that repository's root —
|
|
54
|
-
every tool takes `repoRoot`, and both stay open at once. The backend accepts that id only from the
|
|
55
|
-
current project or a directly linked project the account can work in, and derives the display key
|
|
56
|
-
from the work item. `context.prepare_change` then returns `siblingTasks`: the other task's
|
|
57
|
-
objective, status and recent checkpoints, so a decision taken on one side is visible on the other
|
|
58
|
-
without anyone repeating it. A legacy key is never an authorization boundary; key-only tasks can
|
|
59
|
-
see siblings only inside directly linked projects where the account is already a member.
|
|
50
|
+
If another project needs changes beyond the requested scope, explain the concrete missing behavior and use the native questionnaire to ask whether to include it. Do not use a chat instruction or invent approval. An explicit instruction already authorizing both sides is sufficient; reuse it. Call `work_item.confirm_plan` only with that approved scope, the common branch and a short reason. Resume an existing plan rather than asking again. An unavailable project needs administrator coordination; do not infer or bind its repository.
|
|
60
51
|
|
|
61
|
-
|
|
52
|
+
One WorkItem UUID coordinates independent EngineeringTask runs, one per selected repository. Use the recorded branch name (for example `task/kan-101`) in each repository, and a separate worktree for each concurrent task. `task.branch` with the exact externalTaskId records the existing user choice and creates or adopts the branch in a suitable worktree. Use its returned repoRoot for subsequent tools and file commands. Never switch another task's reserved worktree.
|
|
53
|
+
|
|
54
|
+
Open each side with the same workItemId and its own repository binding when work reaches that side. Each side has its own lease, reconciliation, verify, close and commit gate. Check `work_item.runs` and sibling checkpoints when handing off; one side completing does not prove all selected projects are complete. A plan's scope and branch cannot change while an implementation run is active. Read-only analysis does not acquire application write rights; its later write transition must satisfy discipline and the confirmed plan.
|
|
55
|
+
|
|
56
|
+
### Administration, product management and QA
|
|
57
|
+
|
|
58
|
+
Administrative authority and work discipline are independent. Organization admins can manage projects, assignments, repository URLs and work items in their organization. A global admin has those rights only in organizations they actively belong to. Ordinary users need a live explicit project assignment. Fullstack/backend/web/mobile/frontend disciplines govern implementation, while project overrides do not grant administration.
|
|
59
|
+
|
|
60
|
+
An assigned product manager can create, edit, assign, archive and restore work items through the work_item tools. Designer work includes design context and requirement participation; it does not grant general application code leases. A tester can create an unassigned bug and record `work_item.record_test` evidence, then track it with `work_item.test_results`. QA evidence must contain no credentials, PII or production payloads and must identify the exact requirement version. A passed result never silently marks the requirement done. Organization administration separately permits backlog management regardless of discipline.
|
|
62
61
|
|
|
63
62
|
### A task that reports a defect
|
|
64
63
|
|
|
@@ -185,4 +184,4 @@ When a pull request has been opened, keep going: check whether it merges cleanly
|
|
|
185
184
|
|
|
186
185
|
## Offline Behavior
|
|
187
186
|
|
|
188
|
-
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
|
|
187
|
+
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.
|
|
@@ -40,7 +40,7 @@ The user may say mid-chat that they want to change organization or project, swit
|
|
|
40
40
|
|
|
41
41
|
Switching off records the decision and ends the subject. Switching back on clears it and runs the organization and project questions again.
|
|
42
42
|
|
|
43
|
-
Changing the organization always means asking for the project again afterwards, because a project belongs to exactly one organization and the old answer cannot survive the change. Run the organization questionnaire, then the project one, then bind the repository to the chosen project with `project.resolve` so the
|
|
43
|
+
Changing the organization always means asking for the project again afterwards, because a project belongs to exactly one organization and the old answer cannot survive the change. Run the organization questionnaire, then the project one, then bind the repository to the chosen project with `project.resolve` so the local binding matches what the user just said.
|
|
44
44
|
|
|
45
45
|
When the repository being bound already has a working system in it — code somebody else wrote, a shape nobody here decided — say so and offer the survey rather than starting as though the project began now. Ask whether to adopt it: read what is there, record its modules, its architecture and the discovery policy that matches its actual layout, and list where it diverges from the rules with what changing and keeping each one would cost. Do not offer a single choice between reworking the project and leaving it alone; that question is asked once per divergence, after the survey, and the adoption rule says why. The survey is a read-only task and changes nothing.
|
|
46
46
|
|
|
@@ -48,11 +48,11 @@ A task that is still open blocks the switch, and says so plainly. Its lease, its
|
|
|
48
48
|
|
|
49
49
|
## Unbound Repository
|
|
50
50
|
|
|
51
|
-
Ask whether the current repository belongs to an existing accessible project, should become a new project, or should skip Engineering Memory for this task. When an existing project is chosen, present only projects returned for the authenticated user. Confirm before
|
|
51
|
+
Ask whether the current repository belongs to an existing accessible project, should become a new project, or should skip Engineering Memory for this task. When an existing project is chosen, present only projects returned for the authenticated user. Confirm the project selection before saving its user-local binding; no repository file is written.
|
|
52
52
|
|
|
53
|
-
For a repository the backend has not seen before, ask whether this is an existing codebase import or a greenfield project. Before a
|
|
53
|
+
For a repository the backend has not seen before, ask whether this is an existing codebase import or a greenfield project. Before a local binding or backend task exists, an existing-codebase import may perform one bounded local read-only structural and Figma discovery pass. It must not edit code. Use the findings in a native questionnaire to confirm the initial project profile and the repository-relative patterns that identify new memory resources. Send them as `discoveryUnits`, one unit per kind the project actually has: a client project declares `screen_logic` and `component_mapping`, and a server-side project declares `module_logic`, `data_model` and `api_endpoint` against its own layout. Never ask a server-side project for screen patterns, and never invent a unit for a kind the repository does not contain. Then call `project.setup`; the backend creates the project, owner membership, active profile revision, and pinned discovery policy atomically. Persist the user-local binding only after that succeeds, then start the normal `session.bootstrap` lifecycle. Later discoveries are reviewable memory proposals.
|
|
54
54
|
|
|
55
|
-
Greenfield setup asks for project name, framework, the response envelope, the exception model, authentication needs, localization languages, storage policy, and the initial discovery patterns. A client project is also asked for the Figma library and screen links if available, the design token sources, the page architecture and the navigation pattern; a server-side project is asked instead for the data layer, the migration tool and how configuration and secrets arrive. A Spring project must also name Maven or Gradle, its Java release, its exact Spring Boot version and its `javax` or `jakarta` namespace before any architecture template is offered; `Spring Boot` alone is not enough information to select compatible source. Record those confirmed values in `initialProjectProfile.metadata.runtimeContract` as `language: "java"`, `languageVersion`, `framework: "spring-boot"`, `frameworkVersion`, `buildTool` and `namespace`. Never infer or invent this object. The backend compares it with each source module and returns no incompatible module; an absent contract means broad Spring rules still apply but no constrained source template is offered. Ask what the project is before deciding which list applies. Do not
|
|
55
|
+
Greenfield setup asks for project name, framework, the response envelope, the exception model, authentication needs, localization languages, storage policy, and the initial discovery patterns. A client project is also asked for the Figma library and screen links if available, the design token sources, the page architecture and the navigation pattern; a server-side project is asked instead for the data layer, the migration tool and how configuration and secrets arrive. A Spring project must also name Maven or Gradle, its Java release, its exact Spring Boot version and its `javax` or `jakarta` namespace before any architecture template is offered; `Spring Boot` alone is not enough information to select compatible source. Record those confirmed values in `initialProjectProfile.metadata.runtimeContract` as `language: "java"`, `languageVersion`, `framework: "spring-boot"`, `frameworkVersion`, `buildTool` and `namespace`. Never infer or invent this object. The backend compares it with each source module and returns no incompatible module; an absent contract means broad Spring rules still apply but no constrained source template is offered. Ask what the project is before deciding which list applies. Do not save a binding unless the setup response contains the active initial profile and discovery policy.
|
|
56
56
|
|
|
57
57
|
After setup, ask whether to build the project from the organization architecture templates. If the user accepts, follow `scaffolding.md`: confirm the optional modules, then confirm the package name and the concrete class name behind every rename placeholder in one questionnaire, then ask for each required asset role and each tenant-specific value the templates deliberately leave open.
|
|
58
58
|
|