memorix 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +3 -3
  3. package/README.zh-CN.md +3 -3
  4. package/dist/cli/index.js +452 -109
  5. package/dist/index.js +376 -87
  6. package/dist/maintenance-runner.js +174 -41
  7. package/dist/media-audio-runner.js +174 -41
  8. package/dist/media-video-runner.js +174 -41
  9. package/dist/memcode-runtime/CHANGELOG.md +37 -0
  10. package/dist/sdk.js +375 -86
  11. package/dist/types.d.ts +2 -0
  12. package/dist/vector-backfill-runner.js +174 -41
  13. package/docs/1.2.4-PERSISTENT-MEMORY-DELIVERY.md +1 -1
  14. package/docs/API_REFERENCE.md +1 -1
  15. package/docs/DEVELOPMENT.md +5 -3
  16. package/package.json +1 -1
  17. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  18. package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
  19. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  20. package/plugins/copilot/memorix/plugin.json +1 -1
  21. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  22. package/plugins/omp/memorix/package.json +1 -1
  23. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  24. package/plugins/pi/memorix/package.json +1 -1
  25. package/server.json +2 -2
  26. package/src/cli/commands/codegraph.ts +48 -5
  27. package/src/cli/commands/context.ts +20 -4
  28. package/src/cli/commands/resume.ts +4 -0
  29. package/src/codegraph/auto-context.ts +4 -1
  30. package/src/codegraph/context-pack.ts +3 -1
  31. package/src/codegraph/external-provider.ts +93 -0
  32. package/src/embedding/api-provider.ts +105 -22
  33. package/src/embedding/provider.ts +2 -0
  34. package/src/hooks/installers/index.ts +1 -1
  35. package/src/hooks/official-skills.ts +3 -3
  36. package/src/hooks/rules/memorix-agent-rules.md +1 -1
  37. package/src/knowledge/context-receipt.ts +41 -0
  38. package/src/knowledge/workflows.ts +6 -3
  39. package/src/knowledge/workset.ts +56 -2
  40. package/src/llm/provider.ts +17 -8
  41. package/src/memory/formation/extract.ts +6 -3
  42. package/src/memory/formation/index.ts +2 -1
  43. package/src/memory/formation/resolve.ts +10 -6
  44. package/src/memory/formation/types.ts +3 -1
  45. package/src/memory/long-term.ts +7 -1
  46. package/src/server.ts +29 -13
  47. package/src/store/orama-store.ts +6 -4
  48. package/src/timeout.ts +67 -0
  49. package/src/types.ts +2 -0
@@ -7,6 +7,9 @@ import {
7
7
  } from '../../codegraph/auto-context.js';
8
8
  import { getAllObservations } from '../../memory/observations.js';
9
9
  import { filterReadableObservations } from '../../memory/visibility.js';
10
+ import { buildBoundedContextReceipt } from '../../knowledge/context-receipt.js';
11
+ import { WORKFLOW_AGENT_TARGETS } from '../../knowledge/workflows.js';
12
+ import type { AgentTarget } from '../../types.js';
10
13
  import { emitError, emitResult, getCliProjectContext } from './operator-shared.js';
11
14
 
