engineering-memory 1.11.9 → 1.11.10

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.
Files changed (34) hide show
  1. package/dispatcher/sections.mjs +6 -2
  2. package/install/codex-approval.mjs +457 -0
  3. package/install/files.mjs +5 -1
  4. package/install/installer.mjs +24 -1
  5. package/package.json +1 -1
  6. package/runtime/build.json +1 -1
  7. package/runtime/dist/src/config.js +1 -0
  8. package/runtime/dist/src/git/git-inspector.js +82 -3
  9. package/runtime/dist/src/git/verification-gate.js +30 -17
  10. package/runtime/dist/src/mcp/onboarding-tools.js +3 -16
  11. package/runtime/dist/src/mcp/questionnaire-tools.js +99 -15
  12. package/runtime/dist/src/mcp/tool-annotations.js +111 -0
  13. package/runtime/dist/src/mcp/tool-definitions.js +148 -28
  14. package/runtime/dist/src/mcp/worktree-tools.js +323 -236
  15. package/runtime/dist/src/project/repository.js +11 -5
  16. package/runtime/dist/src/runtime/active-context-store.js +3 -0
  17. package/runtime/dist/src/runtime/branch-preferences.js +0 -19
  18. package/runtime/dist/src/runtime/bridge-service.js +1131 -220
  19. package/runtime/dist/src/runtime/offline-outbox.js +33 -158
  20. package/runtime/dist/src/runtime/phase-timer.js +26 -0
  21. package/runtime/dist/src/runtime/privacy-detector.js +263 -0
  22. package/runtime/dist/src/runtime/questionnaire-store.js +210 -49
  23. package/runtime/dist/src/runtime/recovery-error.js +3 -1
  24. package/runtime/dist/src/runtime/runtime-entry.js +8 -0
  25. package/runtime/dist/src/runtime/runtime-host.js +1 -3
  26. package/runtime/dist/src/runtime/task-branch-store.js +17 -1
  27. package/runtime/dist/src/runtime/task-start.js +281 -0
  28. package/runtime/dist/src/runtime/worktree-pool.js +334 -24
  29. package/runtime/dist/src/utilities/process.js +1 -0
  30. package/skill/SKILL.md +3 -3
  31. package/skill/references/lifecycle.md +44 -19
  32. package/skill/references/memory-updates.md +9 -3
  33. package/skill/references/project-onboarding.md +19 -2
  34. package/skill/references/questionnaires.md +77 -15
@@ -1,7 +1,7 @@
1
1
  import * as z from 'zod/v4';
2
2
  import { validationIds } from '../runtime/bridge-service.js';
3
3
  import { hostAnswerSchema, questionnaireDefinitionSchema } from '../runtime/questionnaire-store.js';
4
- import { answerQuestionnaireFromHost, askQuestionnaire, resumeQuestionnaire, } from './questionnaire-tools.js';
4
+ import { answerChoice, answerQuestionnaireFromHost, askQuestionnaire, resumeQuestionnaire, } from './questionnaire-tools.js';
5
5
  const optionalRepoRoot = z.string().min(1).optional();
6
6
  const stringList = z.array(z.string().min(1));
