engineering-memory 0.3.0 → 0.4.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/package.json +1 -1
- package/runtime/dist/src/config.js +3 -0
- package/runtime/dist/src/git/git-inspector.js +14 -0
- package/runtime/dist/src/mcp/tool-definitions.js +29 -0
- package/runtime/dist/src/runtime/bridge-service.js +82 -4
- package/skill/references/lifecycle.md +16 -1
- package/skill/references/questionnaires.md +20 -0
- package/skill/references/scaffolding.md +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "engineering-memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|
|
@@ -57,5 +57,8 @@ export const endpoints = {
|
|
|
57
57
|
projectResolve: '/projects/resolve',
|
|
58
58
|
projectBind: (projectId) => `/projects/${projectId}/bind`,
|
|
59
59
|
projectMemberAdd: (projectId) => `/projects/${projectId}/members`,
|
|
60
|
+
projectLink: '/projects/link',
|
|
61
|
+
projectUnlink: '/projects/unlink',
|
|
62
|
+
projectLinks: (projectId) => `/projects/${projectId}/links`,
|
|
60
63
|
};
|
|
61
64
|
//# sourceMappingURL=config.js.map
|
|
@@ -262,6 +262,20 @@ export class GitInspector {
|
|
|
262
262
|
mode: fileStat.mode & 0o111 ? '100755' : '100644',
|
|
263
263
|
};
|
|
264
264
|
}
|
|
265
|
+
async clone(repositoryUrl, targetPath) {
|
|
266
|
+
if (!/^https:\/\//.test(repositoryUrl)) {
|
|
267
|
+
return {
|
|
268
|
+
cloned: false,
|
|
269
|
+
reason: 'only an https repository address is cloned',
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
const result = await this.runner.run('git', ['clone', repositoryUrl, targetPath], {
|
|
273
|
+
cwd: process.cwd(),
|
|
274
|
+
});
|
|
275
|
+
return result.exitCode === 0
|
|
276
|
+
? { cloned: true, reason: '' }
|
|
277
|
+
: { cloned: false, reason: result.stderr.trim().slice(0, 400) };
|
|
278
|
+
}
|
|
265
279
|
async pathHistorySubjects(repoRoot, paths) {
|
|
266
280
|
if (paths.length === 0)
|
|
267
281
|
return [];
|
|
@@ -73,6 +73,10 @@ export const engineeringMemoryToolNames = [
|
|
|
73
73
|
'project.list',
|
|
74
74
|
'project.resolve',
|
|
75
75
|
'project.member_add',
|
|
76
|
+
'project.clone',
|
|
77
|
+
'project.link',
|
|
78
|
+
'project.unlink',
|
|
79
|
+
'project.links',
|
|
76
80
|
'auth.status',
|
|
77
81
|
'auth.signin_browser',
|
|
78
82
|
'auth.logout',
|
|
@@ -445,6 +449,31 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
445
449
|
confirmLocalOnly: z.boolean().optional(),
|
|
446
450
|
}),
|
|
447
451
|
}, async (input) => toolResult(await service.authLogout(input)));
|
|
452
|
+
server.registerTool('project.clone', {
|
|
453
|
+
description: "Clone a project's repository into an empty directory, for a project this account belongs to that has a repository address recorded. The clone uses the developer's own Git credentials; Engineering Memory never asks for, stores or transmits one, so an access failure is reported and left to them.",
|
|
454
|
+
inputSchema: z.object({
|
|
455
|
+
projectId: z.string().min(1),
|
|
456
|
+
targetPath: z.string().min(1).describe('Where to place the checkout.'),
|
|
457
|
+
}),
|
|
458
|
+
}, async (input) => toolResult(await service.projectClone(input)));
|
|
459
|
+
server.registerTool('project.link', {
|
|
460
|
+
description: 'Link a backend project to a frontend project of the same organization, so each side sees the endpoint and flow contracts of the other. Only an organization owner may do this.',
|
|
461
|
+
inputSchema: z.object({
|
|
462
|
+
backendProjectId: z.string().min(1),
|
|
463
|
+
frontendProjectId: z.string().min(1),
|
|
464
|
+
}),
|
|
465
|
+
}, async (input) => toolResult(await service.projectLink(input)));
|
|
466
|
+
server.registerTool('project.unlink', {
|
|
467
|
+
description: 'Remove the link between a backend and a frontend project. Only an organization owner may do this.',
|
|
468
|
+
inputSchema: z.object({
|
|
469
|
+
backendProjectId: z.string().min(1),
|
|
470
|
+
frontendProjectId: z.string().min(1),
|
|
471
|
+
}),
|
|
472
|
+
}, async (input) => toolResult(await service.projectUnlink(input)));
|
|
473
|
+
server.registerTool('project.links', {
|
|
474
|
+
description: 'List the projects linked to this one, and which side of the product each is.',
|
|
475
|
+
inputSchema: z.object({ projectId: z.string().min(1) }),
|
|
476
|
+
}, async (input) => toolResult(await service.projectLinks(input)));
|
|
448
477
|
server.registerTool('auth.status', {
|
|
449
478
|
description: 'Check whether OS credential storage contains an active local Engineering Memory session.',
|
|
450
479
|
inputSchema: z.object({}),
|
|
@@ -474,6 +474,73 @@ export class BridgeService {
|
|
|
474
474
|
});
|
|
475
475
|
});
|
|
476
476
|
}
|
|
477
|
+
async clonableProjects() {
|
|
478
|
+
try {
|
|
479
|
+
const response = await this.dependencies.client.request(endpoints.projectList);
|
|
480
|
+
const projects = Array.isArray(response.data) ? response.data : [];
|
|
481
|
+
return projects.flatMap((entry) => {
|
|
482
|
+
const project = objectValue(entry);
|
|
483
|
+
const repositoryUrl = project?.repositoryUrl;
|
|
484
|
+
return project && typeof repositoryUrl === 'string' && repositoryUrl
|
|
485
|
+
? [
|
|
486
|
+
asJsonValue({
|
|
487
|
+
projectId: project.id,
|
|
488
|
+
name: project.name,
|
|
489
|
+
framework: project.framework ?? null,
|
|
490
|
+
repositoryUrl,
|
|
491
|
+
}),
|
|
492
|
+
]
|
|
493
|
+
: [];
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
catch {
|
|
497
|
+
return [];
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
async projectClone(input) {
|
|
501
|
+
return await this.execute(async () => {
|
|
502
|
+
const projects = await this.clonableProjects();
|
|
503
|
+
const project = projects
|
|
504
|
+
.map((entry) => objectValue(entry))
|
|
505
|
+
.find((entry) => entry?.projectId === input.projectId);
|
|
506
|
+
if (!project) {
|
|
507
|
+
throw new Error('That project has no repository address recorded, or this account is not a member of it. An organization owner records the address.');
|
|
508
|
+
}
|
|
509
|
+
const result = await this.dependencies.repositories.git.clone(String(project.repositoryUrl), input.targetPath);
|
|
510
|
+
if (!result.cloned) {
|
|
511
|
+
throw new Error(`The repository could not be cloned: ${result.reason}. Engineering Memory never handles Git credentials, so this is between the developer's own Git configuration and the host.`);
|
|
512
|
+
}
|
|
513
|
+
return asJsonValue({
|
|
514
|
+
cloned: true,
|
|
515
|
+
projectId: input.projectId,
|
|
516
|
+
path: input.targetPath,
|
|
517
|
+
});
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
async projectLink(input) {
|
|
521
|
+
return await this.execute(async () => {
|
|
522
|
+
const response = await this.dependencies.client.request(endpoints.projectLink, {
|
|
523
|
+
method: 'POST',
|
|
524
|
+
body: cleanJson(input),
|
|
525
|
+
});
|
|
526
|
+
return asJsonValue({ link: response.data });
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
async projectUnlink(input) {
|
|
530
|
+
return await this.execute(async () => {
|
|
531
|
+
await this.dependencies.client.request(endpoints.projectUnlink, {
|
|
532
|
+
method: 'POST',
|
|
533
|
+
body: cleanJson(input),
|
|
534
|
+
});
|
|
535
|
+
return asJsonValue({ unlinked: true });
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
async projectLinks(input) {
|
|
539
|
+
return await this.execute(async () => {
|
|
540
|
+
const response = await this.dependencies.client.request(endpoints.projectLinks(input.projectId), { method: 'POST', body: {} });
|
|
541
|
+
return asJsonValue({ links: objectValue(response.data)?.links ?? [] });
|
|
542
|
+
});
|
|
543
|
+
}
|
|
477
544
|
async sessionAnswerShadowNotice(input) {
|
|
478
545
|
return await this.execute(async () => {
|
|
479
546
|
await this.dependencies.shadowNotices.answer(input.resourceId, input.revisionNumber);
|
|
@@ -1222,11 +1289,16 @@ export class BridgeService {
|
|
|
1222
1289
|
repository = await this.dependencies.repositories.resolve(input.repoRoot ?? process.cwd());
|
|
1223
1290
|
}
|
|
1224
1291
|
catch (error) {
|
|
1292
|
+
const authenticated = authentication.authenticated === true;
|
|
1293
|
+
const clonable = authenticated ? await this.clonableProjects() : [];
|
|
1225
1294
|
return asJsonValue({
|
|
1226
|
-
authenticated
|
|
1295
|
+
authenticated,
|
|
1227
1296
|
repository: null,
|
|
1228
1297
|
decision: 'none',
|
|
1229
|
-
|
|
1298
|
+
clonableProjects: clonable,
|
|
1299
|
+
nextAction: clonable.length > 0
|
|
1300
|
+
? 'This directory is not a Git repository. Offer the listed projects and clone the one the user chooses with project.clone. Do not mention Engineering Memory otherwise.'
|
|
1301
|
+
: 'This directory is not a Git repository, so Engineering Memory has nothing to bind. Work normally and say nothing about it.',
|
|
1230
1302
|
reason: error instanceof Error ? error.message : String(error),
|
|
1231
1303
|
});
|
|
1232
1304
|
}
|
|
@@ -1794,6 +1866,7 @@ export class BridgeService {
|
|
|
1794
1866
|
return await this.deliverCloseIntent(repository, body, intent.taskChanges, true);
|
|
1795
1867
|
}
|
|
1796
1868
|
async deliverCloseIntent(repository, body, taskChanges, recoveredAfterResponseLoss) {
|
|
1869
|
+
const touchedContract = taskChanges.some((change) => /\.controller\.[tj]s$/.test(change.path));
|
|
1797
1870
|
let response;
|
|
1798
1871
|
try {
|
|
1799
1872
|
response = await this.dependencies.client.request(endpoints.taskClose, {
|
|
@@ -1828,7 +1901,7 @@ export class BridgeService {
|
|
|
1828
1901
|
...closedTask,
|
|
1829
1902
|
repository: publicRepository(repository),
|
|
1830
1903
|
recoveredAfterResponseLoss,
|
|
1831
|
-
delivery: deliveryQuestion(),
|
|
1904
|
+
delivery: deliveryQuestion(touchedContract),
|
|
1832
1905
|
});
|
|
1833
1906
|
}
|
|
1834
1907
|
async seedResumeSnapshot(pointer, backendPatch) {
|
|
@@ -2078,7 +2151,7 @@ function entryNextAction(authenticated, decision) {
|
|
|
2078
2151
|
}
|
|
2079
2152
|
return 'Ask which organization and then which project, whatever the user asked for, listing what they already have with the option to create a new one last. Switching Engineering Memory off in this repository is the other answer, and it is remembered.';
|
|
2080
2153
|
}
|
|
2081
|
-
function deliveryQuestion() {
|
|
2154
|
+
function deliveryQuestion(touchedContract = false) {
|
|
2082
2155
|
return asJsonValue({
|
|
2083
2156
|
required: true,
|
|
2084
2157
|
question: 'The task is closed and nothing has been committed. Ask the user which of these to do, and do only what they choose.',
|
|
@@ -2096,6 +2169,11 @@ function deliveryQuestion() {
|
|
|
2096
2169
|
input: 'base branch the pull request targets',
|
|
2097
2170
|
},
|
|
2098
2171
|
],
|
|
2172
|
+
...(touchedContract
|
|
2173
|
+
? {
|
|
2174
|
+
alsoAsk: 'This task changed an endpoint contract. Ask whether to document the change for the linked project, and if so write what the other side must do differently with memory.propose_revision as an integration_note.',
|
|
2175
|
+
}
|
|
2176
|
+
: {}),
|
|
2099
2177
|
afterPullRequest: 'Do not end the turn once a pull request exists. Check whether it merges cleanly, report the conflicting files if it does not, and ask whether to resolve them before touching anything.',
|
|
2100
2178
|
});
|
|
2101
2179
|
}
|
|
@@ -25,7 +25,7 @@ This is what makes the difference between changing code and understanding it. A
|
|
|
25
25
|
A request can be as short as "add the KYC flow from Figma". That is enough, and it is not a licence to guess. Work it out:
|
|
26
26
|
|
|
27
27
|
1. The Figma address arrives in the core pack as the `figma_reference` record. Never ask where the design lives; read it. If the user names a different file, that is a correction to that record — propose the revision and ask for approval in the same reply, so the next session already knows.
|
|
28
|
-
2. Ask for existing flows with `memory.query` naming the `flow_logic` kind. If the flow is already recorded, read it and its history before touching anything.
|
|
28
|
+
2. Ask for existing flows with `memory.query` naming the `flow_logic` kind. If the flow is already recorded, read it and its history before touching anything — including a record belonging to the linked project on the other side of the product, which arrives in the core pack. An approved flow record outranks re-deriving the same flow from the design, and deriving it twice is how two halves of one product end up disagreeing about what the steps are.
|
|
29
29
|
3. Read the flow in Figma, at orientation depth only: which frames belong to it, the order its prototype links imply, and the node id of every screen. A file-level link is orientation, not evidence. Stop there — geometry, fills, typography and tokens belong to the screen being built, read at the moment it is built, not gathered for the whole flow up front. Read this yourself rather than delegating it; nothing else can proceed until it is answered, so a second agent only adds the wait.
|
|
30
30
|
4. Read what surrounds it — the screen records the flow starts from and returns to, the navigation contract, the trackers that already exist — and pull `memory.history` wherever the header shows earlier work or a run of corrections.
|
|
31
31
|
5. Decide the entry point, the order, whether a tracker is needed and exactly what it carries, and where the flow ends.
|
|
@@ -33,6 +33,21 @@ A request can be as short as "add the KYC flow from Figma". That is enough, and
|
|
|
33
33
|
|
|
34
34
|
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.
|
|
35
35
|
|
|
36
|
+
### Work that spans both sides of a product
|
|
37
|
+
|
|
38
|
+
When the request covers both halves of a linked product — a flow that needs endpoints as well
|
|
39
|
+
as screens — and the user's discipline covers both, it is one work item and two tasks: one in
|
|
40
|
+
each repository. Verification and the commit gate stay per repository because a commit is.
|
|
41
|
+
|
|
42
|
+
Ask which side to start on, propose the work item key, and open the task in the repository
|
|
43
|
+
being worked in. When the work moves to the other side, open its task with the same
|
|
44
|
+
`workItemKey` and that repository's root — every tool takes `repoRoot`, and both stay open at
|
|
45
|
+
once. `context.prepare_change` then returns `siblingTasks`: the other task's objective, status
|
|
46
|
+
and recent checkpoints, so a decision taken on one side is visible on the other without anyone
|
|
47
|
+
repeating it.
|
|
48
|
+
|
|
49
|
+
Do not open the second task speculatively. Open it when the work actually reaches that side.
|
|
50
|
+
|
|
36
51
|
### A task that reports a defect
|
|
37
52
|
|
|
38
53
|
Settle first whether the report is about what the user sees or where they go. If it is, the
|
|
@@ -113,6 +113,26 @@ Offer the product option whenever the classification supports it, and say plainl
|
|
|
113
113
|
|
|
114
114
|
For any permanent option, show the old rule, proposed rule, reason, affected areas, and regression evidence. Create a proposal but do not approve it until the user explicitly confirms the proposal review.
|
|
115
115
|
|
|
116
|
+
## Work Across Both Sides
|
|
117
|
+
|
|
118
|
+
When a request covers a linked backend and frontend and the user's discipline covers both, say
|
|
119
|
+
so and ask which side to start on and what to call the work item — offering the task
|
|
120
|
+
identifier if there is one. Confirm before opening the task on the second side; it is opened
|
|
121
|
+
when the work reaches it, not in advance.
|
|
122
|
+
|
|
123
|
+
When the discipline covers only one side, say plainly which part of the request this account
|
|
124
|
+
can carry and leave the rest for whoever holds the other discipline.
|
|
125
|
+
|
|
126
|
+
## Documenting for the Linked Project
|
|
127
|
+
|
|
128
|
+
When a task changed anything an `api_endpoint` record covers, the delivery question gains an
|
|
129
|
+
option to document the change for the linked project. Take it when the other side has to do
|
|
130
|
+
something differently: a changed request or response shape, a new error code, a route that
|
|
131
|
+
moved. Say what changed and what the other side must do, not what was refactored internally.
|
|
132
|
+
|
|
133
|
+
A note can also be asked for at any time, in a chat of its own, for work that was already
|
|
134
|
+
finished.
|
|
135
|
+
|
|
116
136
|
## Delivery
|
|
117
137
|
|
|
118
138
|
After `task.close`, and every time, ask the user what to do with the finished work. Nothing has been committed at this point and nothing may be until they answer. Offer exactly these:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Greenfield Scaffolding
|
|
2
2
|
|
|
3
|
-
Architecture templates are
|
|
3
|
+
Architecture templates are source modules stored as approved engineering memory. The product ships a starter for each stack it supports and an organization may hold its own, which takes precedence; a template never belongs to a single project. A greenfield project reproduces the team architecture from those modules instead of re-deriving it from prose rules. The backend stores, versions and authorizes them; it never generates code. The agent applies the rename map and writes every file itself.
|
|
4
4
|
|
|
5
5
|
## Organization first
|
|
6
6
|
|
|
@@ -15,7 +15,7 @@ Call `organization.list` and ask the user which organization the project belongs
|
|
|
15
15
|
3. Present the optional modules through the native questionnaire. A package-shaped project usually skips the application modules; an application usually takes them.
|
|
16
16
|
4. Confirm the naming decisions in one questionnaire: package name and the concrete class name behind every rename placeholder. Never invent a name the user did not choose.
|
|
17
17
|
5. For each module in `applyOrder`, call `architecture.module`, apply the rename map and string replacements, write the files, then call `architecture.record_application` with the template path and the written path of every file. Respect `dependsOn`; do not reorder modules.
|
|
18
|
-
6. Merge every module's `
|
|
18
|
+
6. Merge every module's `packageDependencies` into the project's dependency manifest — `pubspec.yaml`, `package.json`, whatever the stack uses. Keep the existing constraint when a dependency already exists and report the conflict.
|
|
19
19
|
7. Satisfy the `assetContract`. Ask the user for each required asset role. Never invent an asset, never ship a placeholder binary, and never copy a licensed font from another project.
|
|
20
20
|
8. Resolve every `tenantSpecific` point through the questionnaire: base URLs, backend header contracts, storage key prefixes, supported locales, bundle identifiers. These are deliberately absent from the template. Do not guess them.
|
|
21
21
|
9. Run code generation and localization generation, then static analysis and tests. Record the validation checkpoints as usual.
|
|
@@ -27,7 +27,7 @@ Verification exempts a scaffolded file only while its working-tree hash still eq
|
|
|
27
27
|
|
|
28
28
|
Nothing else is relaxed. A file you edit after scaffolding no longer matches its recorded hash and falls back to the normal screen and component memory rules. A file the template never declared is never exempt. A scaffold task that recorded no applied module fails verification, so scaffold mode cannot be used to skip memory obligations on ordinary work.
|
|
29
29
|
|
|
30
|
-
Write the first real screen as a separate `write` task. Do not extend the scaffold task to cover feature work.
|
|
30
|
+
Write the first real unit of work — a screen, a resource — as a separate `write` task. Do not extend the scaffold task to cover feature work.
|
|
31
31
|
|
|
32
32
|
## Keeping templates current
|
|
33
33
|
|