engineering-memory 1.10.2 → 1.10.4
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/dispatcher/sections.mjs +4 -2
- package/package.json +1 -1
- package/runtime/dist/src/git/source-snapshot.js +450 -0
- package/runtime/dist/src/index.js +4 -0
- package/runtime/dist/src/mcp/onboarding-tools.js +663 -0
- package/runtime/dist/src/mcp/questionnaire-tools.js +149 -0
- package/runtime/dist/src/mcp/server.js +4 -1
- package/runtime/dist/src/mcp/tool-definitions.js +71 -0
- package/runtime/dist/src/project/project-intake.js +406 -0
- package/runtime/dist/src/runtime/api-client.js +3 -0
- package/runtime/dist/src/runtime/bridge-service.js +393 -8
- package/runtime/dist/src/runtime/onboarding-store.js +47 -0
- package/runtime/dist/src/runtime/questionnaire-store.js +259 -0
- package/runtime/dist/src/utilities/files.js +8 -1
- package/skill/SKILL.md +2 -2
- package/skill/references/lifecycle.md +47 -20
- package/skill/references/project-onboarding.md +146 -0
- package/skill/references/questionnaires.md +85 -7
- package/skill/references/scaffolding.md +13 -4
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { link, readdir } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import * as z from 'zod/v4';
|
|
5
|
+
import { assertManagedPath, ensureManagedDirectory, isNodeError, readJson, removeFile, writeJson, } from '../utilities/files.js';
|
|
6
|
+
import { sha256, stableStringify } from '../utilities/hash.js';
|
|
7
|
+
import { assertSafeToPersist } from './offline-outbox.js';
|
|
8
|
+
const identifier = z.string().regex(/^[A-Za-z0-9_-]{1,100}$/);
|
|
9
|
+
export const questionnaireDefinitionSchema = z.strictObject({
|
|
10
|
+
questionnaireId: identifier,
|
|
11
|
+
message: z.string().trim().min(1).max(2000),
|
|
12
|
+
options: z
|
|
13
|
+
.array(z.strictObject({
|
|
14
|
+
id: z
|
|
15
|
+
.string()
|
|
16
|
+
.regex(/^[A-Za-z0-9_-]{1,80}$/)
|
|
17
|
+
.refine((value) => value !== '__other__'),
|
|
18
|
+
label: z.string().trim().min(1).max(240),
|
|
19
|
+
}))
|
|
20
|
+
.min(2)
|
|
21
|
+
.max(12)
|
|
22
|
+
.refine((values) => new Set(values.map((value) => value.id)).size === values.length),
|
|
23
|
+
allowFreeText: z.boolean().default(false),
|
|
24
|
+
textField: z
|
|
25
|
+
.strictObject({
|
|
26
|
+
title: z.string().trim().min(1).max(120),
|
|
27
|
+
maxLength: z.number().int().min(1).max(2000),
|
|
28
|
+
requiredForChoice: z
|
|
29
|
+
.string()
|
|
30
|
+
.regex(/^[A-Za-z0-9_-]{1,80}$/)
|
|
31
|
+
.optional(),
|
|
32
|
+
valueType: z.enum(['text', 'uuid_list']).optional(),
|
|
33
|
+
})
|
|
34
|
+
.optional(),
|
|
35
|
+
});
|
|
36
|
+
const scopeSchema = z.strictObject({
|
|
37
|
+
principalHash: z.string().regex(/^[0-9a-f]{64}$/),
|
|
38
|
+
repoFingerprint: z.string().regex(/^[0-9a-f]{64}$/),
|
|
39
|
+
projectId: z.string().uuid().nullable(),
|
|
40
|
+
});
|
|
41
|
+
const storedQuestionSchema = questionnaireDefinitionSchema.extend({
|
|
42
|
+
schemaVersion: z.literal(1),
|
|
43
|
+
scope: scopeSchema,
|
|
44
|
+
contentHash: z.string().regex(/^[0-9a-f]{64}$/),
|
|
45
|
+
requestKey: z.string().regex(/^questionnaire_[0-9a-f]{64}$/),
|
|
46
|
+
createdAt: z.string().datetime(),
|
|
47
|
+
});
|
|
48
|
+
const storedAnswerSchema = z.strictObject({
|
|
49
|
+
requestKey: z.string().regex(/^questionnaire_[0-9a-f]{64}$/),
|
|
50
|
+
answeredAt: z.string().datetime(),
|
|
51
|
+
answerAvailable: z.boolean(),
|
|
52
|
+
withdrawn: z.literal(true).optional(),
|
|
53
|
+
answer: z.strictObject({ choice: z.string(), text: z.string().optional() }).optional(),
|
|
54
|
+
});
|
|
55
|
+
export function questionnaireAnswerSchema(record) {
|
|
56
|
+
const textSchema = record.textField
|
|
57
|
+
? record.textField.valueType === 'uuid_list'
|
|
58
|
+
? z
|
|
59
|
+
.string()
|
|
60
|
+
.trim()
|
|
61
|
+
.min(2)
|
|
62
|
+
.max(record.textField.maxLength)
|
|
63
|
+
.refine((value) => {
|
|
64
|
+
try {
|
|
65
|
+
const parsed = JSON.parse(value);
|
|
66
|
+
if (!Array.isArray(parsed) || new Set(parsed).size !== parsed.length)
|
|
67
|
+
return false;
|
|
68
|
+
return z.array(z.string().uuid()).max(10).safeParse(parsed).success;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}, 'Enter a JSON array containing valid project IDs.')
|
|
74
|
+
.optional()
|
|
75
|
+
: z.string().trim().min(1).max(record.textField.maxLength).optional()
|
|
76
|
+
: record.allowFreeText
|
|
77
|
+
? z.string().trim().min(1).max(2000).optional()
|
|
78
|
+
: z.never().optional();
|
|
79
|
+
return z
|
|
80
|
+
.strictObject({
|
|
81
|
+
choice: z.enum([
|
|
82
|
+
...record.options.map((option) => option.id),
|
|
83
|
+
...(record.allowFreeText ? ['__other__'] : []),
|
|
84
|
+
]),
|
|
85
|
+
text: z.preprocess((value) => (typeof value === 'string' && value.trim() === '' ? undefined : value), textSchema),
|
|
86
|
+
})
|
|
87
|
+
.refine((answer) => {
|
|
88
|
+
if (record.textField?.requiredForChoice === answer.choice)
|
|
89
|
+
return typeof answer.text === 'string';
|
|
90
|
+
if (answer.choice === '__other__')
|
|
91
|
+
return typeof answer.text === 'string';
|
|
92
|
+
return answer.text === undefined;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
export class QuestionnaireStore {
|
|
96
|
+
root;
|
|
97
|
+
constructor(stateRoot) {
|
|
98
|
+
this.root = join(stateRoot, 'questionnaires');
|
|
99
|
+
}
|
|
100
|
+
async ask(scope, input) {
|
|
101
|
+
const definition = questionnaireDefinitionSchema.parse(input);
|
|
102
|
+
scopeSchema.parse(scope);
|
|
103
|
+
assertSafeToPersist(definition);
|
|
104
|
+
const contentHash = sha256(stableStringify(definition));
|
|
105
|
+
const question = {
|
|
106
|
+
...definition,
|
|
107
|
+
schemaVersion: 1,
|
|
108
|
+
scope,
|
|
109
|
+
contentHash,
|
|
110
|
+
requestKey: `questionnaire_${sha256(`${stableStringify(scope)}\n${contentHash}\n${randomUUID()}`)}`,
|
|
111
|
+
createdAt: new Date().toISOString(),
|
|
112
|
+
};
|
|
113
|
+
await this.publish(this.pathFor(scope, definition.questionnaireId, 'pending.json'), question);
|
|
114
|
+
const record = await this.get(scope, definition.questionnaireId);
|
|
115
|
+
if (!record || record.contentHash !== contentHash) {
|
|
116
|
+
throw new Error('Questionnaire id was reused with different content. Resume the original question or use a new id.');
|
|
117
|
+
}
|
|
118
|
+
return record;
|
|
119
|
+
}
|
|
120
|
+
async get(scope, questionnaireId) {
|
|
121
|
+
const raw = await readJson(this.pathFor(scope, questionnaireId, 'pending.json'), this.root);
|
|
122
|
+
if (!raw)
|
|
123
|
+
return null;
|
|
124
|
+
const question = storedQuestionSchema.parse(raw);
|
|
125
|
+
const definition = questionnaireDefinitionSchema.parse({
|
|
126
|
+
questionnaireId: question.questionnaireId,
|
|
127
|
+
message: question.message,
|
|
128
|
+
options: question.options,
|
|
129
|
+
allowFreeText: question.allowFreeText,
|
|
130
|
+
textField: question.textField,
|
|
131
|
+
});
|
|
132
|
+
if (stableStringify(question.scope) !== stableStringify(scope) ||
|
|
133
|
+
question.questionnaireId !== questionnaireId ||
|
|
134
|
+
question.contentHash !== sha256(stableStringify(definition))) {
|
|
135
|
+
throw new Error('Questionnaire scope or content does not match its durable record.');
|
|
136
|
+
}
|
|
137
|
+
assertSafeToPersist(question);
|
|
138
|
+
const rawAnswer = await readJson(this.pathFor(scope, questionnaireId, 'answer.json'), this.root);
|
|
139
|
+
if (!rawAnswer)
|
|
140
|
+
return { ...question, status: 'pending' };
|
|
141
|
+
const answer = storedAnswerSchema.parse(rawAnswer);
|
|
142
|
+
if (answer.withdrawn) {
|
|
143
|
+
if (answer.requestKey !== question.requestKey || answer.answerAvailable || answer.answer)
|
|
144
|
+
throw new Error('Invalid questionnaire withdrawal receipt.');
|
|
145
|
+
return { ...question, ...answer, status: 'withdrawn' };
|
|
146
|
+
}
|
|
147
|
+
if (answer.requestKey !== question.requestKey ||
|
|
148
|
+
(answer.answerAvailable
|
|
149
|
+
? !answer.answer || !question.options.some((option) => option.id === answer.answer.choice)
|
|
150
|
+
: !question.allowFreeText || answer.answer !== undefined)) {
|
|
151
|
+
throw new Error('Questionnaire answer does not match its durable question.');
|
|
152
|
+
}
|
|
153
|
+
if (answer.answer?.text !== undefined && !question.textField)
|
|
154
|
+
throw new Error('Questionnaire text answer does not match its durable question.');
|
|
155
|
+
if (question.textField?.requiredForChoice &&
|
|
156
|
+
question.textField.requiredForChoice === answer.answer?.choice &&
|
|
157
|
+
typeof answer.answer?.text !== 'string')
|
|
158
|
+
throw new Error('Questionnaire text answer is required for this choice.');
|
|
159
|
+
assertSafeToPersist(answer);
|
|
160
|
+
return { ...question, ...answer, status: 'answered' };
|
|
161
|
+
}
|
|
162
|
+
async pending(scope) {
|
|
163
|
+
const directory = this.scopeRoot(scope);
|
|
164
|
+
await ensureManagedDirectory(this.root, directory);
|
|
165
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
166
|
+
const records = [];
|
|
167
|
+
for (const entry of entries) {
|
|
168
|
+
if (!/^[0-9a-f]{64}$/.test(entry.name))
|
|
169
|
+
continue;
|
|
170
|
+
if (!entry.isDirectory() || entry.isSymbolicLink())
|
|
171
|
+
throw new Error('Unsafe questionnaire directory.');
|
|
172
|
+
const path = await assertManagedPath(this.root, join(directory, entry.name, 'pending.json'), true);
|
|
173
|
+
const raw = await readJson(path, this.root);
|
|
174
|
+
if (!raw)
|
|
175
|
+
continue;
|
|
176
|
+
const question = storedQuestionSchema.parse(raw);
|
|
177
|
+
if (sha256(question.questionnaireId) !== entry.name)
|
|
178
|
+
throw new Error('Questionnaire directory does not match its id.');
|
|
179
|
+
const record = await this.get(scope, question.questionnaireId);
|
|
180
|
+
if (record?.status === 'pending')
|
|
181
|
+
records.push(record);
|
|
182
|
+
}
|
|
183
|
+
return records.sort((left, right) => left.createdAt.localeCompare(right.createdAt));
|
|
184
|
+
}
|
|
185
|
+
async accept(scope, questionnaireId, requestKey, input) {
|
|
186
|
+
const record = await this.get(scope, questionnaireId);
|
|
187
|
+
if (!record || record.requestKey !== requestKey)
|
|
188
|
+
throw new Error('Questionnaire response is stale or belongs to a different scope.');
|
|
189
|
+
if (record.status === 'withdrawn')
|
|
190
|
+
throw new Error('Questionnaire was explicitly withdrawn. Start a new decision if the user resumes the work.');
|
|
191
|
+
const answer = questionnaireAnswerSchema(record).parse(input);
|
|
192
|
+
assertSafeToPersist(answer);
|
|
193
|
+
if (record.status === 'answered') {
|
|
194
|
+
if (record.answer && stableStringify(record.answer) !== stableStringify(answer))
|
|
195
|
+
throw new Error('Questionnaire was already answered differently.');
|
|
196
|
+
return { record, answer: record.answer, replayed: true };
|
|
197
|
+
}
|
|
198
|
+
const receipt = {
|
|
199
|
+
requestKey,
|
|
200
|
+
answeredAt: new Date().toISOString(),
|
|
201
|
+
answerAvailable: answer.choice !== '__other__',
|
|
202
|
+
...(answer.choice !== '__other__'
|
|
203
|
+
? { answer: { choice: answer.choice, ...(answer.text ? { text: answer.text } : {}) } }
|
|
204
|
+
: {}),
|
|
205
|
+
};
|
|
206
|
+
const published = await this.publish(this.pathFor(scope, questionnaireId, 'answer.json'), receipt);
|
|
207
|
+
const resolved = (await this.get(scope, questionnaireId));
|
|
208
|
+
if (resolved.status === 'withdrawn')
|
|
209
|
+
throw new Error('Questionnaire was explicitly withdrawn before this response.');
|
|
210
|
+
if (!published &&
|
|
211
|
+
resolved.answer &&
|
|
212
|
+
stableStringify(resolved.answer) !== stableStringify(answer))
|
|
213
|
+
throw new Error('Questionnaire was already answered differently.');
|
|
214
|
+
return { record: resolved, answer: published ? answer : resolved.answer, replayed: !published };
|
|
215
|
+
}
|
|
216
|
+
async withdraw(scope, questionnaireId) {
|
|
217
|
+
const record = await this.get(scope, questionnaireId);
|
|
218
|
+
if (!record)
|
|
219
|
+
throw new Error('No questionnaire exists in this scope.');
|
|
220
|
+
if (record.status !== 'pending')
|
|
221
|
+
return record;
|
|
222
|
+
await this.publish(this.pathFor(scope, questionnaireId, 'answer.json'), {
|
|
223
|
+
requestKey: record.requestKey,
|
|
224
|
+
answeredAt: new Date().toISOString(),
|
|
225
|
+
answerAvailable: false,
|
|
226
|
+
withdrawn: true,
|
|
227
|
+
});
|
|
228
|
+
return (await this.get(scope, questionnaireId));
|
|
229
|
+
}
|
|
230
|
+
scopeRoot(scope) {
|
|
231
|
+
scopeSchema.parse(scope);
|
|
232
|
+
return join(this.root, scope.principalHash, sha256(`${scope.repoFingerprint}\n${scope.projectId ?? ''}`));
|
|
233
|
+
}
|
|
234
|
+
pathFor(scope, questionnaireId, filename) {
|
|
235
|
+
identifier.parse(questionnaireId);
|
|
236
|
+
return join(this.scopeRoot(scope), sha256(questionnaireId), filename);
|
|
237
|
+
}
|
|
238
|
+
async publish(path, value) {
|
|
239
|
+
assertSafeToPersist(value);
|
|
240
|
+
const temporaryPath = `${path}.${randomUUID()}.tmp`;
|
|
241
|
+
await writeJson(temporaryPath, value, this.root);
|
|
242
|
+
try {
|
|
243
|
+
const target = await assertManagedPath(this.root, path, true);
|
|
244
|
+
try {
|
|
245
|
+
await link(temporaryPath, target);
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
if (isNodeError(error) && error.code === 'EEXIST')
|
|
250
|
+
return false;
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
finally {
|
|
255
|
+
await removeFile(temporaryPath, this.root);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
//# sourceMappingURL=questionnaire-store.js.map
|
|
@@ -136,7 +136,14 @@ export async function ensureManagedDirectory(root, directory) {
|
|
|
136
136
|
if (!isNodeError(error) || error.code !== 'ENOENT') {
|
|
137
137
|
throw error;
|
|
138
138
|
}
|
|
139
|
-
|
|
139
|
+
try {
|
|
140
|
+
await mkdir(current, { mode: 0o700 });
|
|
141
|
+
}
|
|
142
|
+
catch (creationError) {
|
|
143
|
+
if (!isNodeError(creationError) || creationError.code !== 'EEXIST') {
|
|
144
|
+
throw creationError;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
140
147
|
const created = await lstat(current);
|
|
141
148
|
if (!created.isDirectory() || created.isSymbolicLink()) {
|
|
142
149
|
throw new Error(`Managed directory could not be created safely: ${current}`);
|
package/skill/SKILL.md
CHANGED
|
@@ -7,11 +7,11 @@ description: Enforces the Engineering Memory lifecycle for Codex and Claude in r
|
|
|
7
7
|
|
|
8
8
|
This skill is a thin runtime protocol. It contains no proprietary engineering rules. The task-specific rules, project profile, screen logic, component mappings, service contracts, decisions, and quality gates must come from the Engineering Memory MCP server.
|
|
9
9
|
|
|
10
|
-
Read [lifecycle.md](references/lifecycle.md) before acting in a bound repository. Read [questionnaires.md](references/questionnaires.md) when authentication, project binding, project creation, membership, correction scope, proposal review, or Git-hook choices require user input. Read [memory-updates.md](references/memory-updates.md) before changing screen, component, service, project, or organization memory. Read [scaffolding.md](references/scaffolding.md) before creating a project from the organization architecture templates or changing a template.
|
|
10
|
+
Read [lifecycle.md](references/lifecycle.md) before acting in a bound repository. Read [project-onboarding.md](references/project-onboarding.md) for greenfield, adopt, rework and refresh flows. Read [questionnaires.md](references/questionnaires.md) when authentication, project binding, project creation, membership, correction scope, proposal review, or Git-hook choices require user input. Read [memory-updates.md](references/memory-updates.md) before changing screen, component, service, project, or organization memory. Read [scaffolding.md](references/scaffolding.md) before creating a project from the organization architecture templates or changing a template.
|
|
11
11
|
|
|
12
12
|
Mandatory behavior:
|
|
13
13
|
|
|
14
|
-
Use the host's native questionnaire for every question to the user, including implementation choices, names, clarification, branch/worktree decisions and delivery.
|
|
14
|
+
Use the host's native questionnaire for every question to the user, including implementation choices, names, clarification, branch/worktree decisions and delivery. For a required decision, read [questionnaires.md](references/questionnaires.md), then use `questionnaire.ask` to open a durable native MCP form and `questionnaire.resume` to return to the same unanswered question. Never use request_user_input_async for a required decision: it does not wait for an answer. When the durable form is unavailable, use a blocking native control only where the host permits it: request_user_input in Codex or AskUserQuestion in Claude. 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. A timeout, dismissed form, empty response or ended turn is not an answer. Keep the decision pending and resume it; do not start dependent work or report it as resolved. Existing answers remain valid through retries and handoffs.
|
|
15
15
|
|
|
16
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.
|
|
@@ -22,6 +22,8 @@ A task nobody is going to finish is abandoned rather than inherited. Ask the use
|
|
|
22
22
|
|
|
23
23
|
If an existing task is identified or execution resumes after compaction, call `session.resume`. Reconcile backend sequence, local outbox, Markdown projections, current Git diff, pinned revisions, and the active lease before any further action.
|
|
24
24
|
|
|
25
|
+
Reconcile pending questions too. A required question remains unanswered across a timeout, dismissed form, interruption or restart. Use `questionnaire.resume` for its recorded identifier and wait for an explicit valid response before dependent work. Follow `questionnaires.md`; do not create a replacement async prompt or consume an unrelated task's answer.
|
|
26
|
+
|
|
25
27
|
## Discovery
|
|
26
28
|
|
|
27
29
|
Perform only read operations until the relevant current code, tests, Figma evidence, Git diff, and returned memory have been inspected.
|
|
@@ -81,26 +83,51 @@ Temporary code written to reach or force a path — a pinned state, a fixed serv
|
|
|
81
83
|
|
|
82
84
|
Do not write task Markdown files directly. The bridge owns event IDs, expected task versions, atomic projections, outbox state, and synchronization.
|
|
83
85
|
|
|
84
|
-
##
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
86
|
+
## Four-mode project onboarding
|
|
87
|
+
|
|
88
|
+
Use `references/project-onboarding.md` and `engineering-core.adopting-an-existing-project` for the
|
|
89
|
+
full flow. Project creation asks through a native questionnaire for `greenfield`, `adopt`, or
|
|
90
|
+
`rework`; later incremental work is `refresh`. Read-only maturity analysis can recommend a mode but
|
|
91
|
+
cannot select one. Adopt, rework discovery, and refresh use committed `HEAD` by default, walk an
|
|
92
|
+
exhaustive cross-stack source inventory, and classify results as evidence, inference, or unknown.
|
|
93
|
+
The inventory covers frontend pages, navigation, state, service classes and network layers; backend
|
|
94
|
+
modules, services, data and API endpoints; project architecture; and the contracts connecting both
|
|
95
|
+
sides.
|
|
96
|
+
|
|
97
|
+
Greenfield applies compatible organization architecture modules and gates them with the real local
|
|
98
|
+
toolchain. Adopt and refresh never modify source. Rework asks per project whether the target keeps
|
|
99
|
+
behavior or redesigns it before proposing phased branches and tasks. Existing membership, active
|
|
100
|
+
user, tenant scope, explicit assignments, Project-first strongest-lock-first ordering, supplied
|
|
101
|
+
`EntityManager` transactions, immutable revisions and scope authority remain in force.
|
|
102
|
+
|
|
103
|
+
The implemented onboarding operations are `project.inspect`, `project.initialize`, `memory.source`,
|
|
104
|
+
`memory.sync_start`, `memory.sync_status`, `memory.sync_inventory`, `memory.sync_plan`,
|
|
105
|
+
`memory.sync_next`, `memory.sync_submit`, `memory.sync_reopen`, `memory.sync_delta`,
|
|
106
|
+
`memory.sync_verify`, `memory.catalog`, `memory.read_revisions`, and `memory.review_batch`.
|
|
107
|
+
`memory.sync_start` snapshots a selected Git ref, persists its commit/tree/manifest hashes and file
|
|
108
|
+
count, and uploads all manifest pages including exclusions. `memory.source` serves only bounded pages
|
|
109
|
+
or exact file reads from that committed snapshot. `memory.sync_status` lists runs or paged units and
|
|
110
|
+
coverage counters without exposing claim tokens.
|
|
111
|
+
|
|
112
|
+
`memory.sync_plan` uploads semantic units and their paths and dependency keys. The initial plan must
|
|
113
|
+
cover included and removed behavior, but analyzing may add newly discovered cross-module flow units.
|
|
114
|
+
Existing unit identities and definitions remain immutable; every added unit needs the same complete
|
|
115
|
+
submission, exact memory approval references and final verification before the run can complete.
|
|
116
|
+
`memory.sync_next` claims a distinct unit for 15 minutes;
|
|
117
|
+
the saved claim token explicitly resumes or renews that claim. `memory.sync_submit` records evidence,
|
|
118
|
+
all behavior facets, gaps and exact proposal or unchanged revision references. No unit is autoapproved,
|
|
119
|
+
and no historical task or motivation may be invented. Rejected or gapped unfinished units use
|
|
120
|
+
`memory.sync_reopen` with the current expected version. `memory.sync_delta` compares a completed prior
|
|
121
|
+
run's content hashes, memory revisions and transitive dependencies.
|
|
122
|
+
|
|
123
|
+
`memory.review_batch` displays full proposal texts before a durable native questionnaire records the
|
|
124
|
+
exact proposal IDs, content digests and base revisions. Pending or dismissed approval writes nothing;
|
|
125
|
+
stale batches fail atomically. `memory.sync_verify` requires sealed inventory, complete semantic and
|
|
126
|
+
removed-file coverage, submitted evidence, no unresolved gaps or failed unknown execution, and
|
|
127
|
+
approved immutable references for changed memory. `questionnaire.ask` and `questionnaire.resume`
|
|
128
|
+
support `preparation: true` for missing-folder decisions; `project.initialize` uses that native
|
|
129
|
+
confirmation before preparing the folder and Git repository. Raw secrets, headers, customer payloads
|
|
130
|
+
and PII are never uploaded.
|
|
104
131
|
|
|
105
132
|
## The Task Baseline
|
|
106
133
|
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Project onboarding and memory refresh
|
|
2
|
+
|
|
3
|
+
## Choose the journey before changing the repository
|
|
4
|
+
|
|
5
|
+
Call `project.inspect` for the selected directory, including a missing directory. Its maturity
|
|
6
|
+
classification is `missing`, `empty`, `scaffold` or `established`, supported by manifests and
|
|
7
|
+
architecture/state/service/network/data/screen evidence. These are observations, not a mode choice.
|
|
8
|
+
Explain the appropriate options to the user and use `project.onboard` (or `project.initialize` for
|
|
9
|
+
initial Git preparation). The tool opens native forms for the actual mode and profile; do not write
|
|
10
|
+
an answer in chat or infer a choice from the directory classification.
|
|
11
|
+
|
|
12
|
+
The modes are `greenfield`, `adopt`, `rework` and `refresh`. Initial onboarding offers the first three;
|
|
13
|
+
refresh is available for a selected existing project at any later time. Recognize the user's intent
|
|
14
|
+
semantically in their own phrasing, with no keyword trigger table. Rework asks preserve or redesign
|
|
15
|
+
explicitly. Choosing another mode than the proposed input returns that choice without changing files;
|
|
16
|
+
repeat with the same requestKey and the user's selected mode.
|
|
17
|
+
|
|
18
|
+
Use a stable requestKey per onboarding occurrence. Profile answers must be collected with native
|
|
19
|
+
forms; each independent field is reviewed separately with its current value visible. The user may
|
|
20
|
+
keep it, explicitly defer it, or request an edit in the same bounded native text field. An edited value
|
|
21
|
+
is persisted as part of the exact decision and reaches the final profile digest; it cannot be silently
|
|
22
|
+
replaced by a later chat value. The onboarding forms confirm the exact
|
|
23
|
+
stack/runtime/architecture, service/API/auth/storage/environment choices and existing backend project
|
|
24
|
+
IDs. For Flutter/React they additionally confirm Figma, color palette, font families and text styles,
|
|
25
|
+
design system, navigation, platforms and languages; for Nest/Spring they confirm migration strategy.
|
|
26
|
+
An explicit absence or deferral is acceptable; a made-up answer is not.
|
|
27
|
+
The local machine's installed runtime is validation capability, not an architecture preference.
|
|
28
|
+
|
|
29
|
+
`project.initialize` only prepares Git for the approved greenfield mode. It supports missing, empty
|
|
30
|
+
and barely-started non-Git directories without overwriting existing files; parent repositories and
|
|
31
|
+
symbolic links are protected. Choices persist in user-local Engineering Memory state outside the
|
|
32
|
+
repository and installed package. After creating/selecting the backend project, pass projectId to
|
|
33
|
+
`project.onboard` to store its exact choice in the project onboarding history. Include the confirmed
|
|
34
|
+
profile in the normal project/profile proposal; onboarding choices do not independently activate
|
|
35
|
+
engineering rules. `project.onboarding_status` reads those choices and any rework plans. Old projects
|
|
36
|
+
remain uninspected until they opt in.
|
|
37
|
+
|
|
38
|
+
If a native form is pending, preserve its identifier and resume it. A wizard can return the next
|
|
39
|
+
pending form after one answer is accepted; complete that form and call the originating onboarding
|
|
40
|
+
operation again to advance. Restart reuses answered questions and exact choice data. It never assumes
|
|
41
|
+
approval after a cancellation, empty response or timeout.
|
|
42
|
+
|
|
43
|
+
## Greenfield architecture and links
|
|
44
|
+
|
|
45
|
+
Prepare and bind the repository through the existing project.setup flow, then use architecture.plan
|
|
46
|
+
and architecture.module with the approved project profile. Apply the matching Flutter, React, NestJS
|
|
47
|
+
or Spring templates through the scaffold task lifecycle. Replace technical demonstration inputs with
|
|
48
|
+
approved application profile values; a compile fixture is not a user product or a ready live API.
|
|
49
|
+
Flutter exposes injected service/transport, state lifecycle and cancellation, storage boundary,
|
|
50
|
+
navigation and profile-controlled palette/typography. Validate the actual selected toolchain.
|
|
51
|
+
|
|
52
|
+
List authorized backend/frontend projects when services may already exist. Offer the actual selected
|
|
53
|
+
project IDs in the native profile form. Create approved links with project.link; a selection alone
|
|
54
|
+
never changes a link, and fullstack discipline never replaces membership or link authority. Keep
|
|
55
|
+
unavailable service contracts explicit rather than inventing endpoint behavior.
|
|
56
|
+
|
|
57
|
+
## Durable deep learning
|
|
58
|
+
|
|
59
|
+
Adopt, refresh and the learning stage of rework do not modify application source. They inventory and
|
|
60
|
+
learn the whole architecture: modules, service classes, network and API contracts, state management,
|
|
61
|
+
persistence/data models/migrations, navigation, screens/components, localization, deployment,
|
|
62
|
+
authentication, runtime/toolchain and cross-module/end-to-end flows. Record source facts, tested
|
|
63
|
+
behavior, inference and unknown historical reasons separately. Never fabricate historical tasks,
|
|
64
|
+
prior developer decisions or reasons the code cannot establish.
|
|
65
|
+
|
|
66
|
+
1. `memory.sync_start` opens a durable native confirmation bound to the mode, committed source hash
|
|
67
|
+
and previousRunId/rework behavior. Only acceptance starts the run. It snapshots the selected HEAD
|
|
68
|
+
or explicit ref, uploads every manifest page with exclusion reasons and seals the inventory.
|
|
69
|
+
Dirty worktree changes never silently enter shared memory. Repeating an occurrence reuses its key;
|
|
70
|
+
another commit requires a new occurrence and its own exact confirmation.
|
|
71
|
+
2. `memory.source` pages the committed manifest or bounded chunks of one exact-commit file. Follow
|
|
72
|
+
nextCursor/nextOffset until null. Exclusions are reviewable evidence; inspect suspicious exclusions
|
|
73
|
+
and correct the classifier rather than declaring an incomplete project fully learned. Large owned
|
|
74
|
+
files remain in scope; bounded Git reads at the same commit can supplement the tool.
|
|
75
|
+
3. `memory.sync_plan` creates meaningful semantic units with module keys, knowledge kinds, source
|
|
76
|
+
paths and dependency unit keys. Declare applicable architecture/state/services/network/data/UI/flow
|
|
77
|
+
areas and justify absence with source evidence. File coverage is necessary but does not certify
|
|
78
|
+
semantic understanding. Do not collapse an application into a generic all-files summary.
|
|
79
|
+
4. `memory.sync_next` leases one unit for fifteen minutes. Save claimToken and use distinct requestKeys
|
|
80
|
+
for parallel workers; retry the same key after response loss. `memory.sync_status` exposes progress
|
|
81
|
+
and missing units without exposing another worker's token. No database transaction spans analysis.
|
|
82
|
+
5. `memory.sync_submit` attaches per-path source or actual execution evidence and explicit findings
|
|
83
|
+
for entry, exit, authority, effects, errors, cancellation, retry, persistence and dependencies.
|
|
84
|
+
Link concrete witnesses; lack of applicability needs evidence and a reason. A source read and a
|
|
85
|
+
successful test are separate claims. Automated structural checks cannot prove the prose is true;
|
|
86
|
+
review the complete module package against actual code and tests before approval.
|
|
87
|
+
6. Reuse the normal KnowledgeResource → pending Proposal → immutable Revision model. Present complete
|
|
88
|
+
module records, source evidence, uncertainty, deviations and archival actions. `memory.review_batch`
|
|
89
|
+
binds native approval to exact IDs, content hashes and base revisions, and reviews atomically under
|
|
90
|
+
existing authority. Deferring makes no change; when the user wants to reconsider, use its returned
|
|
91
|
+
nextReviewAttempt. Retrying the old attempt preserves the deferral, not an invented approval.
|
|
92
|
+
7. `memory.sync_reopen` uses expectedVersion to repair an unfinished unit. `memory.sync_verify` requires
|
|
93
|
+
sealed inventory, source/removed-path and declared-area coverage, valid evidence, resolved gaps and
|
|
94
|
+
approved exact memory references. It cannot replace the Git diff gate for any source-writing task.
|
|
95
|
+
|
|
96
|
+
## Incremental refresh and Git evidence
|
|
97
|
+
|
|
98
|
+
Use a completed previousRunId. `memory.sync_delta` refuses an unsealed new inventory and pages changed
|
|
99
|
+
units and dependent flows. With repoRoot on the first page it adds bounded Git history/rename evidence;
|
|
100
|
+
`memory.source` with previousCommit and commit can read further source change pages. Commit IDs and
|
|
101
|
+
source trees establish provenance; commit messages do not establish developer intent. Squash or
|
|
102
|
+
rewritten/missing history falls back explicitly to the persisted file/blob comparison. A missing
|
|
103
|
+
commit is not proof of zero changes.
|
|
104
|
+
|
|
105
|
+
Review renames against existing resource identities and propose selector relocation rather than
|
|
106
|
+
unnecessarily creating duplicate records. Removed behavior requires reviewed archival with the old
|
|
107
|
+
identity; revision history is retained. Code deviating from a rule does not silently change that rule.
|
|
108
|
+
Known changed memory invalidates its units/dependants. New records are compared against selectors;
|
|
109
|
+
unscoped records may require conservative review because there is no defensible narrow scope.
|
|
110
|
+
A source/memory no-op reuses approved evidence with provenance and creates no gratuitous revisions.
|
|
111
|
+
|
|
112
|
+
At module boundaries read memory.sync_status with repoRoot. If newer commits exist, keep the pinned
|
|
113
|
+
inspection unchanged and start a new `memory.sync_start` refresh occurrence with the follow-up commit
|
|
114
|
+
and previousRunId; never silently move its baseline or call old coverage current. Read-only completion
|
|
115
|
+
is a coverage decision, not an implementation verification. Long-running analysis must checkpoint its
|
|
116
|
+
remaining units and decisions durably.
|
|
117
|
+
|
|
118
|
+
The core pack is intentionally bounded. Linked contracts remain inline while they fit the budget; when
|
|
119
|
+
the response reports `additionalMemory`, read the full authorized catalogue with
|
|
120
|
+
`memory.catalog({projectId, includeLinked: true, afterId, limit})`, then use the current session with
|
|
121
|
+
`memory.read_revisions` for the exact revision IDs needed by the task. Do not assume the core pack is
|
|
122
|
+
exhaustive. `memory.catalog` is the paging surface for linked project memory; it does not grant access
|
|
123
|
+
or change project links.
|
|
124
|
+
|
|
125
|
+
For source orientation, prefer the bounded summary returned by `project.inspect` and its nested stack
|
|
126
|
+
signals. Its `totalFiles`, `omittedFiles`, `prunedDirectories` and `historyTruncated` fields are part
|
|
127
|
+
of the evidence: a nonzero omission or truncated history requires bounded follow-up, not a claim that
|
|
128
|
+
the tree or history was fully read. Use the existing paged `memory.source` pages only for the committed
|
|
129
|
+
paths and symbols required by the chosen unit; do not request or serialize an unbounded full tree.
|
|
130
|
+
|
|
131
|
+
## Rework target and execution
|
|
132
|
+
|
|
133
|
+
Complete deep learning first. Propose and approve target architecture in existing immutable knowledge;
|
|
134
|
+
redesign also requires the intended screens, flows and API/service contracts. Build a dependency-ordered
|
|
135
|
+
phase plan with acceptance criteria and branch names, covering every inspected unit by key. Present it
|
|
136
|
+
in full. `project.rework_plan` opens a native form for that exact source/target/phase digest and persists
|
|
137
|
+
real WorkItems and their dependencies; stale targets, incomplete learning or changed idempotency inputs
|
|
138
|
+
must refuse before partial creation. Deferred plans can be submitted under a new explicit requestKey
|
|
139
|
+
when the user requests review again.
|
|
140
|
+
|
|
141
|
+
Each phase opens an ordinary implementation task on its recorded branch in a separate worktree where
|
|
142
|
+
needed. Preserve all lease, history, reconciliation, self-review, validation, verify, close and delivery
|
|
143
|
+
gates. Cross-project phases use existing work_item.plan/confirm_plan with explicit scope authorization
|
|
144
|
+
and independent per-repository runs. Planning tasks does not perform their implementation.
|
|
145
|
+
|
|
146
|
+
A deferred individual field or final profile saves no onboarding choice. Retrying that request preserves its deferral. When the user explicitly resumes, use the returned `nextRequestKey` for fresh native decisions; never infer approval from a previous defer.
|