7
7
  const jsonValue = z.lazy(() => z.union([
@@ -149,7 +149,10 @@ const reconciliationEntry = z.object({
149
149
  resourceId: z.string().min(1),
150
150
  type: z.enum(['approved_revision', 'no_semantic_memory_change', 'scaffold_applied']),
151
151
  proposalId: z.string().optional(),
152
- revisionId: z.string().optional(),
152
+ revisionId: z
153
+ .string()
154
+ .optional()
155
+ .describe('Optional for approved_revision; resolved from the approved proposal when omitted.'),
153
156
  reason: z.string().optional(),
154
157
  });
155
158
  export function registerQuestionnaireTools(server, service) {
@@ -191,10 +194,11 @@ export function registerQuestionnaireTools(server, service) {
191
194
  }
192
195
  });
193
196
  server.registerTool('questionnaire.ask', {
194
- description: 'Persist a required decision before displaying a native questionnaire. Write the message and labels in the conversation language and set language (tr or en) for the native form help. Use a questionnaireId unique to this decision occurrence or task, and reuse it only for identical retries. A later decision or changed wording/options requires a new id. Only a schema-validated native acceptance answers it. Dismissal, timeout and missing replies remain pending without expiry. Never supply answers in questionnaire.ask arguments; use questionnaire.answer_from_host only after an actual permitted blocking native answer. Do not put personal information or secrets in the question or options. Free text is returned once and never stored; use explicit options for replayable decisions.',
197
+ description: 'Persist a required decision before displaying a native questionnaire. Write the message and labels in the conversation language and set language (tr or en) for the native form help. Use a questionnaireId unique to this decision occurrence or task, and reuse it only for identical retries. A later decision or changed wording/options requires a new id. Only a schema-validated native acceptance answers it. Dismissal, timeout and missing replies remain pending without expiry. Never supply answers in questionnaire.ask arguments; use questionnaire.answer_from_host only after an actual permitted blocking native answer. Do not put personal information or secrets in the question or options. Free text is returned once and never stored; use explicit options for replayable decisions. After the first decline in this session, pass presentation host_native to skip the doomed elicitation round trip.',
195
198
  inputSchema: questionnaireDefinitionSchema.extend({
196
199
  repoRoot: optionalRepoRoot,
197
200
  preparation: z.boolean().optional(),
201
+ presentation: z.literal('host_native').optional(),
198
202
  }),
199
203
  }, async (input, context) => await askQuestionnaire(server, service, input, context));
200
204
  server.registerTool('questionnaire.resume', {
@@ -259,14 +263,32 @@ export function registerEngineeringMemoryTools(server, service) {
259
263
  }),
260
264
  }, async (input) => toolResult(await service.sessionAnswerShadowNotice(input)));
261
265
  server.registerTool('session.bootstrap', {
262
- description: 'Authenticate, resolve the repository project, open or resume a write or read-only task, and load mandatory engineering context before planning.',
266
+ description: 'Authenticate, resolve the repository project, open or resume a write or read-only task, and load mandatory engineering context before planning. objective is one line of at most 240 characters. contextPack holds only what fit the response budget; deferredResources lists what did not, each entry carrying its revisionId so it can be read directly with memory.read_revisions rather than looked up again through memory.catalog.',
263
267
  inputSchema: z.object({
264
268
  repoRoot: optionalRepoRoot,
265
269
  projectId: z.string().optional(),
266
- externalTaskId: z.string().min(2),
267
- objective: z.string().min(2),
268
- taskKind: z.string().min(2),
269
- workItemKey: z.string().min(1).optional(),
270
+ externalTaskId: z
271
+ .string()
272
+ .min(2)
273
+ .max(160)
274
+ .regex(/^[^\r\n]+$/, 'externalTaskId must be a single line of at most 160 characters.'),
275
+ objective: z
276
+ .string()
277
+ .min(2)
278
+ .max(240)
279
+ .regex(/^[^\r\n]+$/, 'objective must be a single line of at most 240 characters.')
280
+ .describe('One line, 2-240 characters: what this task delivers. Put detail in checkpoints, not here.'),
281
+ taskKind: z
282
+ .string()
283
+ .min(2)
284
+ .max(80)
285
+ .regex(/^[^\r\n]+$/, 'taskKind must be a single line of at most 80 characters.'),
286
+ workItemKey: z
287
+ .string()
288
+ .min(2)
289
+ .max(120)
290
+ .regex(/^[^\r\n]+$/, 'workItemKey must be a single line of at most 120 characters.')
291
+ .optional(),
270
292
  workItemId: z.string().uuid().optional(),
271
293
  mode: z.enum(['write', 'read_only', 'scaffold']).optional(),
272
294
  linkedSources: z
@@ -300,14 +322,14 @@ export function registerEngineeringMemoryTools(server, service) {
300
322
  }),
301
323
  }, async (input) => toolResult(await service.contextPrepareChange(input)));
302
324
  server.registerTool('context.refresh', {
303
- description: 'Refresh pins within the task fixed code source after memory approval. Use the task repoRoot; source changes require a new task.',
325
+ description: 'Refresh pins within the task fixed code source after memory approval. Use the task repoRoot; source changes require a new task. contextPack carries only the revisions that changed since the session pinned them; unchangedResources lists the rest with the revisionId to reread one through memory.read_revisions, and deferredResources entries carry theirs too.',
304
326
  inputSchema: z.object({
305
327
  repoRoot: optionalRepoRoot,
306
328
  sessionId: z.string().min(1),
307
329
  }),
308
330
  }, async (input) => toolResult(await service.contextRefresh(input)));
309
331
  server.registerTool('memory.query', {
310
- description: 'Read the smallest project memory pack needed for screens, components, services, navigation, localization or state work.',
332
+ description: 'Read the smallest project memory pack needed for screens, components, services, navigation, localization or state work. resources holds what fits one response; deferredResources lists the rest, each with the revisionId to read it through memory.read_revisions.',
311
333
  inputSchema: z.object({
312
334
  projectId: z.string().min(1),
313
335
  sessionId: z.string().min(1),
@@ -319,17 +341,19 @@ export function registerEngineeringMemoryTools(server, service) {
319
341
  }),
320
342
  }, async (input) => toolResult(await service.memoryQuery(input)));
321
343
  server.registerTool('memory.history', {
322
- description: 'Read why the code you are about to change is the way it is: the revision history of the screen, component or contract records covering the given paths or keys, each revision with the task that produced it and the stated reason, plus the earlier tasks that touched those records and how many corrections each of them took. Also returns the task references found in the Git history of those paths. Verification refuses while a record with earlier work on it was never read.',
344
+ description: 'Read why the code you are about to change is the way it is: the revision history of the screen, component or contract records covering the given paths, keys or ids, each revision with the task that produced it and the stated reason, plus the earlier tasks that touched those records and how many corrections each of them took. Also returns the task references found in the Git history of those paths. Verification refuses while a record with earlier work on it was never read; required:true reads every such record of the current lease in one call. A path no record covers comes back in uncoveredPaths instead of an error.',
323
345
  inputSchema: z.object({
324
346
  repoRoot: optionalRepoRoot,
325
347
  sessionId: z.string().min(1),
326
348
  resourceKeys: z.array(z.string().min(1)).optional(),
349
+ resourceIds: z.array(z.string().uuid()).max(50).optional(),
327
350
  paths: z.array(z.string().min(1)).optional(),
351
+ required: z.boolean().optional(),
328
352
  depth: z.number().int().min(1).max(50).optional(),
329
353
  }),
330
354
  }, async (input) => toolResult(await service.memoryHistory(input)));
331
355
  server.registerTool('memory.propose_revision', {
332
- description: 'Create an inactive, reviewable proposal for permanent product, organization or project memory. Do not block independent task work on drafting, submission or approval. Use authorized host background agents or concurrent tools and continue useful work; collect the result only before an operation needs its proposal or revision ID. Without concurrency, checkpoint the pending draft and defer this call until needed. Keep one writer per resource and preserve baseRevision and task/source identity. A response distinguishes stored (queued:false) from outbox-queued; neither means approved. Native user approval and task verification remain required.',
356
+ description: "Create an inactive, reviewable proposal for permanent product, organization or project memory. Do not block independent task work on drafting, submission or approval. Use authorized host background agents or concurrent tools and continue useful work; collect the result only before an operation needs its proposal or revision ID. Without concurrency, checkpoint the pending draft and defer this call until needed. Keep one writer per resource and preserve baseRevision and task/source identity. A response distinguishes stored (queued:false) from outbox-queued; neither means approved. A stored proposal reports reviewableByCaller: ask this user to approve it only when that is true; otherwise report it as submitted, with its id, to the reviewPath it names (product_release, organization_owner or project_maintainer). Task verification remains required. metadata carries only the structured fields the resource kind defines, such as a project profile's pathRoles, architecture and resourceDiscovery, appliesToStacks, or an architecture template's manifest; classification, containsPii, containsSecrets and resourceDescriptor are computed by the server and ignored when sent, so metadata copied from a served revision is accepted. A project profile's metadata.architecture, when present, is exactly {adopted: [{structure, pressure}], declined: [{structure, why}]} (structure up to 80 characters, pressure/why up to 600, at most 40 entries each) and no other key; a free-text style such as 'MVVM' is not that shape and goes in metadata.architectureStyle (a plain string) or the profile content instead.",
333
357
  inputSchema: z.object({
334
358
  repoRoot: optionalRepoRoot,
335
359
  sourceRunId: z.string().uuid().optional(),
@@ -374,10 +398,15 @@ export function registerEngineeringMemoryTools(server, service) {
374
398
  }),
375
399
  }, async (input) => toolResult(await service.memoryProposeRevision(input)));
376
400
  server.registerTool('memory.list_proposals', {
377
- description: 'List the permanent memory proposals still waiting for review, so an authorized reviewer can find them without database access. Pass proposalId to read one proposal with its proposed content.',
401
+ description: "List the permanent memory proposals still waiting for review, so an authorized reviewer can find them without database access. Each entry carries a reasonPreview, not the full reason; pass proposalId to read one proposal in full, with its reason and proposed content. Narrow a long list with taskId, scope or status (defaults to pending); page through the rest with limit (default 50, max 100) and afterId, set to the response's nextAfterId to continue, which is null once nothing more is waiting. Each proposal reports reviewableByCaller; when it is false, the caller cannot approve it, so never ask this user for approval: report it as waiting on the reviewPath it names.",
378
402
  inputSchema: z.object({
379
403
  projectId: z.string().min(1),
380
404
  proposalId: z.string().min(1).optional(),
405
+ taskId: z.string().min(1).optional(),
406
+ scope: z.enum(['project', 'organization', 'product']).optional(),
407
+ status: z.enum(['pending', 'approved', 'rejected', 'superseded']).optional(),
408
+ afterId: z.string().min(1).optional(),
409
+ limit: z.number().int().min(1).max(100).optional(),
381
410
  }),
382
411
  }, async (input) => toolResult(await service.memoryListProposals(input)));
383
412
  server.registerTool('memory.review_proposal', {
@@ -415,21 +444,55 @@ export function registerEngineeringMemoryTools(server, service) {
415
444
  }),
416
445
  }, async (input) => toolResult(await service.taskRecordCorrection(input)));
417
446
  server.registerTool('task.self_review', {
418
- description: 'Record that the changed code was read back against the rules that govern it, naming the knowledge resources reviewed and every conflict found with how it was resolved. Returns a durable pending receipt without waiting for backend delivery; continue independent local validation. task.verify waits for required delivery and refuses until this exists for the current diff, so any further edit requires reviewing again.',
447
+ description: 'Record that each changed file was read back against the rules that govern it: one entry per changed file (deleted files excepted), naming every rule context.prepare_change returned for that path in governingRules, and for a file no role maps, the engineering rules you read it against. Each rule gets an outcome: follows; fixed, with the issue and the change you made; or user_accepted_deviation, with the rule resourceKey and the issue. A rule conflict is a question for the user, never a judgment call: for every deviation this tool asks the user natively whether to keep the code, records the deviation only on their approval, and refuses the review when they choose to change the code. Matching the surrounding code is not an outcome; existing code is evidence, not authority. Set language to the conversation language (tr or en) for the question. Returns a durable pending receipt without waiting for backend delivery; task.verify refuses until a review covers the current diff, so any further edit requires reviewing again.',
419
448
  inputSchema: z.object({
420
449
  repoRoot: optionalRepoRoot,
421
450
  taskId: z.string().min(1),
422
- reviewedResourceIds: z.array(z.string().min(1)),
423
- findings: z.array(z.object({
424
- path: z.string().min(1),
425
- rule: z.string().min(1),
426
- issue: z.string().min(8),
427
- resolution: z.string().min(8),
428
- })),
451
+ language: z.enum(['tr', 'en']).optional(),
452
+ files: z
453
+ .array(z.object({
454
+ path: z.string().min(1).max(512),
455
+ rules: z
456
+ .array(z.discriminatedUnion('outcome', [
457
+ z.object({
458
+ resourceId: z.string().uuid(),
459
+ outcome: z.literal('follows'),
460
+ }),
461
+ z.object({
462
+ resourceId: z.string().uuid(),
463
+ outcome: z.literal('fixed'),
464
+ issue: z.string().min(8).max(2000),
465
+ change: z.string().min(8).max(2000),
466
+ }),
467
+ z.object({
468
+ resourceId: z.string().uuid(),
469
+ outcome: z.literal('user_accepted_deviation'),
470
+ resourceKey: z.string().min(1).max(240),
471
+ issue: z.string().min(8).max(1000),
472
+ }),
473
+ ]))
474
+ .min(1)
475
+ .max(50),
476
+ }))
477
+ .max(500),
429
478
  }),
430
- }, async (input) => toolResult(await service.taskSelfReview(input)));
479
+ }, async (input, context) => {
480
+ let questions;
481
+ try {
482
+ questions = service.ruleDeviationQuestions(input);
483
+ }
484
+ catch {
485
+ return toolResult(await service.taskSelfReview(input));
486
+ }
487
+ for (const { definition, previousDefinitions } of questions) {
488
+ const form = await askQuestionnaire(server, service, { ...definition, repoRoot: input.repoRoot }, context, previousDefinitions);
489
+ if (!answerChoice(form))
490
+ return form;
491
+ }
492
+ return toolResult(await service.taskSelfReview(input));
493
+ });
431
494
  server.registerTool('task.reconcile', {
432
- 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. A pending result is a durable local receipt; continue independent work without polling. Required delivery is checked before verification.',
495
+ 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. After memory.review_proposal approves a proposal, pass its reconcileEntry ({resourceId, type: approved_revision, proposalId, revisionId}) straight through here — revisionId is optional and is resolved from the approved proposal when omitted. A pending result is a durable local receipt; continue independent work without polling. Required delivery is checked before verification.',
433
496
  inputSchema: z
434
497
  .object({
435
498
  repoRoot: optionalRepoRoot,
@@ -464,10 +527,10 @@ export function registerEngineeringMemoryTools(server, service) {
464
527
  return;
465
528
  }
466
529
  for (const entry of entries) {
467
- if (entry.type === 'approved_revision' && (!entry.proposalId || !entry.revisionId)) {
530
+ if (entry.type === 'approved_revision' && !entry.proposalId) {
468
531
  context.addIssue({
469
532
  code: 'custom',
470
- message: 'Approved reconciliation requires proposalId and revisionId',
533
+ message: 'Approved reconciliation requires proposalId',
471
534
  });
472
535
  }
473
536
  if (entry.type === 'no_semantic_memory_change' &&
@@ -498,11 +561,15 @@ export function registerEngineeringMemoryTools(server, service) {
498
561
  }),
499
562
  }, async (input) => toolResult(await service.taskResolvePendingDelivery(input)));
500
563
  server.registerTool('task.verify', {
501
- description: 'Verify a write task with its lease or a read-only task against its pinned Git baseline, mandatory checkpoints, structured validations and synchronized outbox.',
564
+ description: 'Verify a write task with its lease or a read-only task against its pinned Git baseline, mandatory checkpoints, structured validations and synchronized outbox. A refusal lists every unmet requirement at once. If the user decides a required check is not needed for this task, pass it in waivers; the tool asks them natively and records the answer.',
502
565
  inputSchema: z.object({
503
566
  repoRoot: optionalRepoRoot,
504
567
  taskId: z.string().min(1),
505
568
  sessionId: z.string().min(1),
569
+ language: z
570
+ .enum(['tr', 'en'])
571
+ .optional()
572
+ .describe('Language of the current conversation, for the waiver question.'),
506
573
  leaseId: z.string().min(1).optional(),
507
574
  changedPaths: stringList.min(1).optional(),
508
575
  validations: z
@@ -525,8 +592,61 @@ export function registerEngineeringMemoryTools(server, service) {
525
592
  .max(20)
526
593
  .optional()
527
594
  .describe('Architectural structures this task adds that the project did not have: a cache, a broker, a read replica, a second deployable, a projection, an event store. Verification refuses any the project profile has not recorded under architecture.adopted with the pressure it relieves.'),
528
- }),
529
- }, async (input) => toolResult(await service.taskVerify(input)));
595
+ waivers: z
596
+ .array(z.object({
597
+ validationId: z.enum(['ui']),
598
+ reason: z.string().min(10).max(500),
599
+ }))
600
+ .max(3)
601
+ .optional()
602
+ .describe('Only when the user decides a required ui check is not needed for this change. Never inferred from prose; the tool asks the user and verifies only after they agree.'),
603
+ }),
604
+ }, async (input, context) => {
605
+ const { language = 'en', ...request } = input;
606
+ for (const waiver of request.waivers ?? []) {
607
+ const subject = await service.validationWaiverSubject({
608
+ repoRoot: request.repoRoot,
609
+ taskId: request.taskId,
610
+ sessionId: request.sessionId,
611
+ ...waiver,
612
+ });
613
+ if (!subject.ok)
614
+ return toolResult(subject);
615
+ const { questionnaireId, externalTaskId, paths, reason } = subject.data;
616
+ const listed = paths.slice(0, 20).join(', ') + (paths.length > 20 ? ` (+${paths.length - 20})` : '');
617
+ const definitions = [
618
+ {
619
+ questionnaireId,
620
+ language: 'tr',
621
+ message: `${externalTaskId}: Bu görev için arayüz (UI) kontrolü atlansın mı? Değişen şu dosyalar ekranda görünen arayüzü oluşturuyor: ${listed}. Belirtilen gerekçe: ${reason}. Görev, bu dosyaların widget, golden veya ekran görüntüsü testi olmadan doğrulanır. Karar görevin denetim kaydına yazılır.`,
622
+ options: [
623
+ { id: 'waive', label: 'Bu dosyalar için UI kontrolünü atla' },
624
+ { id: 'require', label: 'UI kontrolü zorunlu kalsın' },
625
+ ],
626
+ },
627
+ {
628
+ questionnaireId,
629
+ language: 'en',
630
+ message: `${externalTaskId}: skip the UI check for this task? These changed files render UI: ${listed}. Reason given: ${reason}. The task will verify without a widget, golden or screenshot check of them. The decision is stored in the task's audit trail.`,
631
+ options: [
632
+ { id: 'waive', label: 'Skip the UI check for these files' },
633
+ { id: 'require', label: 'Keep the UI check required' },
634
+ ],
635
+ },
636
+ ];
637
+ const form = await askQuestionnaire(server, service, {
638
+ ...definitions.find((definition) => definition.language === language),
639
+ repoRoot: request.repoRoot,
640
+ }, context, definitions);
641
+ const record = await service.questionnaireResume({
642
+ repoRoot: request.repoRoot,
643
+ questionnaireId,
644
+ });
645
+ if (record.status !== 'answered' || record.answer?.choice !== 'waive')
646
+ return form;
647
+ }
648
+ return toolResult(await service.taskVerify(request));
649
+ });
530
650
  server.registerTool('task.close', {
531
651
  description: 'Close a task only when backend verification still matches the current Git diff hash.',
532
652
  inputSchema: z.object({