engineering-memory 0.2.2 → 0.2.5
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/install/installer.mjs +24 -11
- package/install/mcp-registration.mjs +25 -2
- package/package.json +1 -1
- package/runtime/dist/src/git/git-inspector.js +37 -0
- package/runtime/dist/src/git/verification-gate.js +11 -1
- package/runtime/dist/src/index.js +3 -0
- package/runtime/dist/src/mcp/tool-definitions.js +51 -11
- package/runtime/dist/src/runtime/bridge-service.js +63 -13
- package/runtime/dist/src/runtime/shadow-notice-store.js +26 -0
- package/skill/references/lifecycle.md +14 -0
- package/skill/references/questionnaires.md +21 -2
package/install/installer.mjs
CHANGED
|
@@ -107,8 +107,29 @@ export async function installEngineeringMemory(options = {}) {
|
|
|
107
107
|
dispatcherSection: dispatcherSections.claude,
|
|
108
108
|
},
|
|
109
109
|
};
|
|
110
|
+
const mcpPlans = await planMcpRegistrations({
|
|
111
|
+
selectedClients,
|
|
112
|
+
bridgeEntry,
|
|
113
|
+
nodePath,
|
|
114
|
+
apiUrl,
|
|
115
|
+
clientVersion,
|
|
116
|
+
state: existingState,
|
|
117
|
+
commandRunner,
|
|
118
|
+
clientsWereRequested: options.selectedClients !== undefined,
|
|
119
|
+
});
|
|
120
|
+
const absentClients = mcpPlans
|
|
121
|
+
.filter((plan) => plan.action === 'absent')
|
|
122
|
+
.map((plan) => plan.clientName);
|
|
123
|
+
const installedClients = selectedClients.filter(
|
|
124
|
+
(clientName) => !absentClients.includes(clientName),
|
|
125
|
+
);
|
|
126
|
+
if (installedClients.length === 0) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`None of the supported agent CLIs are on PATH: ${selectedClients.join(', ')}. Install one, then rerun the installer.`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
110
131
|
const clientPlans = await Promise.all(
|
|
111
|
-
|
|
132
|
+
installedClients.map(async (clientName) => {
|
|
112
133
|
const target = clientTargets[clientName];
|
|
113
134
|
return {
|
|
114
135
|
clientName,
|
|
@@ -120,15 +141,6 @@ export async function installEngineeringMemory(options = {}) {
|
|
|
120
141
|
};
|
|
121
142
|
}),
|
|
122
143
|
);
|
|
123
|
-
const mcpPlans = await planMcpRegistrations({
|
|
124
|
-
selectedClients,
|
|
125
|
-
bridgeEntry,
|
|
126
|
-
nodePath,
|
|
127
|
-
apiUrl,
|
|
128
|
-
clientVersion,
|
|
129
|
-
state: existingState,
|
|
130
|
-
commandRunner,
|
|
131
|
-
});
|
|
132
144
|
const hookPlan = await planGitHook({
|
|
133
145
|
repoRoot: options.repoRoot ? resolve(options.repoRoot) : null,
|
|
134
146
|
mode: options.hookMode ?? 'cancel',
|
|
@@ -189,7 +201,8 @@ export async function installEngineeringMemory(options = {}) {
|
|
|
189
201
|
statePath,
|
|
190
202
|
skillPaths: clientPlans.map((plan) => plan.skillPath),
|
|
191
203
|
dispatcherPaths: clientPlans.map((plan) => plan.dispatcherPath),
|
|
192
|
-
selectedClients,
|
|
204
|
+
selectedClients: installedClients,
|
|
205
|
+
absentClients,
|
|
193
206
|
apiUrl,
|
|
194
207
|
clientVersion,
|
|
195
208
|
runtimePath,
|
|
@@ -48,6 +48,7 @@ export async function planMcpRegistrations({
|
|
|
48
48
|
clientVersion,
|
|
49
49
|
state,
|
|
50
50
|
commandRunner,
|
|
51
|
+
clientsWereRequested = true,
|
|
51
52
|
}) {
|
|
52
53
|
const plans = [];
|
|
53
54
|
for (const clientName of selectedClients) {
|
|
@@ -55,6 +56,10 @@ export async function planMcpRegistrations({
|
|
|
55
56
|
if (!client) throw new Error(`Unsupported client: ${clientName}`);
|
|
56
57
|
const version = await commandRunner(client.executable, ['--version']);
|
|
57
58
|
if (version.code !== 0) {
|
|
59
|
+
if (!clientsWereRequested && !state?.mcp?.[clientName]) {
|
|
60
|
+
plans.push({ clientName, client, action: 'absent' });
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
58
63
|
throw new Error(
|
|
59
64
|
`${clientName} CLI is unavailable. Install its native CLI, ensure it is on PATH, then rerun the installer. ${commandFailure(client.executable, version)}`,
|
|
60
65
|
);
|
|
@@ -121,6 +126,7 @@ export async function planMcpRegistrations({
|
|
|
121
126
|
export async function applyMcpRegistrations(plans, commandRunner, transaction) {
|
|
122
127
|
const result = {};
|
|
123
128
|
for (const plan of plans) {
|
|
129
|
+
if (plan.action === 'absent') continue;
|
|
124
130
|
if (plan.action === 'add') {
|
|
125
131
|
await addRegistration(plan, plan.registration, commandRunner);
|
|
126
132
|
transaction.add(() => removeRegistration(plan, commandRunner));
|
|
@@ -211,6 +217,19 @@ function registrationFingerprint(clientName, registration) {
|
|
|
211
217
|
.digest('hex');
|
|
212
218
|
}
|
|
213
219
|
|
|
220
|
+
function fingerprintBeforeClientVersion(clientName, registration) {
|
|
221
|
+
return createHash('sha256')
|
|
222
|
+
.update(
|
|
223
|
+
JSON.stringify({
|
|
224
|
+
clientName,
|
|
225
|
+
nodePath: registration.nodePath,
|
|
226
|
+
bridgeEntry: registration.bridgeEntry,
|
|
227
|
+
apiUrl: registration.apiUrl ?? null,
|
|
228
|
+
}),
|
|
229
|
+
)
|
|
230
|
+
.digest('hex');
|
|
231
|
+
}
|
|
232
|
+
|
|
214
233
|
function sameRegistration(left, right) {
|
|
215
234
|
return (
|
|
216
235
|
left?.nodePath === right.nodePath &&
|
|
@@ -462,9 +481,13 @@ function isOwnedRegistration(clientName, managed) {
|
|
|
462
481
|
typeof managed.bridgeEntry === 'string'
|
|
463
482
|
);
|
|
464
483
|
}
|
|
484
|
+
if (typeof managed.apiUrl !== 'string') return false;
|
|
485
|
+
if (managed.fingerprint === registrationFingerprint(clientName, managed)) {
|
|
486
|
+
return true;
|
|
487
|
+
}
|
|
465
488
|
return (
|
|
466
|
-
|
|
467
|
-
managed.fingerprint ===
|
|
489
|
+
managed.clientVersion === undefined &&
|
|
490
|
+
managed.fingerprint === fingerprintBeforeClientVersion(clientName, managed)
|
|
468
491
|
);
|
|
469
492
|
}
|
|
470
493
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "engineering-memory",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
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",
|
|
@@ -3,6 +3,7 @@ import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
|
3
3
|
import { assertManagedPath, canonicalPath } from '../utilities/files.js';
|
|
4
4
|
import { sha256, stableStringify } from '../utilities/hash.js';
|
|
5
5
|
import { NativeCommandRunner } from '../utilities/process.js';
|
|
6
|
+
export const temporaryScaffoldingMarker = 'ENGINEERING-MEMORY-TEMPORARY';
|
|
6
7
|
export class GitInspector {
|
|
7
8
|
runner;
|
|
8
9
|
constructor(runner = new NativeCommandRunner()) {
|
|
@@ -273,6 +274,42 @@ export class GitInspector {
|
|
|
273
274
|
.map((line) => line.trim())
|
|
274
275
|
.filter(Boolean);
|
|
275
276
|
}
|
|
277
|
+
async temporaryScaffolding(repoRoot) {
|
|
278
|
+
const root = await this.findRoot(repoRoot);
|
|
279
|
+
const [worktree, staged] = await Promise.all([
|
|
280
|
+
this.markerMatches(root, ['--untracked'], 'worktree'),
|
|
281
|
+
this.markerMatches(root, ['--cached'], 'staged'),
|
|
282
|
+
]);
|
|
283
|
+
const found = new Map();
|
|
284
|
+
for (const match of [...worktree, ...staged]) {
|
|
285
|
+
const identity = `${match.path}:${match.line}`;
|
|
286
|
+
const existing = found.get(identity);
|
|
287
|
+
found.set(identity, existing ? { ...existing, staged: existing.staged || match.staged } : match);
|
|
288
|
+
}
|
|
289
|
+
return [...found.values()].sort((left, right) => left.path === right.path ? left.line - right.line : left.path.localeCompare(right.path));
|
|
290
|
+
}
|
|
291
|
+
async markerMatches(root, scope, origin) {
|
|
292
|
+
const result = await this.runner.run('git', ['grep', '--no-color', '-n', '-I', '-F', ...scope, '-e', temporaryScaffoldingMarker], { cwd: root });
|
|
293
|
+
if (result.exitCode !== 0)
|
|
294
|
+
return [];
|
|
295
|
+
return result.stdout
|
|
296
|
+
.split(/\r?\n/)
|
|
297
|
+
.map((line) => line.trim())
|
|
298
|
+
.filter(Boolean)
|
|
299
|
+
.flatMap((line) => {
|
|
300
|
+
const match = /^(.+?):(\d+):(.*)$/.exec(line);
|
|
301
|
+
if (!match)
|
|
302
|
+
return [];
|
|
303
|
+
return [
|
|
304
|
+
{
|
|
305
|
+
path: normalizeGitPath(match[1]),
|
|
306
|
+
line: Number(match[2]),
|
|
307
|
+
text: match[3].trim().slice(0, 200),
|
|
308
|
+
staged: origin === 'staged',
|
|
309
|
+
},
|
|
310
|
+
];
|
|
311
|
+
});
|
|
312
|
+
}
|
|
276
313
|
async gitValue(repoRoot, args) {
|
|
277
314
|
const result = await this.runner.run('git', args, { cwd: repoRoot });
|
|
278
315
|
const value = result.stdout.trim().split(/\r?\n/)[0];
|
|
@@ -35,15 +35,25 @@ export class VerificationGate {
|
|
|
35
35
|
}, this.root);
|
|
36
36
|
}
|
|
37
37
|
async verify(repoRoot) {
|
|
38
|
-
const [fingerprint, manifest, stagedChanges, pending] = await Promise.all([
|
|
38
|
+
const [fingerprint, manifest, stagedChanges, pending, scaffolding] = await Promise.all([
|
|
39
39
|
this.git.fingerprint(repoRoot),
|
|
40
40
|
this.git.manifest(repoRoot),
|
|
41
41
|
this.git.stagedManifest(repoRoot),
|
|
42
42
|
this.outbox.list(),
|
|
43
|
+
this.git.temporaryScaffolding(repoRoot),
|
|
43
44
|
]);
|
|
44
45
|
if (pending.length > 0) {
|
|
45
46
|
return { allowed: false, reason: 'offline_outbox_pending', diffHash: manifest.diffHash };
|
|
46
47
|
}
|
|
48
|
+
const staged = scaffolding.filter((match) => match.staged);
|
|
49
|
+
if (staged.length > 0) {
|
|
50
|
+
return {
|
|
51
|
+
allowed: false,
|
|
52
|
+
reason: 'temporary_scaffolding_staged',
|
|
53
|
+
diffHash: manifest.diffHash,
|
|
54
|
+
details: staged.map((match) => `${match.path}:${match.line}`),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
47
57
|
const receipt = await readJson(this.pathFor(fingerprint), this.root);
|
|
48
58
|
if (!receipt) {
|
|
49
59
|
return { allowed: false, reason: 'task_verify_required', diffHash: manifest.diffHash };
|
|
@@ -14,6 +14,7 @@ import { BridgeService } from './runtime/bridge-service.js';
|
|
|
14
14
|
import { OfflineOutbox } from './runtime/offline-outbox.js';
|
|
15
15
|
import { ActiveContextStore } from './runtime/active-context-store.js';
|
|
16
16
|
import { RepositoryDecisionStore } from './runtime/repository-decision-store.js';
|
|
17
|
+
import { ShadowNoticeStore } from './runtime/shadow-notice-store.js';
|
|
17
18
|
import { UpdateChoiceStore } from './runtime/update-choice-store.js';
|
|
18
19
|
import { PrincipalStateGuard } from './runtime/principal-state.js';
|
|
19
20
|
export function createBridgeService() {
|
|
@@ -43,6 +44,7 @@ export function createBridgeService() {
|
|
|
43
44
|
const activeContexts = new ActiveContextStore(stateRoot);
|
|
44
45
|
const repositoryDecisions = new RepositoryDecisionStore(stateRoot);
|
|
45
46
|
const updateChoices = new UpdateChoiceStore(stateRoot);
|
|
47
|
+
const shadowNotices = new ShadowNoticeStore(stateRoot);
|
|
46
48
|
const gate = new VerificationGate(stateRoot, git, outbox);
|
|
47
49
|
const principalState = new PrincipalStateGuard(stateRoot, credentials, cache, outbox, activeContexts, gate);
|
|
48
50
|
return new BridgeService({
|
|
@@ -56,6 +58,7 @@ export function createBridgeService() {
|
|
|
56
58
|
activeContexts,
|
|
57
59
|
repositoryDecisions,
|
|
58
60
|
updateChoices,
|
|
61
|
+
shadowNotices,
|
|
59
62
|
clientVersion: config.clientVersion,
|
|
60
63
|
principalState,
|
|
61
64
|
});
|
|
@@ -47,6 +47,7 @@ export const engineeringMemoryToolNames = [
|
|
|
47
47
|
'session.entry',
|
|
48
48
|
'session.set_decision',
|
|
49
49
|
'session.decline_update',
|
|
50
|
+
'session.answer_shadow_notice',
|
|
50
51
|
'session.bootstrap',
|
|
51
52
|
'session.resume',
|
|
52
53
|
'context.prepare_change',
|
|
@@ -76,6 +77,13 @@ export const engineeringMemoryToolNames = [
|
|
|
76
77
|
'auth.signin_browser',
|
|
77
78
|
'auth.logout',
|
|
78
79
|
];
|
|
80
|
+
const reconciliationEntry = z.object({
|
|
81
|
+
resourceId: z.string().min(1),
|
|
82
|
+
type: z.enum(['approved_revision', 'no_semantic_memory_change']),
|
|
83
|
+
proposalId: z.string().optional(),
|
|
84
|
+
revisionId: z.string().optional(),
|
|
85
|
+
reason: z.string().optional(),
|
|
86
|
+
});
|
|
79
87
|
export function registerEngineeringMemoryTools(server, service) {
|
|
80
88
|
server.registerTool('session.entry', {
|
|
81
89
|
description: 'The first call of every session in a repository, before answering anything about the project. Reports whether the user is signed in, whether this repository is bound, what this user decided about it last time, and the one thing to do now. A repository the user switched Engineering Memory off in reports that, and is left alone.',
|
|
@@ -92,6 +100,16 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
92
100
|
description: 'Record that the user turned down the available client update. Call it only when they say no. The same version is never offered again; a newer one is a new question. A required update is not declinable and must not be recorded here.',
|
|
93
101
|
inputSchema: z.object({}),
|
|
94
102
|
}, async () => toolResult(await service.sessionDeclineUpdate()));
|
|
103
|
+
server.registerTool('session.answer_shadow_notice', {
|
|
104
|
+
description: 'Record that the user has answered a shadowed rule notice: the record in force overrides a wider rule that has since changed, and they chose whether to read the new text or keep the override. Call it only after they answer. The same revision of that wider rule is never raised again; a later revision is a new question.',
|
|
105
|
+
inputSchema: z.object({
|
|
106
|
+
resourceId: z.string().describe('The wider rule that is being hidden.'),
|
|
107
|
+
revisionNumber: z
|
|
108
|
+
.number()
|
|
109
|
+
.int()
|
|
110
|
+
.describe('The revision of that rule the user was told about.'),
|
|
111
|
+
}),
|
|
112
|
+
}, async (input) => toolResult(await service.sessionAnswerShadowNotice(input)));
|
|
95
113
|
server.registerTool('session.bootstrap', {
|
|
96
114
|
description: 'Authenticate, resolve the repository project, open or resume a write or read-only task, and load mandatory engineering context before planning.',
|
|
97
115
|
inputSchema: z.object({
|
|
@@ -247,30 +265,52 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
247
265
|
}),
|
|
248
266
|
}, async (input) => toolResult(await service.taskSelfReview(input)));
|
|
249
267
|
server.registerTool('task.reconcile', {
|
|
250
|
-
description: 'Reconcile
|
|
268
|
+
description: 'Reconcile changed screens and components with an approved revision or an explicit no-semantic-memory-change reason. Pass every record the task touched as `entries` in one call rather than calling once per record; the whole set is applied together and rejected together.',
|
|
251
269
|
inputSchema: z
|
|
252
270
|
.object({
|
|
253
271
|
repoRoot: optionalRepoRoot,
|
|
254
272
|
taskId: z.string().min(1),
|
|
255
|
-
|
|
256
|
-
|
|
273
|
+
entries: z.array(reconciliationEntry).min(1).max(50).optional(),
|
|
274
|
+
resourceId: z.string().min(1).optional(),
|
|
275
|
+
type: z.enum(['approved_revision', 'no_semantic_memory_change']).optional(),
|
|
257
276
|
proposalId: z.string().optional(),
|
|
258
277
|
revisionId: z.string().optional(),
|
|
259
278
|
reason: z.string().optional(),
|
|
260
279
|
})
|
|
261
280
|
.superRefine((value, context) => {
|
|
262
|
-
|
|
281
|
+
const entries = value.entries ??
|
|
282
|
+
(value.resourceId && value.type
|
|
283
|
+
? [
|
|
284
|
+
{
|
|
285
|
+
resourceId: value.resourceId,
|
|
286
|
+
type: value.type,
|
|
287
|
+
proposalId: value.proposalId,
|
|
288
|
+
revisionId: value.revisionId,
|
|
289
|
+
reason: value.reason,
|
|
290
|
+
},
|
|
291
|
+
]
|
|
292
|
+
: null);
|
|
293
|
+
if (!entries) {
|
|
263
294
|
context.addIssue({
|
|
264
295
|
code: 'custom',
|
|
265
|
-
message: '
|
|
296
|
+
message: 'Reconciliation requires entries, or a single resourceId and type',
|
|
266
297
|
});
|
|
298
|
+
return;
|
|
267
299
|
}
|
|
268
|
-
|
|
269
|
-
(!
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
300
|
+
for (const entry of entries) {
|
|
301
|
+
if (entry.type === 'approved_revision' && (!entry.proposalId || !entry.revisionId)) {
|
|
302
|
+
context.addIssue({
|
|
303
|
+
code: 'custom',
|
|
304
|
+
message: 'Approved reconciliation requires proposalId and revisionId',
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
if (entry.type === 'no_semantic_memory_change' &&
|
|
308
|
+
(!entry.reason || entry.reason.length < 8)) {
|
|
309
|
+
context.addIssue({
|
|
310
|
+
code: 'custom',
|
|
311
|
+
message: 'No-semantic-change reconciliation requires a reason of at least 8 characters',
|
|
312
|
+
});
|
|
313
|
+
}
|
|
274
314
|
}
|
|
275
315
|
}),
|
|
276
316
|
}, async (input) => toolResult(await service.taskReconcile(input)));
|
|
@@ -439,6 +439,7 @@ export class BridgeService {
|
|
|
439
439
|
...(preparedData?.lease ? { activeLease: preparedData.lease } : {}),
|
|
440
440
|
});
|
|
441
441
|
}
|
|
442
|
+
const shadowed = await this.unansweredShadowNotices(response.data);
|
|
442
443
|
return asJsonValue({
|
|
443
444
|
...objectOrEmpty(response.data),
|
|
444
445
|
editLeaseGranted: true,
|
|
@@ -446,9 +447,42 @@ export class BridgeService {
|
|
|
446
447
|
strictVerifyBlocked: false,
|
|
447
448
|
backendSource: responseSource,
|
|
448
449
|
repository: publicRepository(repository),
|
|
450
|
+
shadowedRuleNotices: shadowed,
|
|
451
|
+
...(shadowed.length > 0
|
|
452
|
+
? {
|
|
453
|
+
nextAction: 'A record this change relies on overrides a wider rule that has changed since the override was written. Say so in this reply, name the wider rule, and ask whether to read its new text or keep the override. Record the answer with session.answer_shadow_notice so it is not raised again until that rule moves.',
|
|
454
|
+
}
|
|
455
|
+
: {}),
|
|
449
456
|
});
|
|
450
457
|
});
|
|
451
458
|
}
|
|
459
|
+
async sessionAnswerShadowNotice(input) {
|
|
460
|
+
return await this.execute(async () => {
|
|
461
|
+
await this.dependencies.shadowNotices.answer(input.resourceId, input.revisionNumber);
|
|
462
|
+
return asJsonValue({ resourceId: input.resourceId, answered: true });
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
async unansweredShadowNotices(data) {
|
|
466
|
+
const shadowed = objectValue(objectValue(data)?.shadowed);
|
|
467
|
+
if (!shadowed)
|
|
468
|
+
return [];
|
|
469
|
+
const notices = Object.values(shadowed)
|
|
470
|
+
.flatMap((entry) => (Array.isArray(entry) ? entry : []))
|
|
471
|
+
.flatMap((entry) => {
|
|
472
|
+
const hidden = objectValue(entry);
|
|
473
|
+
const resourceId = hidden?.resourceId;
|
|
474
|
+
const resourceKey = hidden?.resourceKey;
|
|
475
|
+
const revisionNumber = hidden?.currentRevisionNumber;
|
|
476
|
+
return typeof resourceId === 'string' &&
|
|
477
|
+
typeof resourceKey === 'string' &&
|
|
478
|
+
typeof revisionNumber === 'number'
|
|
479
|
+
? [{ resourceId, resourceKey, revisionNumber }]
|
|
480
|
+
: [];
|
|
481
|
+
});
|
|
482
|
+
const unanswered = await this.dependencies.shadowNotices.unanswered(notices);
|
|
483
|
+
const wanted = new Set(unanswered.map((notice) => notice.resourceId));
|
|
484
|
+
return notices.filter((notice) => wanted.has(notice.resourceId));
|
|
485
|
+
}
|
|
452
486
|
async contextRefresh(input) {
|
|
453
487
|
return await this.execute(async () => {
|
|
454
488
|
const response = await this.dependencies.client.request(endpoints.contextRefresh, {
|
|
@@ -627,29 +661,31 @@ export class BridgeService {
|
|
|
627
661
|
}
|
|
628
662
|
async taskReconcile(input) {
|
|
629
663
|
return await this.execute(async () => {
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
664
|
+
const repoRoot = input.repoRoot ?? process.cwd();
|
|
665
|
+
const entries = (input.entries ?? [
|
|
666
|
+
{
|
|
667
|
+
resourceId: input.resourceId,
|
|
668
|
+
type: input.type,
|
|
669
|
+
proposalId: input.proposalId,
|
|
670
|
+
revisionId: input.revisionId,
|
|
671
|
+
reason: input.reason,
|
|
672
|
+
},
|
|
673
|
+
]).map((entry) => ({
|
|
674
|
+
...entry,
|
|
675
|
+
reason: normalizePersistentInput(entry.reason, repoRoot),
|
|
637
676
|
}));
|
|
677
|
+
assertSafeToPersist(cleanJson({ entries }));
|
|
638
678
|
const snapshot = await this.activeTaskSnapshot(input.taskId, undefined, input.repoRoot);
|
|
639
679
|
const response = await this.dependencies.client.request(endpoints.taskReconcile, {
|
|
640
680
|
method: 'POST',
|
|
641
681
|
body: cleanJson({
|
|
642
682
|
taskId: input.taskId,
|
|
643
|
-
|
|
644
|
-
type: input.type,
|
|
645
|
-
proposalId: input.proposalId,
|
|
646
|
-
revisionId: input.revisionId,
|
|
647
|
-
reason,
|
|
683
|
+
entries,
|
|
648
684
|
expectedTaskVersion: snapshot.taskVersion,
|
|
649
685
|
}),
|
|
650
686
|
});
|
|
651
687
|
await this.dependencies.gate.invalidateTask(input.taskId);
|
|
652
|
-
return asJsonValue({
|
|
688
|
+
return asJsonValue({ reconciliations: response.data });
|
|
653
689
|
});
|
|
654
690
|
}
|
|
655
691
|
async taskResolvePendingDelivery(input) {
|
|
@@ -784,6 +820,20 @@ export class BridgeService {
|
|
|
784
820
|
diffHash: repository.git.diffHash,
|
|
785
821
|
});
|
|
786
822
|
}
|
|
823
|
+
const scaffolding = await this.dependencies.repositories.git.temporaryScaffolding(repository.repoRoot);
|
|
824
|
+
if (scaffolding.length > 0) {
|
|
825
|
+
return asJsonValue({
|
|
826
|
+
verified: false,
|
|
827
|
+
reason: 'temporary_scaffolding_present',
|
|
828
|
+
temporaryScaffolding: scaffolding.map((match) => ({
|
|
829
|
+
location: `${match.path}:${match.line}`,
|
|
830
|
+
text: match.text,
|
|
831
|
+
staged: match.staged,
|
|
832
|
+
})),
|
|
833
|
+
nextAction: 'Temporary code written to reach or force a path is still in the working tree. Remove every listed line and restore what it replaced, then verify again. A manipulation that ships is a defect, so this is checked in the tree rather than taken on trust.',
|
|
834
|
+
diffHash: repository.git.diffHash,
|
|
835
|
+
});
|
|
836
|
+
}
|
|
787
837
|
let taskChangedPaths = [];
|
|
788
838
|
let taskChanges = [];
|
|
789
839
|
const snapshotResponse = await this.dependencies.client.request(endpoints.sessionResume, {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { readJson, writeJson } from '../utilities/files.js';
|
|
3
|
+
export class ShadowNoticeStore {
|
|
4
|
+
path;
|
|
5
|
+
root;
|
|
6
|
+
constructor(stateRoot) {
|
|
7
|
+
this.root = join(stateRoot, 'client');
|
|
8
|
+
this.path = join(this.root, 'shadow-notices.json');
|
|
9
|
+
}
|
|
10
|
+
async unanswered(pending) {
|
|
11
|
+
const record = await readJson(this.path, this.root);
|
|
12
|
+
const answered = record?.answered ?? {};
|
|
13
|
+
return pending.filter((notice) => (answered[notice.resourceId] ?? -1) < notice.revisionNumber);
|
|
14
|
+
}
|
|
15
|
+
async answer(resourceId, revisionNumber) {
|
|
16
|
+
const existing = await readJson(this.path, this.root);
|
|
17
|
+
const record = {
|
|
18
|
+
schemaVersion: 1,
|
|
19
|
+
answered: { ...(existing?.answered ?? {}), [resourceId]: revisionNumber },
|
|
20
|
+
updatedAt: new Date().toISOString(),
|
|
21
|
+
};
|
|
22
|
+
await writeJson(this.path, record, this.root);
|
|
23
|
+
return record;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=shadow-notice-store.js.map
|
|
@@ -33,8 +33,22 @@ A request can be as short as "add the KYC flow from Figma". That is enough, and
|
|
|
33
33
|
|
|
34
34
|
Then propose the `flow_logic` record and stop. The plan is not a message in the chat; it is the proposal, and the user approving it is the approval. Do not write the second screen before that approval exists — a flow's shape replicated across six screens costs six times as much to undo, and verification refuses a task that adds several screens without an approved flow record reconciled to it.
|
|
35
35
|
|
|
36
|
+
### A task that reports a defect
|
|
37
|
+
|
|
38
|
+
Settle first whether the report is about what the user sees or where they go. If it is, the
|
|
39
|
+
design carries the intended behaviour and the code carries the actual one, so read both
|
|
40
|
+
before naming a cause: open the frame the report starts from, follow its prototype link to
|
|
41
|
+
the destination the design intends, and state the divergence. Never repoint navigation at a
|
|
42
|
+
target inferred only from surrounding code. Carry both node ids into the discovery
|
|
43
|
+
checkpoint as evidence. A defect in a calculation, a response shape or a service has no
|
|
44
|
+
answer in the design; do not spend the read there.
|
|
45
|
+
|
|
36
46
|
Do not pull history for everything. Pull it for the records the task actually touches, and for anything the header shows a surprising number of corrections on. Record `task.checkpoint` with type `discovery` and update STATE, DECISIONS, DISCOVERY, and HANDOFF projections through the bridge.
|
|
37
47
|
|
|
48
|
+
Send calls that do not feed each other in one batch rather than one at a time: reads of any kind, and proposals for different records. Reconcile every record the task touched in a single `task.reconcile` call with `entries`, not one call per record. Keep in order only what moves the task version, and what genuinely waits on another answer.
|
|
49
|
+
|
|
50
|
+
Temporary code written to reach or force a path — a pinned state, a fixed service response, a jump straight to the screen — is allowed and expected, carries the marker `ENGINEERING-MEMORY-TEMPORARY` with its reason, and is removed before verification. `task.verify` refuses while any marker is in the tree and names every line, and the commit gate refuses while one is staged.
|
|
51
|
+
|
|
38
52
|
Do not write task Markdown files directly. The bridge owns event IDs, expected task versions, atomic projections, outbox state, and synchronization.
|
|
39
53
|
|
|
40
54
|
## Change Preparation
|
|
@@ -70,9 +70,28 @@ When the user names a different Figma file or link, treat it as a correction to
|
|
|
70
70
|
|
|
71
71
|
## Running the Application
|
|
72
72
|
|
|
73
|
-
When the change is something a person sees or interacts with and this environment can run the application, ask before launching it rather than starting on your own. A run occupies the user's machine
|
|
73
|
+
When the change is something a person sees or interacts with and this environment can run the application, ask before launching it rather than starting on your own. A run occupies the user's machine and their time, and the answer should be theirs.
|
|
74
74
|
|
|
75
|
-
Ask
|
|
75
|
+
Ask with the price attached. Say what would be exercised, what is being looked for, and roughly how long it takes, and carry a recommendation rather than leaving the choice bare. For a change confined to one screen with no service on the path, recommend against running and say what will be verified instead — a five-minute change should not cost an hour of driving the application. For a flow that spans several screens, or anything touching a service, recommend running, because that is exactly what reading cannot answer.
|
|
76
|
+
|
|
77
|
+
When they decline, do not raise it again for this task. Verify what you can without it and state in the handoff that the flow was not exercised in a running application.
|
|
78
|
+
|
|
79
|
+
## Design Reading
|
|
80
|
+
|
|
81
|
+
Before proposing a flow record or naming a cause from the design, put the reading in front of
|
|
82
|
+
the user: the frames found, their node ids, and the order the prototype links imply. Ask
|
|
83
|
+
whether that is the flow and where it is entered from. A prototype link is evidence, not a
|
|
84
|
+
decision, and the design cannot say which entry point the product intends. Never skip
|
|
85
|
+
straight from what the design showed to a proposal.
|
|
86
|
+
|
|
87
|
+
## Shadowed Product Rule
|
|
88
|
+
|
|
89
|
+
When `context.prepare_change` returns `shadowedRuleNotices`, a record this change relies on
|
|
90
|
+
overrides a wider rule that has changed since the override was written. Say so in the same
|
|
91
|
+
reply, name the wider rule, and ask whether to read its new text or keep the override. The
|
|
92
|
+
override still wins if that is the answer — it just stops being silent. Record the answer
|
|
93
|
+
with `session.answer_shadow_notice`, which keeps the same revision from being raised again;
|
|
94
|
+
a later revision of that rule is a new question.
|
|
76
95
|
|
|
77
96
|
## Correction Scope
|
|
78
97
|
|