12
15
  function coerceRefreshMode(input?: string): AutoContextRefreshMode {
@@ -15,6 +18,13 @@ function coerceRefreshMode(input?: string): AutoContextRefreshMode {
15
18
  throw new Error('refresh must be one of: auto, always, never');
16
19
  }
17
20
 
21
+ function coerceAgentTarget(input?: string): AgentTarget | undefined {
22
+ const value = input?.trim().toLowerCase();
23
+ if (!value) return undefined;
24
+ if (WORKFLOW_AGENT_TARGETS.includes(value as AgentTarget)) return value as AgentTarget;
25
+ throw new Error('agent must be one of: ' + WORKFLOW_AGENT_TARGETS.join(', '));
26
+ }
27
+
18
28
  export default defineCommand({
19
29
  meta: {
20
30
  name: 'context',
@@ -29,10 +39,12 @@ export default defineCommand({
29
39
  },
30
40
  resume: { type: 'boolean', description: 'Always include the bounded prior-work projection' },
31
41
  refresh: { type: 'string', description: 'Project scan policy: auto, always, or never' },
42
+ agent: { type: 'string', description: 'Optional target agent for compatible workflow selection' },
32
43
  json: { type: 'boolean', description: 'Emit machine-readable JSON output' },
44
+ briefJson: { type: 'boolean', description: 'Emit only the bounded agent brief and receipt JSON' },
33
45
  },
34
46
  run: async ({ args }) => {
35
- const asJson = !!args.json;
47
+ const asJson = !!args.json || !!args.briefJson;
36
48
  const task = (args.task as string | undefined)?.trim()
37
49
  || (args.input as string | undefined)?.trim()
38
50
  || undefined;
@@ -44,13 +56,13 @@ export default defineCommand({
44
56
  dataDir,
45
57
  observations: filterReadableObservations(getAllObservations(), reader),
46
58
  task,
59
+ agent: coerceAgentTarget(args.agent as string | undefined),
47
60
  refresh: coerceRefreshMode(args.refresh as string | undefined),
48
61
  reader,
49
62
  ...(args.resume ? { continuation: 'always' as const } : {}),
50
63
  });
51
64
 
52
- emitResult(
53
- {
65
+ const detailed = {
54
66
  project,
55
67
  lens: context.lens,
56
68
  brief: buildAutoProjectBrief(context),
@@ -61,7 +73,11 @@ export default defineCommand({
61
73
  workset: context.workset,
62
74
  ...(context.task ? { task: context.task } : {}),
63
75
  ...(context.continuation ? { continuation: context.continuation } : {}),
64
- },
76
+ };
77
+ emitResult(
78
+ args.briefJson
79
+ ? buildBoundedContextReceipt({ workset: context.workset, providerQuality: context.providerQuality })
80
+ : detailed,
65
81
  formatAutoProjectContextPrompt(context),
66
82
  asJson,
67
83
  );
@@ -13,7 +13,9 @@ export default defineCommand({
13
13
  args: {
14
14
  task: { type: 'positional', description: 'Current continuation task', required: false },
15
15
  refresh: { type: 'string', description: 'Project scan policy: auto, always, or never' },
16
+ agent: { type: 'string', description: 'Optional target agent for compatible workflow selection' },
16
17
  json: { type: 'boolean', description: 'Emit machine-readable JSON output' },
18
+ briefJson: { type: 'boolean', description: 'Emit only the bounded agent brief and receipt JSON' },
17
19
  },
18
20
  async run({ args }) {
19
21
  await contextCommand.run?.({
@@ -21,7 +23,9 @@ export default defineCommand({
21
23
  _: [],
22
24
  input: args.task,
23
25
  refresh: args.refresh,
26
+ agent: args.agent,
24
27
  json: args.json,
28
+ briefJson: args.briefJson,
25
29
  resume: true,
26
30
  },
27
31
  rawArgs: [],
@@ -10,7 +10,7 @@ import {
10
10
  } from '../knowledge/workset.js';
11
11
  import type { ContextDeliveryTarget } from '../knowledge/context-assembly.js';
12
12
  import { sanitizeCredentials } from '../memory/secret-filter.js';
13
- import type { ObservationReader, ProjectInfo } from '../types.js';
13
+ import type { AgentTarget, ObservationReader, ProjectInfo } from '../types.js';
14
14
  import { backfillMissingObservationCodeRefs, type CodeRefBackfillResult } from './binder.js';
15
15
  import { collectCurrentProjectFacts, formatGitFact, type CurrentProjectFacts } from './current-facts.js';
16
16
  import { refreshProjectLite } from './lite-provider.js';
@@ -189,6 +189,8 @@ export async function buildAutoProjectContext(input: {
189
189
  dataDir: string;
190
190
  observations: ProjectContextObservation[];
191
191
  task?: string;
192
+ /** Explicit host target for task-compatible workflow selection. */
193
+ agent?: AgentTarget;
192
194
  refresh?: AutoContextRefreshMode;
193
195
  maxAgeMs?: number;
194
196
  now?: Date;
@@ -426,6 +428,7 @@ export async function buildAutoProjectContext(input: {
426
428
  projectId: input.project.id,
427
429
  dataDir: input.dataDir,
428
430
  ...(task ? { task } : {}),
431
+ ...(input.agent ? { agent: input.agent } : {}),
429
432
  lens: lens.id,
430
433
  startHere,
431
434
  ...(alwaysOn.profile.length > 0 || alwaysOn.state || alwaysOn.durable.length > 0
@@ -10,7 +10,7 @@ import type {
10
10
  } from './types.js';
11
11
  import type { CodeGraphStore } from './store.js';
12
12
  import { isCodeGraphExcludedPath } from './exclude.js';
13
- import type { ObservationReader } from '../types.js';
13
+ import type { AgentTarget, ObservationReader } from '../types.js';
14
14
 
15
15
  export interface ContextPackMemory {
16
16
  id: number;
@@ -278,6 +278,7 @@ export async function attachTaskWorkset(input: {
278
278
  providerQuality?: CodeGraphProviderQuality;
279
279
  runtimeCautions?: WorksetCaution[];
280
280
  reader?: ObservationReader;
281
+ agent?: AgentTarget;
281
282
  }): Promise<ContextPack> {
282
283
  const semanticStartHere = input.semanticCode
283
284
  ? [...input.semanticCode.relatedFiles, ...input.semanticCode.entryPoints.map(entry => entry.path)]
@@ -286,6 +287,7 @@ export async function attachTaskWorkset(input: {
286
287
  projectId: input.projectId,
287
288
  dataDir: input.dataDir,
288
289
  task: input.pack.task,
290
+ ...(input.agent ? { agent: input.agent } : {}),
289
291
  lens: input.lens,
290
292
  currentFacts: input.currentFacts ?? [],
291
293
  ...(input.codeState ? { codeState: input.codeState } : {}),
@@ -17,6 +17,8 @@ import type {
17
17
  // Keep the default useful in real sessions while still bounding a local call.
18
18
  export const DEFAULT_EXTERNAL_CODEGRAPH_TIMEOUT_MS = 1_200;
19
19
  export const MAX_EXTERNAL_CODEGRAPH_TIMEOUT_MS = 5_000;
20
+ /** Explicit index maintenance is opt-in and may legitimately take longer. */
21
+ export const EXTERNAL_CODEGRAPH_LIFECYCLE_TIMEOUT_MS = 30_000;
20
22
  export const MAX_EXTERNAL_CODEGRAPH_OUTPUT_BYTES = 256 * 1024;
21
23
  const MAX_EXTERNAL_NODE_COUNT = 16;
22
24
  const MAX_EXTERNAL_EDGE_COUNT = 24;
@@ -85,6 +87,19 @@ export interface ExternalCodeGraphContextResult extends InspectExternalCodeGraph
85
87
  caution?: string;
86
88
  }
87
89
 
90
+ /** Explicit lifecycle operations. Memorix never calls CodeGraph's `install`. */
91
+ export type ExternalCodeGraphLifecycleAction = 'init' | 'sync';
92
+
93
+ export interface ExternalCodeGraphLifecycleInput extends InspectExternalCodeGraphInput {
94
+ action: ExternalCodeGraphLifecycleAction;
95
+ }
96
+
97
+ export interface ExternalCodeGraphLifecycleResult extends InspectExternalCodeGraphResult {
98
+ action: ExternalCodeGraphLifecycleAction;
99
+ performed: boolean;
100
+ message: string;
101
+ }
102
+
88
103
  interface RawExternalStatus {
89
104
  initialized: boolean;
90
105
  projectPath: string;
@@ -437,6 +452,84 @@ export async function inspectExternalCodeGraph(input: InspectExternalCodeGraphIn
437
452
  return { health, quality: providerQuality({ mode, health }) };
438
453
  }
439
454
 
455
+ /**
456
+ * Run only a user-requested local lifecycle operation and re-inspect the
457
+ * result. This deliberately does not alter agent MCP files or invoke the
458
+ * upstream `codegraph install` command.
459
+ */
460
+ export async function runExternalCodeGraphLifecycle(
461
+ input: ExternalCodeGraphLifecycleInput,
462
+ ): Promise<ExternalCodeGraphLifecycleResult> {
463
+ const mode = input.mode ?? 'auto';
464
+ if (mode === 'off') {
465
+ const health: ExternalCodeGraphHealth = { state: 'disabled', reason: 'External CodeGraph is disabled by configuration.' };
466
+ return {
467
+ action: input.action,
468
+ performed: false,
469
+ message: 'External CodeGraph is disabled by configuration.',
470
+ health,
471
+ quality: providerQuality({ mode, health }),
472
+ };
473
+ }
474
+
475
+ const indexPath = join(input.projectRoot, '.codegraph');
476
+ if (input.action === 'sync' && !existsSync(indexPath)) {
477
+ const health: ExternalCodeGraphHealth = { state: 'not-detected', reason: 'No local .codegraph index is present for this project.' };
478
+ return {
479
+ action: input.action,
480
+ performed: false,
481
+ message: 'Initialize CodeGraph first with "memorix codegraph init".',
482
+ health,
483
+ quality: providerQuality({ mode, health }),
484
+ };
485
+ }
486
+
487
+ if (input.action === 'init' && existsSync(indexPath)) {
488
+ const inspected = await inspectExternalCodeGraph(input);
489
+ return {
490
+ action: input.action,
491
+ performed: false,
492
+ message: 'A local CodeGraph directory already exists; no initialization was performed.',
493
+ ...inspected,
494
+ };
495
+ }
496
+
497
+ const result = await runSafely(input.runner ?? defaultRunner, {
498
+ command: input.command,
499
+ args: input.action === 'init'
500
+ ? ['init', input.projectRoot]
501
+ : ['sync', input.projectRoot, '--quiet'],
502
+ cwd: input.projectRoot,
503
+ // A context read must stay fast; an explicit user-requested index sync is
504
+ // a different operation and gets its own bounded maintenance window.
505
+ timeoutMs: EXTERNAL_CODEGRAPH_LIFECYCLE_TIMEOUT_MS,
506
+ maxOutputBytes: MAX_EXTERNAL_CODEGRAPH_OUTPUT_BYTES,
507
+ });
508
+ if (!result.ok) {
509
+ const health = unavailableHealth(result);
510
+ return {
511
+ action: input.action,
512
+ performed: false,
513
+ message: 'CodeGraph ' + input.action + ' did not complete: ' + (health.reason ?? 'unknown local error'),
514
+ health,
515
+ quality: providerQuality({ mode, health }),
516
+ };
517
+ }
518
+
519
+ const inspected = await inspectExternalCodeGraph(input);
520
+ const remainsStale = inspected.health.state === 'stale';
521
+ return {
522
+ action: input.action,
523
+ performed: true,
524
+ message: input.action === 'init'
525
+ ? 'Local CodeGraph initialization completed. Run "memorix codegraph sync" when source changes.'
526
+ : remainsStale
527
+ ? 'Local CodeGraph synchronization completed, but the graph remains stale against the current worktree; this task will use Lite evidence.'
528
+ : 'Local CodeGraph synchronization completed and is ready for task-scoped semantic context.',
529
+ ...inspected,
530
+ };
531
+ }
532
+
440
533
  function safeRelativePath(projectRoot: string, value: unknown, exclude?: string[]): string | undefined {
441
534
  const path = safeText(value, 1_024);
442
535
  if (!path || isAbsolute(path)) return undefined;
@@ -406,6 +406,16 @@ interface APIEmbeddingConfig {
406
406
  requestedDimensions: number | null;
407
407
  }
408
408
 
409
+ class EmbeddingAPIError extends Error {
410
+ readonly status: number;
411
+
412
+ constructor(status: number, detail: string) {
413
+ super(`Embedding API error (${status}): ${detail}`);
414
+ this.name = 'EmbeddingAPIError';
415
+ this.status = status;
416
+ }
417
+ }
418
+
409
419
  export interface APIEmbeddingProviderCreateOptions {
410
420
  /**
411
421
  * When false, only previously persisted dimension metadata may initialize
@@ -432,6 +442,14 @@ function getPreferredBatchSize(config: APIEmbeddingConfig): number {
432
442
  function parseBatchLimit(error: unknown): number | null {
433
443
  if (!(error instanceof Error)) return null;
434
444
 
445
+ // Only a provider-side request-shape error may trigger recursive batch
446
+ // splitting. Billing, authentication, quota, and upstream errors must fail
447
+ // once; retrying them multiplies latency without changing the request.
448
+ const status = error instanceof EmbeddingAPIError
449
+ ? error.status
450
+ : Number(error.message.match(/Embedding API error \((\d{3})\)/i)?.[1] ?? 0);
451
+ if (status > 0 && status !== 400) return null;
452
+
435
453
  const explicit = error.message.match(/should not be larger than\s+(\d+)/i);
436
454
  if (explicit) return parseInt(explicit[1], 10);
437
455
 
@@ -712,16 +730,14 @@ export class APIEmbeddingProvider implements EmbeddingProvider {
712
730
  }
713
731
  } catch (error) {
714
732
  const providerLimit = parseBatchLimit(error);
715
- const fallbackSize = providerLimit ?? Math.ceil(chunkTexts.length / 2);
716
-
717
- if (chunkTexts.length > 1 && fallbackSize < chunkTexts.length) {
733
+ if (providerLimit !== null && chunkTexts.length > 1 && providerLimit < chunkTexts.length) {
718
734
  console.error(
719
- `[memorix] Embedding batch too large for provider, retrying in chunks of ${fallbackSize}`,
735
+ `[memorix] Embedding batch too large for provider, retrying in chunks of ${providerLimit}`,
720
736
  );
721
- for (let start = 0; start < chunkTexts.length; start += fallbackSize) {
737
+ for (let start = 0; start < chunkTexts.length; start += providerLimit) {
722
738
  await processChunk(
723
- chunkTexts.slice(start, start + fallbackSize),
724
- chunkIndices.slice(start, start + fallbackSize),
739
+ chunkTexts.slice(start, start + providerLimit),
740
+ chunkIndices.slice(start, start + providerLimit),
725
741
  );
726
742
  }
727
743
  return;
@@ -842,6 +858,9 @@ async function fetchWithRetry(
842
858
  ? Math.max(100, Math.min(Math.floor(options.timeoutMs), 60_000))
843
859
  : 10_000;
844
860
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
861
+ const signal = options.signal
862
+ ? AbortSignal.any([options.signal, controller.signal])
863
+ : controller.signal;
845
864
  let response: Response;
846
865
  try {
847
866
  response = await fetch(url, {
@@ -850,30 +869,94 @@ async function fetchWithRetry(
850
869
  ? { 'Content-Type': 'application/json', 'x-goog-api-key': apiKey }
851
870
  : { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },
852
871
  body: JSON.stringify(body),
853
- signal: controller.signal,
872
+ signal,
854
873
  });
855
874
  } catch (err: unknown) {
856
875
  clearTimeout(timeout);
857
- if (err instanceof Error && err.name === 'AbortError') {
876
+ if (err instanceof Error && (err.name === 'AbortError' || controller.signal.aborted || options.signal?.aborted)) {
858
877
  throw new Error(`Embedding API timeout after ${timeoutMs}ms: ${url}`);
859
878
  }
860
879
  throw err;
861
880
  }
862
- clearTimeout(timeout);
863
881
 
864
- if (response.ok) {
865
- return response.json() as Promise<EmbeddingAPIResponse>;
866
- }
882
+ try {
883
+ if (response.ok) {
884
+ return await raceWithAbort(
885
+ response.json() as Promise<EmbeddingAPIResponse>,
886
+ signal,
887
+ );
888
+ }
889
+
890
+ if (options.retry !== false && (response.status === 429 || response.status >= 500) && attempt < MAX_RETRIES) {
891
+ const delay = BASE_DELAY_MS * Math.pow(2, attempt);
892
+ const retryAfter = response.headers.get('retry-after');
893
+ const parsedRetryAfter = retryAfter ? parseInt(retryAfter, 10) * 1000 : NaN;
894
+ const waitMs = Number.isFinite(parsedRetryAfter) ? parsedRetryAfter : delay;
895
+ console.error(`[memorix] Embedding API ${response.status}, retry ${attempt + 1}/${MAX_RETRIES} in ${waitMs}ms`);
896
+ await waitForRetry(waitMs, options.signal);
897
+ return fetchWithRetry(url, apiKey, body, options, attempt + 1, nativeGemini);
898
+ }
867
899
 
868
- if (options.retry !== false && (response.status === 429 || response.status >= 500) && attempt < MAX_RETRIES) {
869
- const delay = BASE_DELAY_MS * Math.pow(2, attempt);
870
- const retryAfter = response.headers.get('retry-after');
871
- const waitMs = retryAfter ? parseInt(retryAfter, 10) * 1000 : delay;
872
- console.error(`[memorix] Embedding API ${response.status}, retry ${attempt + 1}/${MAX_RETRIES} in ${waitMs}ms`);
873
- await new Promise(resolve => setTimeout(resolve, waitMs));
874
- return fetchWithRetry(url, apiKey, body, options, attempt + 1, nativeGemini);
900
+ const errorText = await raceWithAbort(
901
+ response.text().catch(() => 'unknown error'),
902
+ signal,
903
+ );
904
+ throw new EmbeddingAPIError(response.status, errorText);
905
+ } catch (err: unknown) {
906
+ if (err instanceof Error && (err.name === 'AbortError' || controller.signal.aborted || options.signal?.aborted)) {
907
+ throw new Error(`Embedding API timeout after ${timeoutMs}ms: ${url}`);
908
+ }
909
+ throw err;
910
+ } finally {
911
+ clearTimeout(timeout);
875
912
  }
913
+ }
876
914
 
877
- const errorText = await response.text().catch(() => 'unknown error');
878
- throw new Error(`Embedding API error (${response.status}): ${errorText}`);
915
+ function abortError(): Error {
916
+ const error = new Error('Embedding API request aborted');
917
+ error.name = 'AbortError';
918
+ return error;
919
+ }
920
+
921
+ function raceWithAbort<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> {
922
+ if (signal.aborted) return Promise.reject(abortError());
923
+ return new Promise<T>((resolve, reject) => {
924
+ const onAbort = () => {
925
+ signal.removeEventListener('abort', onAbort);
926
+ reject(abortError());
927
+ };
928
+ signal.addEventListener('abort', onAbort, { once: true });
929
+ promise.then(
930
+ (value) => {
931
+ signal.removeEventListener('abort', onAbort);
932
+ resolve(value);
933
+ },
934
+ (error) => {
935
+ signal.removeEventListener('abort', onAbort);
936
+ reject(error);
937
+ },
938
+ );
939
+ });
940
+ }
941
+
942
+ async function waitForRetry(waitMs: number, signal?: AbortSignal): Promise<void> {
943
+ if (!signal) {
944
+ await new Promise<void>((resolve) => setTimeout(resolve, waitMs));
945
+ return;
946
+ }
947
+ const abortSignal = signal;
948
+ if (abortSignal.aborted) throw new Error('Embedding API retry aborted');
949
+ await new Promise<void>((resolve, reject) => {
950
+ const timer = setTimeout(done, waitMs);
951
+ const onAbort = () => {
952
+ clearTimeout(timer);
953
+ abortSignal.removeEventListener('abort', onAbort);
954
+ reject(new Error('Embedding API retry aborted'));
955
+ };
956
+ function done() {
957
+ abortSignal.removeEventListener('abort', onAbort);
958
+ resolve();
959
+ }
960
+ abortSignal.addEventListener('abort', onAbort, { once: true });
961
+ });
879
962
  }
@@ -34,6 +34,8 @@ export interface EmbeddingRequestOptions {
34
34
  timeoutMs?: number;
35
35
  /** Disable provider retries for latency-sensitive best-effort work. */
36
36
  retry?: boolean;
37
+ /** Abort the request and any retry wait when the caller's budget expires. */
38
+ signal?: AbortSignal;
37
39
  }
38
40
 
39
41
  export type EmbeddingModality = 'text' | 'image' | 'audio' | 'video' | 'document';
@@ -1259,7 +1259,7 @@ function getAgentRulesContent(agent?: AgentName, scope: 'project' | 'global' = '
1259
1259
  '',
1260
1260
  `Default first step for non-trivial coding work: call \x60memorix_project_context\x60 with the user's actual task before progress files, dev-log reads, ad-hoc file reads, or git archaeology. Memorix will choose a task-lensed brief (bugfix, feature, release, onboarding, refactor, docs, test, or general). When the task is continuing prior work, the same brief also includes a bounded prior-work projection. Treat its "Start here" files as the first ${contextNoun} files to inspect.`,
1261
1261
  '',
1262
- 'If the MCP tool is not visible yet but the client supports tool discovery or dynamic loading, search/select \x60memorix_project_context\x60 first. Continuation fallback is mandatory: when the user asks to continue, resume, take over, or explain prior work and MCP cannot be called in this turn, run exactly one CLI brief with the user\'s real task before inspecting files, Git history, progress notes, or guessing: \x60memorix resume "<task>" --json\x60. For a new task, use \x60memorix context "<task>" --json\x60 instead. The absence of \x60.memorix\x60 or visible memory files never proves project memory is empty. If that one command fails, report it and proceed normally. Do not probe help, enumerate commands, chain broad searches, wait indefinitely on MCP startup, or hand-write tool-call syntax.',
1262
+ 'If the MCP tool is not visible yet but the client supports tool discovery or dynamic loading, search/select \x60memorix_project_context\x60 first. Continuation fallback is mandatory: when the user asks to continue, resume, take over, or explain prior work and MCP cannot be called in this turn, run exactly one CLI brief with the user\'s real task before inspecting files, Git history, progress notes, or guessing: \x60memorix resume "<task>" --brief-json\x60. For a new task, use \x60memorix context "<task>" --brief-json\x60 instead. The absence of \x60.memorix\x60 or visible memory files never proves project memory is empty. Use \x60--json\x60 only when a diagnostic needs the detailed legacy payload. If that one command fails, report it and proceed normally. Do not probe help, enumerate commands, chain broad searches, wait indefinitely on MCP startup, or hand-write tool-call syntax.',
1263
1263
  '',
1264
1264
  'After a successful \x60memorix_project_context\x60 result, the brief is the default retrieval boundary. Do not call more Memorix retrieval tools after a complete brief. Use \x60memorix_context_pack\x60, \x60memorix_search\x60, or \x60memorix_detail\x60 only when the brief lacks a specific reference, freshness field, or fact needed for the task, or when the user explicitly asks for deeper history. In MCP, name that missing fact in \x60purpose\x60 when intentionally expanding beyond the brief. Do not retrieve the same decision twice just to confirm an already-complete brief.',
1265
1265
  'If the user asks for read-only work or says not to modify files, do not call \x60memorix_store\x60 just to record an assessment. Store only when the user explicitly asks to preserve it.',
@@ -31,8 +31,8 @@ export const OFFICIAL_MEMORIX_SKILLS: OfficialMemorixSkill[] = [
31
31
  '',
32
32
  '| Situation | Prefer | CLI fallback |',
33
33
  '|---|---|---|',
34
- '| Starting a coding task and needing the best task-lensed project map | `memorix_project_context` with the user\'s task | `memorix context "<task>" --json` |',
35
- '| Continuing prior project work | `memorix_project_context` with the user\'s task | `memorix resume "<task>" --json` |',
34
+ '| Starting a coding task and needing the best task-lensed project map | `memorix_project_context` with the user\'s task | `memorix context "<task>" --brief-json` |',
35
+ '| Continuing prior project work | `memorix_project_context` with the user\'s task | `memorix resume "<task>" --brief-json` |',
36
36
  '| Need structured refs/freshness for code-bound memories | `memorix_context_pack` | `memorix codegraph context-pack --task "<topic>"` |',
37
37
  '| Explicit memory graph question | `memorix_graph_context` | `memorix memory graph-context --query "<topic>"` |',
38
38
  '| Specific past decision, bug, file, or change | `memorix_search` | `memorix memory search --query "<topic>"` |',
@@ -45,7 +45,7 @@ export const OFFICIAL_MEMORIX_SKILLS: OfficialMemorixSkill[] = [
45
45
  '',
46
46
  '- Default first step for non-trivial coding work: call `memorix_project_context` before progress files, dev-log reads, ad-hoc file reads, or git archaeology. Pass the user\'s actual task so Memorix can choose a bugfix, feature, release, onboarding, refactor, docs, test, or general brief. Start by reading its suggested files.',
47
47
  '- In Claude Code headless/print-mode, an initial MCP `pending` status is not a failure by itself. Prefer MCP first: use the client\'s tool discovery/dynamic loading to find `memorix_project_context` when it is not in the first visible tool list.',
48
- '- Continuation fallback is mandatory: when the user asks to continue, resume, take over, or explain prior work and MCP cannot be called in this turn, run `memorix resume "<task>" --json` exactly once before inspecting files, Git history, progress notes, or guessing. The absence of `.memorix` or visible memory files never proves project memory is empty. For a new task, use `memorix context "<task>" --json` instead. If that one command fails, report it and proceed normally; do not probe help, enumerate commands, or chain broad searches.',
48
+ '- Continuation fallback is mandatory: when the user asks to continue, resume, take over, or explain prior work and MCP cannot be called in this turn, run `memorix resume "<task>" --brief-json` exactly once before inspecting files, Git history, progress notes, or guessing. The absence of `.memorix` or visible memory files never proves project memory is empty. For a new task, use `memorix context "<task>" --brief-json` instead. Use `--json` only when a diagnostic needs the detailed legacy payload. If that one command fails, report it and proceed normally; do not probe help, enumerate commands, or chain broad searches.',
49
49
  '- After a successful `memorix_project_context`, the brief is the default retrieval boundary. Do not call more Memorix retrieval tools after a complete brief. Use Context Pack, search, or detail only when the brief lacks a specific reference, freshness field, or fact needed for the task, or when the user explicitly asks for deeper history. In MCP, name that missing fact in `purpose` when intentionally expanding beyond the brief. Do not retrieve the same decision twice just to confirm an already-complete brief.',
50
50
  '- If the user asks for read-only work or says not to modify files, do not call `memorix_store` just to record an assessment. Store only when the user explicitly asks to preserve it.',
51
51
  '- Use `memorix_search` for a specific decision, bug, file, or prior change that the project context did not answer. Fetch detail only for a result you still need after the brief.',
@@ -7,7 +7,7 @@ You have access to Memorix memory tools. Follow these rules to maintain persiste
7
7
  At the beginning of non-trivial coding work, use Memorix Memory Autopilot before local progress notes or broad file exploration. A session bind is not required for every conversation.
8
8
 
9
9
  1. Default first step for non-trivial coding work: call `memorix_project_context` with the user's actual task before progress files, dev-log reads, ad-hoc file reads, or git archaeology. Memorix will choose a task-lensed brief (bugfix, feature, release, onboarding, refactor, docs, test, or general). Treat its "Start here" files as the first code or docs to inspect.
10
- 2. If the MCP tool is not visible yet but the client supports tool discovery or dynamic loading, search/select `memorix_project_context` first. Continuation fallback is mandatory: when the user asks to continue, resume, take over, or explain prior work and MCP cannot be called in this turn, run exactly one CLI brief with the user's real task before inspecting files, Git history, progress notes, or guessing: use `memorix resume "<task>" --json`. For a new task, use `memorix context "<task>" --json` instead. The absence of `.memorix` or visible memory files never proves project memory is empty. If that one command fails, report it and proceed normally. Do not probe help, enumerate commands, chain broad searches, wait indefinitely on MCP startup, or hand-write tool-call syntax.
10
+ 2. If the MCP tool is not visible yet but the client supports tool discovery or dynamic loading, search/select `memorix_project_context` first. Continuation fallback is mandatory: when the user asks to continue, resume, take over, or explain prior work and MCP cannot be called in this turn, run exactly one CLI brief with the user's real task before inspecting files, Git history, progress notes, or guessing: use `memorix resume "<task>" --brief-json`. For a new task, use `memorix context "<task>" --brief-json` instead. The absence of `.memorix` or visible memory files never proves project memory is empty. If that one command fails, report it and proceed normally. Do not probe help, enumerate commands, chain broad searches, wait indefinitely on MCP startup, or hand-write tool-call syntax. Use `--json` only when a diagnostic needs the detailed legacy payload.
11
11
  3. After a successful `memorix_project_context` result, the brief is the default retrieval boundary. Do not call more Memorix retrieval tools after a complete brief. Use `memorix_context_pack`, `memorix_search`, or `memorix_detail` only when the brief lacks a specific reference, freshness field, or fact needed for the task, or when the user explicitly asks for deeper history. In MCP, name that missing fact in `purpose` when intentionally expanding beyond the brief. Do not retrieve the same decision twice just to confirm an already-complete brief.
12
12
  If the user asks for read-only work or says not to modify files, do not call `memorix_store` just to record an assessment. Store only when the user explicitly asks to preserve it.
13
13
  4. Use `memorix_context_pack` when you need structured refs and freshness for code-bound memories.
@@ -0,0 +1,41 @@
1
+ import type { CodeGraphProviderQuality } from '../codegraph/types.js';
2
+ import type { TaskWorkset } from './workset.js';
3
+
4
+ /**
5
+ * A small machine-readable counterpart to the agent-facing Workset prompt.
6
+ * Detailed CLI JSON remains available for diagnostics; this shape is for
7
+ * clients that need a bounded handoff without loading raw stores.
8
+ */
9
+ export interface BoundedContextReceipt {
10
+ schemaVersion: '1';
11
+ task: string;
12
+ brief: string;
13
+ receipt: TaskWorkset['receipt'];
14
+ code: {
15
+ selected: CodeGraphProviderQuality['selected'];
16
+ quality: CodeGraphProviderQuality['selectedQuality'];
17
+ externalState: CodeGraphProviderQuality['external']['state'];
18
+ snapshotId?: string;
19
+ };
20
+ loadout?: TaskWorkset['agentLoadout'];
21
+ }
22
+
23
+ export function buildBoundedContextReceipt(input: {
24
+ workset: TaskWorkset;
25
+ providerQuality?: CodeGraphProviderQuality;
26
+ }): BoundedContextReceipt {
27
+ const provider = input.providerQuality ?? input.workset.provenance.codeProvider;
28
+ return {
29
+ schemaVersion: '1',
30
+ task: input.workset.task,
31
+ brief: input.workset.prompt,
32
+ receipt: input.workset.receipt,
33
+ code: {
34
+ selected: provider?.selected ?? 'lite',
35
+ quality: provider?.selectedQuality ?? 'heuristic',
36
+ externalState: provider?.external.state ?? 'not-detected',
37
+ ...(input.workset.provenance.snapshotId ? { snapshotId: input.workset.provenance.snapshotId } : {}),
38
+ },
39
+ ...(input.workset.agentLoadout ? { loadout: input.workset.agentLoadout } : {}),
40
+ };
41
+ }
@@ -22,7 +22,7 @@ import type {
22
22
  } from './workflow-types.js';
23
23
  import type { KnowledgeWorkspace } from './workspace-types.js';
24
24
 
25
- const KNOWN_AGENTS: AgentTarget[] = [
25
+ export const WORKFLOW_AGENT_TARGETS: AgentTarget[] = [
26
26
  'windsurf',
27
27
  'cursor',
28
28
  'claude-code',
@@ -117,7 +117,7 @@ function optionalStatus(data: Record<string, unknown>): WorkflowStatus {
117
117
  }
118
118
 
119
119
  function normalizeAgents(values: string[]): AgentTarget[] {
120
- const unknown = values.filter(value => !KNOWN_AGENTS.includes(value as AgentTarget));
120
+ const unknown = values.filter(value => !WORKFLOW_AGENT_TARGETS.includes(value as AgentTarget));
121
121
  if (unknown.length > 0) {
122
122
  throw new Error('workflow frontmatter has unsupported agent: ' + unknown[0]);
123
123
  }
@@ -459,12 +459,15 @@ function taskScore(workflow: WorkflowSpec, task: string): { score: number; reaso
459
459
  export function selectWorkflows(input: {
460
460
  workflows: WorkflowSpec[];
461
461
  task: string;
462
+ /** Only select workflows that explicitly support the target agent. */
463
+ agent?: AgentTarget;
462
464
  projectId?: string;
463
465
  store?: WorkflowStore;
464
466
  limit?: number;
465
467
  }): WorkflowSelection[] {
466
468
  const selected = input.workflows
467
- .filter(workflow => workflow.status === 'active')
469
+ .filter(workflow => workflow.status === 'active'
470
+ && (!input.agent || workflow.compatibleAgents.includes(input.agent)))
468
471
  .map(workflow => {
469
472
  const match = taskScore(workflow, input.task);
470
473
  const cautions = input.store && input.projectId