engineering-memory 1.9.0 → 1.10.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/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 +6 -3
- package/runtime/dist/src/index.js +1 -1
- package/runtime/dist/src/mcp/tool-definitions.js +152 -4
- package/runtime/dist/src/project/repository.js +69 -32
- package/runtime/dist/src/runtime/bridge-service.js +93 -18
- package/skill/SKILL.md +3 -3
- package/skill/references/lifecycle.md +14 -15
- 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.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",
|
|
@@ -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,19 @@
|
|
|
1
1
|
import { readdir } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { RepositoryResolver } from '../project/repository.js';
|
|
3
4
|
import { ensureManagedDirectory, readJson, removeFile, safeSegment, writeJson, } from '../utilities/files.js';
|
|
4
5
|
export class VerificationGate {
|
|
5
6
|
git;
|
|
6
7
|
outbox;
|
|
7
8
|
commitGateAttestor;
|
|
8
9
|
root;
|
|
10
|
+
repositories;
|
|
9
11
|
constructor(stateRoot, git, outbox, commitGateAttestor) {
|
|
10
12
|
this.git = git;
|
|
11
13
|
this.outbox = outbox;
|
|
12
14
|
this.commitGateAttestor = commitGateAttestor;
|
|
13
15
|
this.root = join(stateRoot, 'verification-gates');
|
|
16
|
+
this.repositories = new RepositoryResolver(git, 2, stateRoot);
|
|
14
17
|
}
|
|
15
18
|
async record(input) {
|
|
16
19
|
if (input.taskClosed &&
|
|
@@ -36,13 +39,13 @@ export class VerificationGate {
|
|
|
36
39
|
}, this.root);
|
|
37
40
|
}
|
|
38
41
|
async verify(repoRoot) {
|
|
39
|
-
const [
|
|
40
|
-
this.
|
|
41
|
-
this.git.manifest(repoRoot),
|
|
42
|
+
const [repository, stagedChanges, pending, scaffolding] = await Promise.all([
|
|
43
|
+
this.repositories.resolve(repoRoot),
|
|
42
44
|
this.git.stagedManifest(repoRoot),
|
|
43
45
|
this.outbox.list(),
|
|
44
46
|
this.git.temporaryScaffolding(repoRoot),
|
|
45
47
|
]);
|
|
48
|
+
const { repoFingerprint: fingerprint, git: manifest } = repository;
|
|
46
49
|
if (pending.length > 0) {
|
|
47
50
|
return { allowed: false, reason: 'offline_outbox_pending', diffHash: manifest.diffHash };
|
|
48
51
|
}
|
|
@@ -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,11 +467,20 @@ 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(),
|
|
@@ -518,6 +538,14 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
518
538
|
.min(1),
|
|
519
539
|
}),
|
|
520
540
|
}, async (input) => toolResult(await service.architectureRecordApplication(input)));
|
|
541
|
+
server.registerTool('organization.update', {
|
|
542
|
+
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.',
|
|
543
|
+
inputSchema: z.object({
|
|
544
|
+
organizationId: z.string().uuid(),
|
|
545
|
+
name: z.string().trim().min(2).max(120),
|
|
546
|
+
expectedName: z.string().min(2).max(120),
|
|
547
|
+
}),
|
|
548
|
+
}, async (input) => toolResult(await service.organizationUpdate(input)));
|
|
521
549
|
server.registerTool('organization.list', {
|
|
522
550
|
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
551
|
inputSchema: z.object({}),
|
|
@@ -527,7 +555,7 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
527
555
|
inputSchema: z.object({ includeArchived: z.boolean().optional() }),
|
|
528
556
|
}, async (input) => toolResult(await service.projectList(input)));
|
|
529
557
|
server.registerTool('project.resolve', {
|
|
530
|
-
description: 'Resolve the current repository fingerprint or bind an explicitly selected project, then
|
|
558
|
+
description: 'Resolve the current repository fingerprint or bind an explicitly selected project, then store the user-local binding only after backend authorization succeeds.',
|
|
531
559
|
inputSchema: z.object({
|
|
532
560
|
repoRoot: optionalRepoRoot,
|
|
533
561
|
bind: z.boolean().optional(),
|
|
@@ -554,13 +582,133 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
554
582
|
projectId: z.string().min(1),
|
|
555
583
|
email: z.string().email(),
|
|
556
584
|
role: z.enum(['owner', 'maintainer', 'member', 'reader']),
|
|
557
|
-
discipline: z
|
|
585
|
+
discipline: z
|
|
586
|
+
.enum([
|
|
587
|
+
'backend',
|
|
588
|
+
'frontend',
|
|
589
|
+
'fullstack',
|
|
590
|
+
'web',
|
|
591
|
+
'mobile',
|
|
592
|
+
'designer',
|
|
593
|
+
'product_manager',
|
|
594
|
+
'tester',
|
|
595
|
+
])
|
|
596
|
+
.nullable()
|
|
597
|
+
.optional(),
|
|
558
598
|
}),
|
|
559
599
|
}, async (input) => toolResult(await service.projectMemberAdd(input)));
|
|
560
600
|
server.registerTool('project.member_list', {
|
|
561
601
|
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
602
|
inputSchema: z.object({ projectId: z.string().uuid() }),
|
|
563
603
|
}, async (input) => toolResult(await service.projectMemberList(input)));
|
|
604
|
+
const workItemLocator = { projectId: z.string().uuid(), workItemId: z.string().uuid() };
|
|
605
|
+
const workItemFields = {
|
|
606
|
+
title: z.string().trim().min(2).max(240).optional(),
|
|
607
|
+
description: z.string().max(20000).nullable().optional(),
|
|
608
|
+
priority: z.enum(['lowest', 'low', 'medium', 'high', 'highest']).optional(),
|
|
609
|
+
assigneeUserId: z.string().uuid().nullable().optional(),
|
|
610
|
+
acceptanceCriteria: z.array(z.string().trim().min(2).max(500)).max(50).optional(),
|
|
611
|
+
figmaUrl: z.string().url().nullable().optional(),
|
|
612
|
+
dueAt: z.string().datetime().nullable().optional(),
|
|
613
|
+
position: z.number().int().min(0).optional(),
|
|
614
|
+
dependencyIds: z.array(z.string().uuid()).max(50).optional(),
|
|
615
|
+
};
|
|
616
|
+
server.registerTool('work_item.create', {
|
|
617
|
+
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.',
|
|
618
|
+
inputSchema: z.object({
|
|
619
|
+
projectId: z.string().uuid(),
|
|
620
|
+
data: z.object({
|
|
621
|
+
...workItemFields,
|
|
622
|
+
title: z.string().trim().min(2).max(240),
|
|
623
|
+
kind: z.enum(['task', 'bug']).optional(),
|
|
624
|
+
}),
|
|
625
|
+
}),
|
|
626
|
+
}, async (input) => toolResult(await service.workItemCreate(input)));
|
|
627
|
+
server.registerTool('work_item.update', {
|
|
628
|
+
description: 'Edit or assign a work item at its current version. Assignees must already be active project members; this does not grant project access.',
|
|
629
|
+
inputSchema: z.object({
|
|
630
|
+
...workItemLocator,
|
|
631
|
+
data: z.object({
|
|
632
|
+
...workItemFields,
|
|
633
|
+
expectedVersion: z.number().int().min(1),
|
|
634
|
+
status: z
|
|
635
|
+
.enum(['backlog', 'ready', 'in_progress', 'in_review', 'done', 'cancelled'])
|
|
636
|
+
.optional(),
|
|
637
|
+
}),
|
|
638
|
+
}),
|
|
639
|
+
}, async (input) => toolResult(await service.workItemUpdate(input)));
|
|
640
|
+
server.registerTool('work_item.archive', {
|
|
641
|
+
description: 'Reversibly remove a work item from the active backlog at its current version, preserving history.',
|
|
642
|
+
inputSchema: z.object({ ...workItemLocator, expectedVersion: z.number().int().min(1) }),
|
|
643
|
+
}, async (input) => toolResult(await service.workItemArchive(input)));
|
|
644
|
+
server.registerTool('work_item.restore', {
|
|
645
|
+
description: 'Restore an archived work item at its current version.',
|
|
646
|
+
inputSchema: z.object({ ...workItemLocator, expectedVersion: z.number().int().min(1) }),
|
|
647
|
+
}, async (input) => toolResult(await service.workItemRestore(input)));
|
|
648
|
+
server.registerTool('work_item.runs', {
|
|
649
|
+
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.',
|
|
650
|
+
inputSchema: z.object({
|
|
651
|
+
...workItemLocator,
|
|
652
|
+
offset: z.number().int().min(0).optional(),
|
|
653
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
654
|
+
}),
|
|
655
|
+
}, async (input) => toolResult(await service.workItemRuns(input)));
|
|
656
|
+
server.registerTool('work_item.record_test', {
|
|
657
|
+
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.',
|
|
658
|
+
inputSchema: z.object({
|
|
659
|
+
...workItemLocator,
|
|
660
|
+
data: z.object({
|
|
661
|
+
expectedVersion: z.number().int().min(1),
|
|
662
|
+
idempotencyKey: z.string().uuid(),
|
|
663
|
+
outcome: z.enum(['passed', 'failed', 'blocked']),
|
|
664
|
+
summary: z.string().trim().min(2).max(4000),
|
|
665
|
+
testedRevision: z.string().min(1).max(160).optional(),
|
|
666
|
+
}),
|
|
667
|
+
}),
|
|
668
|
+
}, async (input) => toolResult(await service.workItemRecordTest(input)));
|
|
669
|
+
server.registerTool('work_item.test_results', {
|
|
670
|
+
description: 'Read the versioned QA evidence for an authorized work item. Older evidence remains attached to the requirement version it tested.',
|
|
671
|
+
inputSchema: z.object({
|
|
672
|
+
...workItemLocator,
|
|
673
|
+
offset: z.number().int().min(0).optional(),
|
|
674
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
675
|
+
}),
|
|
676
|
+
}, async (input) => toolResult(await service.workItemTestResults(input)));
|
|
677
|
+
server.registerTool('work_item.plan', {
|
|
678
|
+
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.',
|
|
679
|
+
inputSchema: z.object(workItemLocator),
|
|
680
|
+
}, async (input) => toolResult(await service.workItemPlan(input)));
|
|
681
|
+
server.registerTool('work_item.confirm_plan', {
|
|
682
|
+
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.',
|
|
683
|
+
inputSchema: z.object({
|
|
684
|
+
...workItemLocator,
|
|
685
|
+
data: z.object({
|
|
686
|
+
expectedVersion: z.number().int().min(1),
|
|
687
|
+
projectIds: z.array(z.string().uuid()).min(1).max(12),
|
|
688
|
+
branch: z
|
|
689
|
+
.string()
|
|
690
|
+
.regex(/^(?=.{1,255}$)[a-zA-Z0-9][a-zA-Z0-9_-]*(?:\/[a-zA-Z0-9][a-zA-Z0-9_-]*)*$/),
|
|
691
|
+
reason: z.string().trim().min(2).max(2000),
|
|
692
|
+
userConfirmed: z.literal(true),
|
|
693
|
+
}),
|
|
694
|
+
}),
|
|
695
|
+
}, async (input) => toolResult(await service.workItemConfirmPlan(input)));
|
|
696
|
+
server.registerTool('project.update', {
|
|
697
|
+
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.',
|
|
698
|
+
inputSchema: z.object({
|
|
699
|
+
projectId: z.string().uuid(),
|
|
700
|
+
data: z.object({
|
|
701
|
+
expectedVersion: z.number().int().min(1),
|
|
702
|
+
name: z.string().min(2).max(160).optional(),
|
|
703
|
+
framework: z.string().min(2).max(80).nullable().optional(),
|
|
704
|
+
repositoryUrl: z.string().url().nullable().optional(),
|
|
705
|
+
workItemPrefix: z
|
|
706
|
+
.string()
|
|
707
|
+
.regex(/^[A-Z][A-Z0-9]{1,11}$/)
|
|
708
|
+
.optional(),
|
|
709
|
+
}),
|
|
710
|
+
}),
|
|
711
|
+
}, async (input) => toolResult(await service.projectUpdate(input)));
|
|
564
712
|
server.registerTool('work_item.list', {
|
|
565
713
|
description: 'List selectable work items for a project before opening an engineering run in this chat.',
|
|
566
714
|
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'
|
|
@@ -1248,10 +1247,8 @@ export class BridgeService {
|
|
|
1248
1247
|
target = await this.dependencies.repositories.resolve(targetRoot);
|
|
1249
1248
|
if (target.projectId && target.projectId !== repository.projectId)
|
|
1250
1249
|
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
|
-
}
|
|
1250
|
+
if (!target.bindingPath || target.repoFingerprint !== repository.repoFingerprint)
|
|
1251
|
+
throw refuse('The worktree did not inherit the selected local project binding.', 'project.resolve');
|
|
1255
1252
|
}
|
|
1256
1253
|
const branch = input.name ?? (await git.currentBranch(target.repoRoot));
|
|
1257
1254
|
const store = git.branchStore(target.repoRoot);
|
|
@@ -1391,8 +1388,8 @@ export class BridgeService {
|
|
|
1391
1388
|
readDiscoveryUnits(policy).length === 0) {
|
|
1392
1389
|
throw refuse('Project setup response is not policy-ready', 'project.setup');
|
|
1393
1390
|
}
|
|
1394
|
-
const
|
|
1395
|
-
return asJsonValue({ ...data, markerPath });
|
|
1391
|
+
const bindingPath = await this.dependencies.repositories.writeBinding(repository.repoRoot, project.id);
|
|
1392
|
+
return asJsonValue({ ...data, marker: null, markerPath: null, bindingPath });
|
|
1396
1393
|
});
|
|
1397
1394
|
}
|
|
1398
1395
|
async projectMemberAdd(input) {
|
|
@@ -1493,6 +1490,77 @@ export class BridgeService {
|
|
|
1493
1490
|
return asJsonValue({ workItem: response.data });
|
|
1494
1491
|
});
|
|
1495
1492
|
}
|
|
1493
|
+
async workItemCreate(input) {
|
|
1494
|
+
return this.execute(async () => {
|
|
1495
|
+
const response = await this.dependencies.client.request(endpoints.workItemList(input.projectId), { method: 'POST', body: cleanJson(input.data) });
|
|
1496
|
+
return asJsonValue(response.data);
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
async workItemUpdate(input) {
|
|
1500
|
+
return this.execute(async () => {
|
|
1501
|
+
const response = await this.dependencies.client.request(endpoints.workItemGet(input.projectId, input.workItemId), { method: 'PATCH', body: cleanJson(input.data) });
|
|
1502
|
+
return asJsonValue(response.data);
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
async workItemArchive(input) {
|
|
1506
|
+
return this.execute(async () => {
|
|
1507
|
+
const response = await this.dependencies.client.request(endpoints.workItemGet(input.projectId, input.workItemId), { method: 'DELETE', body: cleanJson({ expectedVersion: input.expectedVersion }) });
|
|
1508
|
+
return asJsonValue(response.data);
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
async workItemRestore(input) {
|
|
1512
|
+
return this.execute(async () => {
|
|
1513
|
+
const response = await this.dependencies.client.request(endpoints.workItemRestore(input.projectId, input.workItemId), { method: 'POST', body: cleanJson({ expectedVersion: input.expectedVersion }) });
|
|
1514
|
+
return asJsonValue(response.data);
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
async workItemRecordTest(input) {
|
|
1518
|
+
return this.execute(async () => {
|
|
1519
|
+
const response = await this.dependencies.client.request(endpoints.workItemTests(input.projectId, input.workItemId), { method: 'POST', body: cleanJson(input.data) });
|
|
1520
|
+
return asJsonValue(response.data);
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
async workItemConfirmPlan(input) {
|
|
1524
|
+
return this.execute(async () => {
|
|
1525
|
+
const response = await this.dependencies.client.request(endpoints.workItemPlan(input.projectId, input.workItemId), { method: 'PUT', body: cleanJson(input.data) });
|
|
1526
|
+
return asJsonValue(response.data);
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
async projectUpdate(input) {
|
|
1530
|
+
return this.execute(async () => {
|
|
1531
|
+
const response = await this.dependencies.client.request(endpoints.projectUpdate(input.projectId), { method: 'PATCH', body: cleanJson(input.data) });
|
|
1532
|
+
return asJsonValue(response.data);
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
async workItemRuns(input) {
|
|
1536
|
+
return this.execute(async () => {
|
|
1537
|
+
const query = new URLSearchParams({
|
|
1538
|
+
offset: String(input.offset ?? 0),
|
|
1539
|
+
limit: String(input.limit ?? 50),
|
|
1540
|
+
});
|
|
1541
|
+
const response = await this.dependencies.client.request(`${endpoints.workItemRuns(input.projectId, input.workItemId)}?${query}`);
|
|
1542
|
+
return asJsonValue(response.data);
|
|
1543
|
+
});
|
|
1544
|
+
}
|
|
1545
|
+
async workItemTestResults(input) {
|
|
1546
|
+
return this.execute(async () => {
|
|
1547
|
+
const query = new URLSearchParams({
|
|
1548
|
+
offset: String(input.offset ?? 0),
|
|
1549
|
+
limit: String(input.limit ?? 50),
|
|
1550
|
+
});
|
|
1551
|
+
const response = await this.dependencies.client.request(`${endpoints.workItemTests(input.projectId, input.workItemId)}?${query}`);
|
|
1552
|
+
return asJsonValue(response.data);
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
async workItemPlan(input) {
|
|
1556
|
+
return this.execute(async () => {
|
|
1557
|
+
const response = await this.dependencies.client.request(endpoints.workItemPlan(input.projectId, input.workItemId));
|
|
1558
|
+
return asJsonValue({
|
|
1559
|
+
...objectOrEmpty(response.data),
|
|
1560
|
+
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.',
|
|
1561
|
+
});
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1496
1564
|
async architecturePlan(input) {
|
|
1497
1565
|
return await this.execute(async () => {
|
|
1498
1566
|
const body = cleanJson(input);
|
|
@@ -1541,6 +1609,15 @@ export class BridgeService {
|
|
|
1541
1609
|
return asJsonValue({ application: response.data });
|
|
1542
1610
|
});
|
|
1543
1611
|
}
|
|
1612
|
+
async organizationUpdate(input) {
|
|
1613
|
+
return this.execute(async () => {
|
|
1614
|
+
const response = await this.dependencies.client.request(endpoints.organizationUpdate(input.organizationId), {
|
|
1615
|
+
method: 'PATCH',
|
|
1616
|
+
body: cleanJson({ name: input.name, expectedName: input.expectedName }),
|
|
1617
|
+
});
|
|
1618
|
+
return asJsonValue(response.data);
|
|
1619
|
+
});
|
|
1620
|
+
}
|
|
1544
1621
|
async organizationCreate(input) {
|
|
1545
1622
|
return await this.execute(async () => {
|
|
1546
1623
|
const response = await this.dependencies.client.request(endpoints.organizationCreate, { method: 'POST', body: cleanJson(input) });
|
|
@@ -1614,17 +1691,14 @@ export class BridgeService {
|
|
|
1614
1691
|
if (!project || project.id !== input.projectId) {
|
|
1615
1692
|
throw refuse('Project bind response does not match the selected project', 'project.resolve');
|
|
1616
1693
|
}
|
|
1617
|
-
const
|
|
1694
|
+
const bindingPath = await this.dependencies.repositories.writeBinding(repository.repoRoot, input.projectId);
|
|
1618
1695
|
return asJsonValue({
|
|
1619
1696
|
resolved: true,
|
|
1620
1697
|
bound: true,
|
|
1621
1698
|
project,
|
|
1622
|
-
markerPath,
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
markerPath,
|
|
1626
|
-
projectId: input.projectId,
|
|
1627
|
-
}),
|
|
1699
|
+
markerPath: null,
|
|
1700
|
+
bindingPath,
|
|
1701
|
+
repository: publicRepository(await this.dependencies.repositories.resolve(repository.repoRoot)),
|
|
1628
1702
|
selectionRequired: false,
|
|
1629
1703
|
});
|
|
1630
1704
|
}
|
|
@@ -1669,11 +1743,11 @@ export class BridgeService {
|
|
|
1669
1743
|
}
|
|
1670
1744
|
const decision = await this.dependencies.repositoryDecisions.load(repository.repoFingerprint);
|
|
1671
1745
|
const authenticated = authentication.authenticated === true;
|
|
1672
|
-
const state = decision?.state
|
|
1746
|
+
const state = decision?.state === 'disabled' ? 'disabled' : repository.bindingPath ? 'bound' : 'none';
|
|
1673
1747
|
const client = await this.clientUpdate(authenticated);
|
|
1674
1748
|
const shipped = objectValue(objectValue(client)?.shippedKnowledge);
|
|
1675
1749
|
const incomplete = shipped && (shipped.failure !== null || Number(shipped.live) < Number(shipped.expected));
|
|
1676
|
-
const projectId = repository.projectId
|
|
1750
|
+
const projectId = repository.projectId;
|
|
1677
1751
|
const liveTasks = authenticated && state === 'bound'
|
|
1678
1752
|
? (await this.dependencies.activeContexts.list(repository.repoFingerprint)).map(describePointer)
|
|
1679
1753
|
: [];
|
|
@@ -2543,6 +2617,7 @@ function publicRepository(repository) {
|
|
|
2543
2617
|
return asJsonValue({
|
|
2544
2618
|
repoRoot: repository.repoRoot,
|
|
2545
2619
|
markerPath: repository.markerPath,
|
|
2620
|
+
bindingPath: repository.bindingPath,
|
|
2546
2621
|
projectId: repository.projectId,
|
|
2547
2622
|
schemaVersion: repository.schemaVersion,
|
|
2548
2623
|
repoFingerprint: repository.repoFingerprint,
|
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
|
|
|
@@ -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
|
|