engineering-memory 0.3.0 → 1.0.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 +45 -2
- package/runtime/dist/src/runtime/bridge-service.js +110 -20
- package/skill/references/lifecycle.md +21 -2
- 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": "1.0.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',
|
|
@@ -376,8 +380,22 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
376
380
|
initialProjectProfile: z.object({
|
|
377
381
|
title: z.string().min(2),
|
|
378
382
|
content: z.string().min(2),
|
|
379
|
-
|
|
380
|
-
|
|
383
|
+
discoveryUnits: z
|
|
384
|
+
.array(z.object({
|
|
385
|
+
kind: z.enum([
|
|
386
|
+
'screen_logic',
|
|
387
|
+
'component_mapping',
|
|
388
|
+
'module_logic',
|
|
389
|
+
'data_model',
|
|
390
|
+
'api_endpoint',
|
|
391
|
+
]),
|
|
392
|
+
patterns: z.array(z.string().min(1)).min(1),
|
|
393
|
+
}))
|
|
394
|
+
.min(1)
|
|
395
|
+
.optional()
|
|
396
|
+
.describe('What this project calls its units and where they live. A Flutter project declares screen_logic and component_mapping; a backend declares module_logic, data_model and api_endpoint.'),
|
|
397
|
+
screenPathPatterns: z.array(z.string().min(1)).min(1).optional(),
|
|
398
|
+
componentPathPatterns: z.array(z.string().min(1)).min(1).optional(),
|
|
381
399
|
metadata: jsonObject,
|
|
382
400
|
provenance: jsonObject,
|
|
383
401
|
}),
|
|
@@ -445,6 +463,31 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
445
463
|
confirmLocalOnly: z.boolean().optional(),
|
|
446
464
|
}),
|
|
447
465
|
}, async (input) => toolResult(await service.authLogout(input)));
|
|
466
|
+
server.registerTool('project.clone', {
|
|
467
|
+
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.",
|
|
468
|
+
inputSchema: z.object({
|
|
469
|
+
projectId: z.string().min(1),
|
|
470
|
+
targetPath: z.string().min(1).describe('Where to place the checkout.'),
|
|
471
|
+
}),
|
|
472
|
+
}, async (input) => toolResult(await service.projectClone(input)));
|
|
473
|
+
server.registerTool('project.link', {
|
|
474
|
+
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.',
|
|
475
|
+
inputSchema: z.object({
|
|
476
|
+
backendProjectId: z.string().min(1),
|
|
477
|
+
frontendProjectId: z.string().min(1),
|
|
478
|
+
}),
|
|
479
|
+
}, async (input) => toolResult(await service.projectLink(input)));
|
|
480
|
+
server.registerTool('project.unlink', {
|
|
481
|
+
description: 'Remove the link between a backend and a frontend project. Only an organization owner may do this.',
|
|
482
|
+
inputSchema: z.object({
|
|
483
|
+
backendProjectId: z.string().min(1),
|
|
484
|
+
frontendProjectId: z.string().min(1),
|
|
485
|
+
}),
|
|
486
|
+
}, async (input) => toolResult(await service.projectUnlink(input)));
|
|
487
|
+
server.registerTool('project.links', {
|
|
488
|
+
description: 'List the projects linked to this one, and which side of the product each is.',
|
|
489
|
+
inputSchema: z.object({ projectId: z.string().min(1) }),
|
|
490
|
+
}, async (input) => toolResult(await service.projectLinks(input)));
|
|
448
491
|
server.registerTool('auth.status', {
|
|
449
492
|
description: 'Check whether OS credential storage contains an active local Engineering Memory session.',
|
|
450
493
|
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);
|
|
@@ -1062,8 +1129,7 @@ export class BridgeService {
|
|
|
1062
1129
|
!policy ||
|
|
1063
1130
|
!marker ||
|
|
1064
1131
|
marker.projectId !== project.id ||
|
|
1065
|
-
|
|
1066
|
-
stringArray(policy.componentPathPatterns).length === 0) {
|
|
1132
|
+
readDiscoveryUnits(policy).length === 0) {
|
|
1067
1133
|
throw new Error('Project setup response is not policy-ready');
|
|
1068
1134
|
}
|
|
1069
1135
|
const markerPath = await this.dependencies.repositories.writeMarker(repository.repoRoot, project.id);
|
|
@@ -1222,11 +1288,16 @@ export class BridgeService {
|
|
|
1222
1288
|
repository = await this.dependencies.repositories.resolve(input.repoRoot ?? process.cwd());
|
|
1223
1289
|
}
|
|
1224
1290
|
catch (error) {
|
|
1291
|
+
const authenticated = authentication.authenticated === true;
|
|
1292
|
+
const clonable = authenticated ? await this.clonableProjects() : [];
|
|
1225
1293
|
return asJsonValue({
|
|
1226
|
-
authenticated
|
|
1294
|
+
authenticated,
|
|
1227
1295
|
repository: null,
|
|
1228
1296
|
decision: 'none',
|
|
1229
|
-
|
|
1297
|
+
clonableProjects: clonable,
|
|
1298
|
+
nextAction: clonable.length > 0
|
|
1299
|
+
? '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.'
|
|
1300
|
+
: 'This directory is not a Git repository, so Engineering Memory has nothing to bind. Work normally and say nothing about it.',
|
|
1230
1301
|
reason: error instanceof Error ? error.message : String(error),
|
|
1231
1302
|
});
|
|
1232
1303
|
}
|
|
@@ -1794,6 +1865,7 @@ export class BridgeService {
|
|
|
1794
1865
|
return await this.deliverCloseIntent(repository, body, intent.taskChanges, true);
|
|
1795
1866
|
}
|
|
1796
1867
|
async deliverCloseIntent(repository, body, taskChanges, recoveredAfterResponseLoss) {
|
|
1868
|
+
const touchedContract = taskChanges.some((change) => /\.controller\.[tj]s$/.test(change.path));
|
|
1797
1869
|
let response;
|
|
1798
1870
|
try {
|
|
1799
1871
|
response = await this.dependencies.client.request(endpoints.taskClose, {
|
|
@@ -1828,7 +1900,7 @@ export class BridgeService {
|
|
|
1828
1900
|
...closedTask,
|
|
1829
1901
|
repository: publicRepository(repository),
|
|
1830
1902
|
recoveredAfterResponseLoss,
|
|
1831
|
-
delivery: deliveryQuestion(),
|
|
1903
|
+
delivery: deliveryQuestion(touchedContract),
|
|
1832
1904
|
});
|
|
1833
1905
|
}
|
|
1834
1906
|
async seedResumeSnapshot(pointer, backendPatch) {
|
|
@@ -2078,7 +2150,7 @@ function entryNextAction(authenticated, decision) {
|
|
|
2078
2150
|
}
|
|
2079
2151
|
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
2152
|
}
|
|
2081
|
-
function deliveryQuestion() {
|
|
2153
|
+
function deliveryQuestion(touchedContract = false) {
|
|
2082
2154
|
return asJsonValue({
|
|
2083
2155
|
required: true,
|
|
2084
2156
|
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 +2168,11 @@ function deliveryQuestion() {
|
|
|
2096
2168
|
input: 'base branch the pull request targets',
|
|
2097
2169
|
},
|
|
2098
2170
|
],
|
|
2171
|
+
...(touchedContract
|
|
2172
|
+
? {
|
|
2173
|
+
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.',
|
|
2174
|
+
}
|
|
2175
|
+
: {}),
|
|
2099
2176
|
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
2177
|
});
|
|
2101
2178
|
}
|
|
@@ -2291,18 +2368,11 @@ export function newMemoryResourceCandidate(entry, policy) {
|
|
|
2291
2368
|
return [];
|
|
2292
2369
|
}
|
|
2293
2370
|
if (policy) {
|
|
2294
|
-
const
|
|
2295
|
-
|
|
2296
|
-
if (screen && component) {
|
|
2371
|
+
const matched = policy.units.filter((unit) => unit.patterns.some((pattern) => globMatches(entry.path, pattern)));
|
|
2372
|
+
if (matched.length > 1) {
|
|
2297
2373
|
throw new Error(`Resource discovery policy is ambiguous for path: ${entry.path}`);
|
|
2298
2374
|
}
|
|
2299
|
-
|
|
2300
|
-
return [{ path: entry.path, kind: 'screen_logic' }];
|
|
2301
|
-
}
|
|
2302
|
-
if (component) {
|
|
2303
|
-
return [{ path: entry.path, kind: 'component_mapping' }];
|
|
2304
|
-
}
|
|
2305
|
-
return [];
|
|
2375
|
+
return matched[0] ? [{ path: entry.path, kind: matched[0].kind }] : [];
|
|
2306
2376
|
}
|
|
2307
2377
|
const extension = entry.path.toLowerCase().match(/\.[^.\/]+$/)?.[0];
|
|
2308
2378
|
if (!extension ||
|
|
@@ -2342,12 +2412,32 @@ function readResourceDiscoveryPolicy(snapshot, activeLease) {
|
|
|
2342
2412
|
if (!discovery) {
|
|
2343
2413
|
return null;
|
|
2344
2414
|
}
|
|
2345
|
-
const
|
|
2346
|
-
|
|
2347
|
-
if (screenPathPatterns.length === 0 || componentPathPatterns.length === 0) {
|
|
2415
|
+
const units = readDiscoveryUnits(discovery);
|
|
2416
|
+
if (units.length === 0) {
|
|
2348
2417
|
throw new Error('Pinned resource discovery policy is incomplete');
|
|
2349
2418
|
}
|
|
2350
|
-
return {
|
|
2419
|
+
return { units };
|
|
2420
|
+
}
|
|
2421
|
+
function readDiscoveryUnits(discovery) {
|
|
2422
|
+
if (!discovery)
|
|
2423
|
+
return [];
|
|
2424
|
+
const declared = Array.isArray(discovery.units) ? discovery.units : null;
|
|
2425
|
+
if (declared) {
|
|
2426
|
+
return declared.flatMap((entry) => {
|
|
2427
|
+
const unit = objectValue(entry);
|
|
2428
|
+
const patterns = stringArray(unit?.patterns);
|
|
2429
|
+
return typeof unit?.kind === 'string' && patterns.length > 0
|
|
2430
|
+
? [{ kind: unit.kind, patterns }]
|
|
2431
|
+
: [];
|
|
2432
|
+
});
|
|
2433
|
+
}
|
|
2434
|
+
return [
|
|
2435
|
+
{ kind: 'screen_logic', patterns: stringArray(discovery.screenPathPatterns) },
|
|
2436
|
+
{
|
|
2437
|
+
kind: 'component_mapping',
|
|
2438
|
+
patterns: stringArray(discovery.componentPathPatterns),
|
|
2439
|
+
},
|
|
2440
|
+
].filter((unit) => unit.patterns.length > 0);
|
|
2351
2441
|
}
|
|
2352
2442
|
function stringArray(value) {
|
|
2353
2443
|
if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string')) {
|
|
@@ -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
|
|
@@ -43,6 +58,8 @@ target inferred only from surrounding code. Carry both node ids into the discove
|
|
|
43
58
|
checkpoint as evidence. A defect in a calculation, a response shape or a service has no
|
|
44
59
|
answer in the design; do not spend the read there.
|
|
45
60
|
|
|
61
|
+
Name the areas this task works in, against the coverage map the core pack carries. Where an area it touches is marked as having no rule, that is not a reason to stop — it is a finding to raise at self review, so the absence is reported by the product rather than found later by whoever inherits the code.
|
|
62
|
+
|
|
46
63
|
Do not pull history for everything. Pull it for the records the task actually touches, and for anything the header shows a surprising number of corrections on. Record `task.checkpoint` with type `discovery` and update STATE, DECISIONS, DISCOVERY, and HANDOFF projections through the bridge.
|
|
47
64
|
|
|
48
65
|
Send calls that do not feed each other in one batch rather than one at a time: reads of any kind, and proposals for different records. Reconcile every record the task touched in a single `task.reconcile` call with `entries`, not one call per record.
|
|
@@ -91,7 +108,9 @@ Before validation, read the changed code back against the rules that govern it.
|
|
|
91
108
|
|
|
92
109
|
Record `task.self_review` naming the resources reviewed and, for every conflict found, the file, the rule, what was wrong and what was done about it. A review that found nothing records an empty finding list, which is a claim about the work rather than a formality.
|
|
93
110
|
|
|
94
|
-
|
|
111
|
+
An area the task worked in that the coverage map marks as having no rule is recorded as a finding too, naming the area and what was decided in its absence. That is how the product learns which rule to write next: an area nothing has needed yet can wait, and one a real task just had to improvise in cannot.
|
|
112
|
+
|
|
113
|
+
This applies to a write task. A read-only task changed nothing, so there is nothing to read back against the rules and `task.self_review` is refused for it; go straight to verification. `task.verify` refuses a write task without a self review of the current diff. Editing after the review invalidates it, which is the point: the last thing that happens to the code is that someone read it against the rules. This exists because a task once shipped code that broke rules it had been given — the rules were present and correct, and nothing in the lifecycle ever asked whether the result matched them.
|
|
95
114
|
|
|
96
115
|
The review is the agent's own job at the end of the work. Do not wait to be asked for it, and do not treat a passing test suite as a substitute: tests leave a receipt and readability does not, which is exactly why the unreviewed one is the one that degrades.
|
|
97
116
|
|
|
@@ -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
|
|