engineering-memory 0.2.3 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/runtime/dist/src/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 +107 -19
- package/runtime/dist/src/runtime/shadow-notice-store.js +26 -0
- package/skill/references/lifecycle.md +16 -0
- package/skill/references/questionnaires.md +21 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "engineering-memory",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
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)));
|
|
@@ -25,8 +25,26 @@ export const validationIds = [
|
|
|
25
25
|
export class BridgeService {
|
|
26
26
|
dependencies;
|
|
27
27
|
taskQueues = new Map();
|
|
28
|
+
deferDeliveries;
|
|
29
|
+
backgroundDelivery = Promise.resolve();
|
|
28
30
|
constructor(dependencies) {
|
|
29
31
|
this.dependencies = dependencies;
|
|
32
|
+
this.deferDeliveries = dependencies.deferDeliveries ?? true;
|
|
33
|
+
}
|
|
34
|
+
deliverInBackground() {
|
|
35
|
+
if (!this.deferDeliveries)
|
|
36
|
+
return;
|
|
37
|
+
this.backgroundDelivery = this.backgroundDelivery.then(async () => {
|
|
38
|
+
try {
|
|
39
|
+
await this.flushOutbox();
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
async settleDeliveries() {
|
|
47
|
+
await this.backgroundDelivery;
|
|
30
48
|
}
|
|
31
49
|
async sessionBootstrap(input) {
|
|
32
50
|
return await this.execute(async () => {
|
|
@@ -439,6 +457,7 @@ export class BridgeService {
|
|
|
439
457
|
...(preparedData?.lease ? { activeLease: preparedData.lease } : {}),
|
|
440
458
|
});
|
|
441
459
|
}
|
|
460
|
+
const shadowed = await this.unansweredShadowNotices(response.data);
|
|
442
461
|
return asJsonValue({
|
|
443
462
|
...objectOrEmpty(response.data),
|
|
444
463
|
editLeaseGranted: true,
|
|
@@ -446,9 +465,42 @@ export class BridgeService {
|
|
|
446
465
|
strictVerifyBlocked: false,
|
|
447
466
|
backendSource: responseSource,
|
|
448
467
|
repository: publicRepository(repository),
|
|
468
|
+
shadowedRuleNotices: shadowed,
|
|
469
|
+
...(shadowed.length > 0
|
|
470
|
+
? {
|
|
471
|
+
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.',
|
|
472
|
+
}
|
|
473
|
+
: {}),
|
|
449
474
|
});
|
|
450
475
|
});
|
|
451
476
|
}
|
|
477
|
+
async sessionAnswerShadowNotice(input) {
|
|
478
|
+
return await this.execute(async () => {
|
|
479
|
+
await this.dependencies.shadowNotices.answer(input.resourceId, input.revisionNumber);
|
|
480
|
+
return asJsonValue({ resourceId: input.resourceId, answered: true });
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
async unansweredShadowNotices(data) {
|
|
484
|
+
const shadowed = objectValue(objectValue(data)?.shadowed);
|
|
485
|
+
if (!shadowed)
|
|
486
|
+
return [];
|
|
487
|
+
const notices = Object.values(shadowed)
|
|
488
|
+
.flatMap((entry) => (Array.isArray(entry) ? entry : []))
|
|
489
|
+
.flatMap((entry) => {
|
|
490
|
+
const hidden = objectValue(entry);
|
|
491
|
+
const resourceId = hidden?.resourceId;
|
|
492
|
+
const resourceKey = hidden?.resourceKey;
|
|
493
|
+
const revisionNumber = hidden?.currentRevisionNumber;
|
|
494
|
+
return typeof resourceId === 'string' &&
|
|
495
|
+
typeof resourceKey === 'string' &&
|
|
496
|
+
typeof revisionNumber === 'number'
|
|
497
|
+
? [{ resourceId, resourceKey, revisionNumber }]
|
|
498
|
+
: [];
|
|
499
|
+
});
|
|
500
|
+
const unanswered = await this.dependencies.shadowNotices.unanswered(notices);
|
|
501
|
+
const wanted = new Set(unanswered.map((notice) => notice.resourceId));
|
|
502
|
+
return notices.filter((notice) => wanted.has(notice.resourceId));
|
|
503
|
+
}
|
|
452
504
|
async contextRefresh(input) {
|
|
453
505
|
return await this.execute(async () => {
|
|
454
506
|
const response = await this.dependencies.client.request(endpoints.contextRefresh, {
|
|
@@ -627,29 +679,47 @@ export class BridgeService {
|
|
|
627
679
|
}
|
|
628
680
|
async taskReconcile(input) {
|
|
629
681
|
return await this.execute(async () => {
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
type: input.type,
|
|
634
|
-
proposalId: input.proposalId,
|
|
635
|
-
revisionId: input.revisionId,
|
|
636
|
-
reason,
|
|
637
|
-
}));
|
|
638
|
-
const snapshot = await this.activeTaskSnapshot(input.taskId, undefined, input.repoRoot);
|
|
639
|
-
const response = await this.dependencies.client.request(endpoints.taskReconcile, {
|
|
640
|
-
method: 'POST',
|
|
641
|
-
body: cleanJson({
|
|
642
|
-
taskId: input.taskId,
|
|
682
|
+
const repoRoot = input.repoRoot ?? process.cwd();
|
|
683
|
+
const entries = (input.entries ?? [
|
|
684
|
+
{
|
|
643
685
|
resourceId: input.resourceId,
|
|
644
686
|
type: input.type,
|
|
645
687
|
proposalId: input.proposalId,
|
|
646
688
|
revisionId: input.revisionId,
|
|
647
|
-
reason,
|
|
648
|
-
|
|
649
|
-
|
|
689
|
+
reason: input.reason,
|
|
690
|
+
},
|
|
691
|
+
]).map((entry) => ({
|
|
692
|
+
...entry,
|
|
693
|
+
reason: normalizePersistentInput(entry.reason, repoRoot),
|
|
694
|
+
}));
|
|
695
|
+
assertSafeToPersist(cleanJson({ entries }));
|
|
696
|
+
const snapshot = await this.activeTaskSnapshot(input.taskId, undefined, input.repoRoot);
|
|
697
|
+
const body = cleanJson({
|
|
698
|
+
taskId: input.taskId,
|
|
699
|
+
entries,
|
|
700
|
+
expectedTaskVersion: snapshot.taskVersion,
|
|
650
701
|
});
|
|
651
702
|
await this.dependencies.gate.invalidateTask(input.taskId);
|
|
652
|
-
|
|
703
|
+
if (this.deferDeliveries) {
|
|
704
|
+
const queued = await this.dependencies.outbox.enqueue({
|
|
705
|
+
operation: 'task.reconcile',
|
|
706
|
+
method: 'POST',
|
|
707
|
+
path: endpoints.taskReconcile,
|
|
708
|
+
body,
|
|
709
|
+
});
|
|
710
|
+
this.deliverInBackground();
|
|
711
|
+
return asJsonValue({
|
|
712
|
+
reconciled: entries.map((entry) => entry.resourceId),
|
|
713
|
+
queued: true,
|
|
714
|
+
outboxId: queued.id,
|
|
715
|
+
deliveryStatus: 'pending',
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
const response = await this.dependencies.client.request(endpoints.taskReconcile, {
|
|
719
|
+
method: 'POST',
|
|
720
|
+
body,
|
|
721
|
+
});
|
|
722
|
+
return asJsonValue({ reconciliations: response.data });
|
|
653
723
|
});
|
|
654
724
|
}
|
|
655
725
|
async taskResolvePendingDelivery(input) {
|
|
@@ -784,6 +854,20 @@ export class BridgeService {
|
|
|
784
854
|
diffHash: repository.git.diffHash,
|
|
785
855
|
});
|
|
786
856
|
}
|
|
857
|
+
const scaffolding = await this.dependencies.repositories.git.temporaryScaffolding(repository.repoRoot);
|
|
858
|
+
if (scaffolding.length > 0) {
|
|
859
|
+
return asJsonValue({
|
|
860
|
+
verified: false,
|
|
861
|
+
reason: 'temporary_scaffolding_present',
|
|
862
|
+
temporaryScaffolding: scaffolding.map((match) => ({
|
|
863
|
+
location: `${match.path}:${match.line}`,
|
|
864
|
+
text: match.text,
|
|
865
|
+
staged: match.staged,
|
|
866
|
+
})),
|
|
867
|
+
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.',
|
|
868
|
+
diffHash: repository.git.diffHash,
|
|
869
|
+
});
|
|
870
|
+
}
|
|
787
871
|
let taskChangedPaths = [];
|
|
788
872
|
let taskChanges = [];
|
|
789
873
|
const snapshotResponse = await this.dependencies.client.request(endpoints.sessionResume, {
|
|
@@ -1313,7 +1397,10 @@ export class BridgeService {
|
|
|
1313
1397
|
assertSafeToPersist(cleanJson(safeInput));
|
|
1314
1398
|
return await this.taskExclusive(input.taskId, async () => {
|
|
1315
1399
|
await this.recoverJournalOutbox(input.projectId, input.taskSlug);
|
|
1316
|
-
|
|
1400
|
+
if (this.deferDeliveries)
|
|
1401
|
+
this.deliverInBackground();
|
|
1402
|
+
else
|
|
1403
|
+
await this.flushOutbox();
|
|
1317
1404
|
const journalInput = {
|
|
1318
1405
|
eventId: idempotencyKey,
|
|
1319
1406
|
taskId: input.taskId,
|
|
@@ -1376,7 +1463,8 @@ export class BridgeService {
|
|
|
1376
1463
|
await this.dependencies.activeContexts.updateTaskSnapshot(input.taskId, expectedTaskVersion + 1, pointer.lastSequence + 1);
|
|
1377
1464
|
}
|
|
1378
1465
|
const predecessorEntries = taskEntries.filter((entry) => entry.id !== queued.id);
|
|
1379
|
-
if (predecessorEntries.length > 0) {
|
|
1466
|
+
if (predecessorEntries.length > 0 || this.deferDeliveries) {
|
|
1467
|
+
this.deliverInBackground();
|
|
1380
1468
|
return asJsonValue({
|
|
1381
1469
|
local: { directory: staged.directory, applied: staged.applied },
|
|
1382
1470
|
queued: true,
|
|
@@ -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,24 @@ 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.
|
|
49
|
+
|
|
50
|
+
Checkpoints, recorded corrections and reconciliations return before the backend has them, reporting `deliveryStatus: 'pending'` with the task version they will occupy. That is a completed call, not a pending one: the journal is already durable, delivery is already under way, and `task.verify` refuses while anything remains undelivered. Do not wait for it, poll it, or send it again. Wait only for what the next step uses — a proposal's identifiers, an approval's revision, a prepared lease, and verification itself.
|
|
51
|
+
|
|
52
|
+
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.
|
|
53
|
+
|
|
38
54
|
Do not write task Markdown files directly. The bridge owns event IDs, expected task versions, atomic projections, outbox state, and synchronization.
|
|
39
55
|
|
|
40
56
|
## 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
|
|