engineering-memory 0.2.6 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "engineering-memory",
3
- "version": "0.2.6",
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: authentication.authenticated === true,
1295
+ authenticated,
1227
1296
  repository: null,
1228
1297
  decision: 'none',
1229
- nextAction: 'This directory is not a Git repository, so Engineering Memory has nothing to bind. Work normally and say nothing about it.',
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,14 +25,29 @@ 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.
29
- 3. Read the flow in Figma: 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.
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
+ 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.
32
32
  6. Ask the user what neither Figma nor memory can answer. Where a flow is entered from and what abandoning it halfway does are almost never in the design. Never invent them, and never invent the content of a document the flow displays.
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
@@ -22,4 +22,12 @@ Screen logic is canonical in the backend. A screen revision should cover purpose
22
22
 
23
23
  Component mappings should cover the Flutter symbol and path, public API, purpose, states, design tokens, responsive behavior, Figma file and exact node IDs, assets, usage guidance, tests, selectors, and evidence. Mark missing or partial Figma evidence instead of inventing node IDs.
24
24
 
25
+ A backend project records three kinds instead of screens and components, and which kinds a project has is declared in its project profile rather than assumed.
26
+
27
+ A `module_logic` record covers one resource: what it is for, the entities it owns, the endpoints it exposes, the guards that protect them, the background jobs and queues it runs, what it depends on and why that dependency exists.
28
+
29
+ A `data_model` record covers one entity: its columns and their types, its relations, what it inherits from the base entity, and every index it declares **with the query that index exists to serve**. An index nobody can name a query for is reported as a finding rather than recorded as a fact.
30
+
31
+ An `api_endpoint` record covers one route and is the surface a client binds to: method and path, the request DTO, the response DTO, the guards, the error codes it can return, the localization keys it uses, its pagination shape, and which client consumes it. When a linked project exists, these are the records that cross to it, so they are written for a reader on the other side of the wire rather than for the person who wrote the controller.
32
+
25
33
  Do not send full source trees, generated files, vendor assets, raw diffs, credentials, or user data as memory content. Prefer relative paths, symbols, hashes, contract summaries, exact approved Figma identifiers, and bounded task-specific evidence.
@@ -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 organization-scoped source modules stored as approved engineering memory. 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.
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 `pubspecDependencies` into the manifest file. Keep the existing constraint when a dependency already exists and report the conflict.
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