amalgm 0.1.107 → 0.1.108

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amalgm",
3
- "version": "0.1.107",
3
+ "version": "0.1.108",
4
4
  "description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -29,13 +29,13 @@
29
29
  "@homebridge/node-pty-prebuilt-multiarch": "^0.13.1",
30
30
  "@modelcontextprotocol/sdk": "^1.29.0",
31
31
  "@openai/codex": "^0.128.0",
32
- "@opencode-ai/sdk": "^1.17.5",
32
+ "@opencode-ai/sdk": "^1.17.6",
33
33
  "agent-browser": "^0.27.3",
34
34
  "ai": "^6.0.116",
35
35
  "better-sqlite3": "^12.10.0",
36
36
  "cron-parser": "^5.5.0",
37
37
  "ffmpeg-static": "^5.3.0",
38
- "opencode-ai": "^1.17.5",
38
+ "opencode-ai": "^1.17.6",
39
39
  "tsx": "^4.21.0",
40
40
  "ws": "^8.18.3",
41
41
  "zod": "^4.1.8"
@@ -3,11 +3,14 @@
3
3
  /**
4
4
  * Project context paths + pure text helpers.
5
5
  *
6
- * Every registered workspace carries `<project>/.amalgm/context/` with three
6
+ * Every registered workspace carries `<project>/.amalgm/context/` with four
7
7
  * markdown docs:
8
- * instructions.md user-authored durable instructions
9
- * change-log.md append-only operational history, newest entry on top
10
- * project-state.md consolidated "where the project stands" snapshot
8
+ * project-instructions.md user-authored durable project instructions
9
+ * project-goals.md durable goals and success criteria
10
+ * project-state.md consolidated "where the project stands" snapshot
11
+ * change-log.md append-only operational history, newest entry on top
12
+ *
13
+ * The machine-wide context is `<AMALGM_DIR>/context/system-instructions.md`.
11
14
  *
12
15
  * This module is dependency-free (no db/event imports) so chat-core and tests
13
16
  * can use the path/text helpers without pulling in the store.
@@ -16,13 +19,18 @@
16
19
  const path = require('path');
17
20
 
18
21
  const CONTEXT_DIR_SEGMENTS = ['.amalgm', 'context'];
19
- const INSTRUCTIONS_FILENAME = 'instructions.md';
22
+ const LEGACY_INSTRUCTIONS_FILENAME = 'instructions.md';
23
+ const SYSTEM_INSTRUCTIONS_FILENAME = 'system-instructions.md';
24
+ const PROJECT_INSTRUCTIONS_FILENAME = 'project-instructions.md';
25
+ const PROJECT_GOALS_FILENAME = 'project-goals.md';
20
26
  const CHANGE_LOG_FILENAME = 'change-log.md';
21
27
  const PROJECT_STATE_FILENAME = 'project-state.md';
22
28
 
23
29
  const CHANGE_LOG_TITLE = '# Change Log';
24
30
  const PROJECT_STATE_TITLE = '# Project State';
25
- const INSTRUCTIONS_TITLE = '# Instructions';
31
+ const PROJECT_GOALS_TITLE = '# Project Goals';
32
+ const PROJECT_INSTRUCTIONS_TITLE = '# Project Instructions';
33
+ const SYSTEM_INSTRUCTIONS_TITLE = '# System Instructions';
26
34
 
27
35
  // "Roughly the last 2,000 words" of history, kept deterministic in chars.
28
36
  const CHANGE_LOG_CONTEXT_MAX_CHARS = 12_000;
@@ -36,18 +44,34 @@ function contextRoot(projectPath) {
36
44
  return path.join(path.resolve(projectPath), ...CONTEXT_DIR_SEGMENTS);
37
45
  }
38
46
 
47
+ function systemContextRoot(amalgmDir) {
48
+ return path.join(path.resolve(amalgmDir), 'context');
49
+ }
50
+
51
+ function systemContextPaths(amalgmDir) {
52
+ const root = systemContextRoot(amalgmDir);
53
+ return {
54
+ root,
55
+ instructions: path.join(root, SYSTEM_INSTRUCTIONS_FILENAME),
56
+ };
57
+ }
58
+
39
59
  function contextPaths(projectPath) {
40
60
  const root = contextRoot(projectPath);
41
61
  return {
42
62
  root,
43
- instructions: path.join(root, INSTRUCTIONS_FILENAME),
63
+ instructions: path.join(root, PROJECT_INSTRUCTIONS_FILENAME),
64
+ goals: path.join(root, PROJECT_GOALS_FILENAME),
44
65
  changeLog: path.join(root, CHANGE_LOG_FILENAME),
45
66
  projectState: path.join(root, PROJECT_STATE_FILENAME),
46
67
  };
47
68
  }
48
69
 
49
70
  const CONTEXT_DOC_FILENAMES = new Set([
50
- INSTRUCTIONS_FILENAME,
71
+ LEGACY_INSTRUCTIONS_FILENAME,
72
+ SYSTEM_INSTRUCTIONS_FILENAME,
73
+ PROJECT_INSTRUCTIONS_FILENAME,
74
+ PROJECT_GOALS_FILENAME,
51
75
  CHANGE_LOG_FILENAME,
52
76
  PROJECT_STATE_FILENAME,
53
77
  ]);
@@ -246,10 +270,15 @@ module.exports = {
246
270
  CHANGE_LOG_FILENAME,
247
271
  CHANGE_LOG_TITLE,
248
272
  CONTEXT_FILE_MAX_CHARS,
249
- INSTRUCTIONS_FILENAME,
250
- INSTRUCTIONS_TITLE,
273
+ LEGACY_INSTRUCTIONS_FILENAME,
274
+ PROJECT_GOALS_FILENAME,
275
+ PROJECT_GOALS_TITLE,
276
+ PROJECT_INSTRUCTIONS_FILENAME,
277
+ PROJECT_INSTRUCTIONS_TITLE,
251
278
  PROJECT_STATE_FILENAME,
252
279
  PROJECT_STATE_TITLE,
280
+ SYSTEM_INSTRUCTIONS_FILENAME,
281
+ SYSTEM_INSTRUCTIONS_TITLE,
253
282
  cappedDocContent,
254
283
  changeLogEntriesSince,
255
284
  changeLogEntryHeading,
@@ -263,4 +292,6 @@ module.exports = {
263
292
  localTimestamp,
264
293
  projectPathForContextPath,
265
294
  sanitizeChangeLogBody,
295
+ systemContextPaths,
296
+ systemContextRoot,
266
297
  };
@@ -9,7 +9,7 @@
9
9
  * contextRevision each session last saw (local_meta), and when the canonical
10
10
  * row moved, the next turn gets a compact <project_context_update> prepended.
11
11
  * Because the change log is append-only, the delta is cheap: entries newer
12
- * than the last-seen entry timestamp, plus full project-state/instructions
12
+ * than the last-seen entry timestamp, plus full project goals/state/instructions
13
13
  * only when their revisions changed.
14
14
  *
15
15
  * Invariant: the prompt for turn N reflects every context write committed
@@ -71,6 +71,7 @@ function writeSeen(sessionId, record) {
71
71
  projectId: record.projectId,
72
72
  contextRevision: record.contextRevision,
73
73
  instructionsRevision: record.files?.instructions?.revision || null,
74
+ projectGoalsRevision: record.files?.goals?.revision || null,
74
75
  projectStateRevision: record.files?.projectState?.revision || null,
75
76
  changeLogRevision: record.files?.changeLog?.revision || null,
76
77
  latestEntryAtMs: record.files?.changeLog?.latestEntryAtMs ?? null,
@@ -96,8 +97,9 @@ function memoryProtocolBlock() {
96
97
  'Project memory is maintained through the memories tool — never by editing these files directly.',
97
98
  '- When you complete durable, user-visible work in this project, or make decisions future agents need, call toolbox__memories_append_change_log with a concise markdown body: 3-6 factual bullets such as "- Completed: ...", "- Decisions: ...", "- Notes: ...". No transcripts, no essays; mark uncertain items as open questions.',
98
99
  '- The runtime owns timestamps and entry placement. Never write timestamps, never rewrite change-log.md — it is append-only.',
100
+ '- Call toolbox__memories_write_project_goals when durable goals, success criteria, or intended outcomes have intentionally changed. Goals are desired future state, not progress notes.',
99
101
  '- Call toolbox__memories_write_project_state only when the consolidated "where this project stands" snapshot must be rewritten (e.g. after a milestone or when consolidating the change log).',
100
- '- instructions.md is user-authored. Do not modify it unless the user explicitly asks.',
102
+ '- project-instructions.md is user-authored. Do not modify it unless the user explicitly asks.',
101
103
  '</project_memory_protocol>',
102
104
  ].join('\n');
103
105
  }
@@ -111,6 +113,13 @@ function contentBlocks(record) {
111
113
  '</project_instructions>',
112
114
  ].join('\n'));
113
115
  }
116
+ if (record.prompt?.goals) {
117
+ blocks.push([
118
+ `<project_goals path="${escapeAttribute(record.paths?.goals)}">`,
119
+ record.prompt.goals,
120
+ '</project_goals>',
121
+ ].join('\n'));
122
+ }
114
123
  if (record.prompt?.projectState) {
115
124
  blocks.push([
116
125
  `<project_state path="${escapeAttribute(record.paths?.projectState)}">`,
@@ -140,7 +149,7 @@ function projectContextPromptBlock(contract) {
140
149
  const lines = [
141
150
  '<project_context>',
142
151
  `Project: ${record.name} (${record.projectPath})`,
143
- 'This is the project\'s durable memory. The change log is operational history (newest first); project state is the consolidated snapshot of where things stand; instructions are user-authored guidance for this project.',
152
+ 'This is the project\'s durable memory. Project instructions are user-authored guidance; project goals are intended outcomes; project state is the current consolidated snapshot; the change log is operational history (newest first).',
144
153
  '',
145
154
  ...contentBlocks(record).flatMap((block) => [block, '']),
146
155
  memoryProtocolBlock(),
@@ -187,6 +196,13 @@ function projectContextTurnUpdateBlock(contract) {
187
196
  '</project_instructions>',
188
197
  ].join('\n'));
189
198
  }
199
+ if (seen.projectGoalsRevision !== record.files?.goals?.revision && record.prompt?.goals) {
200
+ blocks.push([
201
+ `<project_goals path="${escapeAttribute(record.paths?.goals)}">`,
202
+ record.prompt.goals,
203
+ '</project_goals>',
204
+ ].join('\n'));
205
+ }
190
206
  if (seen.projectStateRevision !== record.files?.projectState?.revision && record.prompt?.projectState) {
191
207
  blocks.push([
192
208
  `<project_state path="${escapeAttribute(record.paths?.projectState)}">`,
@@ -219,7 +235,7 @@ function projectContextTurnUpdateBlock(contract) {
219
235
 
220
236
  return [
221
237
  '<project_context_update>',
222
- `Project context for ${record.name} (${record.projectPath}) changed since the last turn. Current state:`,
238
+ `Project context for ${record.name} (${record.projectPath}) changed since the last turn. Current memory:`,
223
239
  '',
224
240
  ...blocks.flatMap((block) => [block, '']),
225
241
  '</project_context_update>',
@@ -87,6 +87,7 @@ function writeDocHandler(writeFn) {
87
87
  }
88
88
 
89
89
  const handleWriteProjectState = writeDocHandler(store.writeProjectState);
90
+ const handleWriteProjectGoals = writeDocHandler(store.writeProjectGoals);
90
91
  const handleWriteInstructions = writeDocHandler(store.writeInstructions);
91
92
  const handleWriteChangeLog = writeDocHandler(store.writeChangeLog);
92
93
 
@@ -96,5 +97,6 @@ module.exports = {
96
97
  handleList,
97
98
  handleWriteChangeLog,
98
99
  handleWriteInstructions,
100
+ handleWriteProjectGoals,
99
101
  handleWriteProjectState,
100
102
  };
@@ -7,10 +7,12 @@
7
7
  * store so timestamps, revisions, the SQLite `project_context` row, and Local
8
8
  * Live events stay coherent:
9
9
  *
10
- * change-log.md append-only via appendChangeLog (runtime owns the
11
- * timestamp; entries land directly under the title)
12
- * project-state.md latest write wins; revision kept as metadata only
13
- * instructions.md user-authored; latest write wins via the UI path
10
+ * change-log.md append-only via appendChangeLog (runtime owns the
11
+ * timestamp; entries land directly under the title)
12
+ * project-goals.md latest write wins; writable by UI and memories tool
13
+ * project-state.md latest write wins; revision kept as metadata only
14
+ * project-instructions.md user-authored; latest write wins via the UI path
15
+ * system-instructions.md global user-authored instructions under AMALGM_DIR
14
16
  *
15
17
  * Direct disk edits (editors, agents with their own file tools) are picked up
16
18
  * by the per-project fs watcher and the fs REST hooks, which funnel into the
@@ -32,8 +34,11 @@ const { AMALGM_DIR } = require('../config');
32
34
  const { canonicalProjectPath } = require('../../lib/project-paths');
33
35
  const {
34
36
  CHANGE_LOG_TITLE,
35
- INSTRUCTIONS_TITLE,
37
+ LEGACY_INSTRUCTIONS_FILENAME,
38
+ PROJECT_GOALS_TITLE,
39
+ PROJECT_INSTRUCTIONS_TITLE,
36
40
  PROJECT_STATE_TITLE,
41
+ SYSTEM_INSTRUCTIONS_TITLE,
37
42
  changeLogEntryHeading,
38
43
  changeLogRecentWindow,
39
44
  cappedDocContent,
@@ -44,6 +49,7 @@ const {
44
49
  localTimestamp,
45
50
  projectPathForContextPath,
46
51
  sanitizeChangeLogBody,
52
+ systemContextPaths: systemContextPathsForAmalgmDir,
47
53
  } = require('./paths');
48
54
 
49
55
  const RESOURCE = 'project_context';
@@ -59,10 +65,7 @@ function systemProject() {
59
65
  }
60
66
 
61
67
  function systemContextPaths() {
62
- // AMALGM_DIR *is* the .amalgm dir, so the system context sits directly
63
- // under it — no extra .amalgm segment like project contexts have.
64
- const root = path.join(path.resolve(AMALGM_DIR), 'context');
65
- return { root, instructions: path.join(root, 'instructions.md') };
68
+ return systemContextPathsForAmalgmDir(AMALGM_DIR);
66
69
  }
67
70
 
68
71
  function isSystemProject(project) {
@@ -187,7 +190,7 @@ function readDoc(filePath) {
187
190
  }
188
191
 
189
192
  function statFingerprint(paths) {
190
- return [paths.instructions, paths.changeLog, paths.projectState]
193
+ return [paths.instructions, paths.goals, paths.changeLog, paths.projectState]
191
194
  .filter(Boolean)
192
195
  .map((filePath) => {
193
196
  try {
@@ -235,11 +238,12 @@ function buildContextRecord(project) {
235
238
 
236
239
  const paths = contextPaths(project.path);
237
240
  const instructions = readDoc(paths.instructions);
241
+ const goals = readDoc(paths.goals);
238
242
  const changeLog = readDoc(paths.changeLog);
239
243
  const projectState = readDoc(paths.projectState);
240
244
  const latestEntry = latestChangeLogEntry(changeLog.content);
241
245
  const contextRevision = hash(
242
- [instructions.revision, changeLog.revision, projectState.revision].join(':'),
246
+ [instructions.revision, goals.revision, changeLog.revision, projectState.revision].join(':'),
243
247
  ).slice(0, 20);
244
248
 
245
249
  return {
@@ -253,6 +257,7 @@ function buildContextRecord(project) {
253
257
  paths,
254
258
  files: {
255
259
  instructions: docMeta(instructions),
260
+ goals: docMeta(goals),
256
261
  changeLog: {
257
262
  ...docMeta(changeLog),
258
263
  latestEntryAt: latestEntry.iso,
@@ -262,6 +267,7 @@ function buildContextRecord(project) {
262
267
  },
263
268
  prompt: {
264
269
  instructions: instructions.empty ? '' : cappedDocContent(instructions.content),
270
+ goals: goals.empty ? '' : cappedDocContent(goals.content),
265
271
  projectState: projectState.empty ? '' : cappedDocContent(projectState.content),
266
272
  changeLogRecent: changeLog.empty ? '' : changeLogRecentWindow(changeLog.content),
267
273
  },
@@ -346,24 +352,40 @@ function reconcileProjectContext(input, options = {}) {
346
352
  return upsertContextRow(record, options);
347
353
  }
348
354
 
349
- const INSTRUCTIONS_TEMPLATE = `${INSTRUCTIONS_TITLE}\n`;
355
+ const SYSTEM_INSTRUCTIONS_TEMPLATE = `${SYSTEM_INSTRUCTIONS_TITLE}\n`;
356
+ const PROJECT_INSTRUCTIONS_TEMPLATE = `${PROJECT_INSTRUCTIONS_TITLE}\n`;
357
+ const PROJECT_GOALS_TEMPLATE = `${PROJECT_GOALS_TITLE}\n`;
350
358
  const CHANGE_LOG_TEMPLATE = `${CHANGE_LOG_TITLE}\n`;
351
359
  const PROJECT_STATE_TEMPLATE = `${PROJECT_STATE_TITLE}\n`;
352
360
 
361
+ function legacyInstructionsPath(paths) {
362
+ return path.join(paths.root, LEGACY_INSTRUCTIONS_FILENAME);
363
+ }
364
+
365
+ function writeInitialDoc(filePath, template, legacyPath = null) {
366
+ if (fs.existsSync(filePath)) return false;
367
+ if (legacyPath && fs.existsSync(legacyPath)) {
368
+ atomicWrite(filePath, fs.readFileSync(legacyPath, 'utf8'));
369
+ } else {
370
+ atomicWrite(filePath, template);
371
+ }
372
+ return true;
373
+ }
374
+
353
375
  function ensureContextFiles(project) {
354
376
  const paths = contextPathsFor(project);
355
377
  const docs = isSystemProject(project)
356
- ? [[paths.instructions, INSTRUCTIONS_TEMPLATE]]
378
+ ? [[paths.instructions, SYSTEM_INSTRUCTIONS_TEMPLATE, legacyInstructionsPath(paths)]]
357
379
  : [
358
- [paths.instructions, INSTRUCTIONS_TEMPLATE],
359
- [paths.changeLog, CHANGE_LOG_TEMPLATE],
380
+ [paths.instructions, PROJECT_INSTRUCTIONS_TEMPLATE, legacyInstructionsPath(paths)],
381
+ [paths.goals, PROJECT_GOALS_TEMPLATE],
360
382
  [paths.projectState, PROJECT_STATE_TEMPLATE],
383
+ [paths.changeLog, CHANGE_LOG_TEMPLATE],
361
384
  ];
362
385
  const created = [];
363
386
  fs.mkdirSync(paths.root, { recursive: true });
364
- for (const [filePath, template] of docs) {
365
- if (!fs.existsSync(filePath)) {
366
- atomicWrite(filePath, template);
387
+ for (const [filePath, template, legacyPath] of docs) {
388
+ if (writeInitialDoc(filePath, template, legacyPath)) {
367
389
  created.push(filePath);
368
390
  }
369
391
  }
@@ -455,7 +477,7 @@ function appendChangeLog(inputPath, body, options = {}) {
455
477
  throw new Error(`No registered Amalgm project contains: ${cleanString(typeof inputPath === 'string' ? inputPath : inputPath?.path) || '(no path)'}`);
456
478
  }
457
479
  if (isSystemProject(project)) {
458
- throw new Error('The system context has instructions only — change logs are per-project.');
480
+ throw new Error('The system context has system-instructions.md only — change logs are per-project.');
459
481
  }
460
482
  const entryBody = sanitizeChangeLogBody(body);
461
483
  if (!entryBody) throw new Error('Change log entry body is required');
@@ -485,7 +507,7 @@ function writeContextDoc(inputPath, docKey, content, options = {}) {
485
507
  throw new Error(`No registered Amalgm project contains: ${cleanString(typeof inputPath === 'string' ? inputPath : inputPath?.path) || '(no path)'}`);
486
508
  }
487
509
  if (isSystemProject(project) && docKey !== 'instructions') {
488
- throw new Error('The system context has instructions only — change logs and project state are per-project.');
510
+ throw new Error('The system context has system-instructions.md only — project goals, project state, and change logs are per-project.');
489
511
  }
490
512
  if (typeof content !== 'string') throw new Error('content must be a string');
491
513
  ensureContextFiles(project);
@@ -500,6 +522,13 @@ function writeContextDoc(inputPath, docKey, content, options = {}) {
500
522
  return { project, record };
501
523
  }
502
524
 
525
+ /** project-goals.md: latest write wins; writable by UI and the memories tool. */
526
+ function writeProjectGoals(inputPath, content, options = {}) {
527
+ return writeContextDoc(inputPath, 'goals', content, {
528
+ source: options.source || 'project-context:write-project-goals',
529
+ });
530
+ }
531
+
503
532
  /** project-state.md: latest write wins; revision is metadata, not a lock. */
504
533
  function writeProjectState(inputPath, content, options = {}) {
505
534
  return writeContextDoc(inputPath, 'projectState', content, {
@@ -507,7 +536,7 @@ function writeProjectState(inputPath, content, options = {}) {
507
536
  });
508
537
  }
509
538
 
510
- /** instructions.md: user-authored; latest write wins via the UI path. */
539
+ /** project/system instructions: user-authored; latest write wins via the UI path. */
511
540
  function writeInstructions(inputPath, content, options = {}) {
512
541
  return writeContextDoc(inputPath, 'instructions', content, {
513
542
  source: options.source || 'project-context:write-instructions',
@@ -667,5 +696,6 @@ module.exports = {
667
696
  watchProjectContext,
668
697
  writeChangeLog,
669
698
  writeInstructions,
699
+ writeProjectGoals,
670
700
  writeProjectState,
671
701
  };
@@ -72,6 +72,42 @@ module.exports = [
72
72
  }
73
73
  },
74
74
  },
75
+ {
76
+ name: 'write_project_goals',
77
+ description:
78
+ 'Rewrite the active project\'s goals document (project-goals.md). Use this when durable goals, intended outcomes, success criteria, or explicit priorities have changed. Latest write wins; submit the COMPLETE new document as markdown. Keep goals distinct from progress: use project-state.md for current status and append_change_log for event history.',
79
+ inputSchema: {
80
+ type: 'object',
81
+ properties: {
82
+ content: {
83
+ type: 'string',
84
+ description: 'Full markdown content of the new project-goals.md (start with "# Project Goals").',
85
+ },
86
+ projectPath: PROJECT_PATH_INPUT,
87
+ },
88
+ required: ['content'],
89
+ },
90
+ async handler({ content, projectPath } = {}, context = {}) {
91
+ const target = targetPathFor({ projectPath }, context);
92
+ if (!target) return noProjectError('');
93
+ if (typeof content !== 'string' || !content.trim()) {
94
+ return errorResult('content is required (full markdown for project-goals.md)');
95
+ }
96
+ try {
97
+ const result = store.writeProjectGoals(target, content, {
98
+ source: 'project-context:tool',
99
+ });
100
+ return textResult(
101
+ `Project goals updated for ${result.project.name} (revision ${result.record?.files?.goals?.revision || 'unknown'}).\n`
102
+ + `File: ${result.record?.paths?.goals || ''}`,
103
+ );
104
+ } catch (error) {
105
+ const message = error instanceof Error ? error.message : String(error);
106
+ if (message.startsWith('No registered Amalgm project')) return noProjectError(target);
107
+ return errorResult(message);
108
+ }
109
+ },
110
+ },
75
111
  {
76
112
  name: 'write_project_state',
77
113
  description:
@@ -111,7 +147,7 @@ module.exports = [
111
147
  {
112
148
  name: 'read_project_context',
113
149
  description:
114
- 'Read the active project\'s memory: instructions.md (user-authored), project-state.md (consolidated snapshot), and the recent change-log.md window (newest first). Use when you need to re-check project memory mid-session or inspect another project\'s context via projectPath.',
150
+ 'Read the active project\'s memory: project-instructions.md (user-authored), project-goals.md (intended outcomes), project-state.md (consolidated snapshot), and the recent change-log.md window (newest first). Use when you need to re-check project memory mid-session or inspect another project\'s context via projectPath.',
115
151
  inputSchema: {
116
152
  type: 'object',
117
153
  properties: {
@@ -128,9 +164,12 @@ module.exports = [
128
164
  `Project: ${record.name} (${record.projectPath})`,
129
165
  `Context revision: ${record.contextRevision}`,
130
166
  '',
131
- '## instructions.md',
167
+ '## project-instructions.md',
132
168
  record.prompt?.instructions || '(empty)',
133
169
  '',
170
+ '## project-goals.md',
171
+ record.prompt?.goals || '(empty)',
172
+ '',
134
173
  '## project-state.md',
135
174
  record.prompt?.projectState || '(empty)',
136
175
  '',
@@ -43,7 +43,7 @@ const CORE_TOOL_GROUPS = [
43
43
  group(
44
44
  'memories',
45
45
  'Memories',
46
- 'Maintain project memory: append change-log entries and keep the project-state snapshot current.',
46
+ 'Maintain project memory: append change-log entries and keep project goals/state current.',
47
47
  require('../project-context/tools'),
48
48
  ),
49
49
  group(
@@ -23,6 +23,10 @@ async function handleProjectContextRoutes(ctx) {
23
23
  await projectContextRest.handleWriteProjectState(await ctx.readJsonBody(), ctx.sendJson);
24
24
  return true;
25
25
  }
26
+ if (ctx.pathname === '/project-context/project-goals' && ctx.method === 'PUT') {
27
+ await projectContextRest.handleWriteProjectGoals(await ctx.readJsonBody(), ctx.sendJson);
28
+ return true;
29
+ }
26
30
  if (ctx.pathname === '/project-context/instructions' && ctx.method === 'PUT') {
27
31
  await projectContextRest.handleWriteInstructions(await ctx.readJsonBody(), ctx.sendJson);
28
32
  return true;
@@ -27,6 +27,7 @@ test('core tool grouping preserves handler definitions for MCP surface wrapping'
27
27
  assert.equal(CORE_TOOLS.some((tool) => tool.name === 'open' && tool.toolboxToolId === 'browser'), true);
28
28
  assert.equal(CORE_TOOLS.some((tool) => tool.name === 'notify_user'), true);
29
29
  assert.equal(CORE_TOOLS.some((tool) => tool.name === 'append_change_log'), true);
30
+ assert.equal(CORE_TOOLS.some((tool) => tool.name === 'write_project_goals'), true);
30
31
  });
31
32
 
32
33
  test('core tool actions carry stable toolbox ids', () => {
@@ -41,7 +41,7 @@ test.after(() => {
41
41
  fs.rmSync(tempRoot, { recursive: true, force: true });
42
42
  });
43
43
 
44
- test('ensureProjectContext creates the three docs and a row, additively', () => {
44
+ test('ensureProjectContext creates project docs and a row, additively', () => {
45
45
  const { projectPath, workspace } = makeProject('ensure');
46
46
  const preexisting = path.join(projectPath, 'keep.txt');
47
47
  fs.writeFileSync(preexisting, 'keep me');
@@ -50,15 +50,27 @@ test('ensureProjectContext creates the three docs and a row, additively', () =>
50
50
 
51
51
  assert.deepEqual(
52
52
  fs.readdirSync(path.join(projectPath, '.amalgm', 'context')).sort(),
53
- ['change-log.md', 'instructions.md', 'project-state.md'],
53
+ ['change-log.md', 'project-goals.md', 'project-instructions.md', 'project-state.md'],
54
54
  );
55
55
  assert.equal(fs.readFileSync(preexisting, 'utf8'), 'keep me');
56
56
  assert.equal(record.projectId, workspace.id);
57
57
  assert.ok(record.contextRevision);
58
58
  // Existing content is never clobbered by a second ensure.
59
- fs.writeFileSync(path.join(projectPath, '.amalgm/context/instructions.md'), '# Instructions\n\nKeep these.\n');
59
+ fs.writeFileSync(path.join(projectPath, '.amalgm/context/project-instructions.md'), '# Project Instructions\n\nKeep these.\n');
60
60
  store.ensureProjectContext(workspace, { source: 'test' });
61
- assert.match(readContextFile(projectPath, 'instructions.md'), /Keep these\./);
61
+ assert.match(readContextFile(projectPath, 'project-instructions.md'), /Keep these\./);
62
+ });
63
+
64
+ test('ensureProjectContext migrates legacy instructions.md without deleting it', () => {
65
+ const { projectPath, workspace } = makeProject('legacy-instructions');
66
+ const contextDir = path.join(projectPath, '.amalgm', 'context');
67
+ fs.mkdirSync(contextDir, { recursive: true });
68
+ fs.writeFileSync(path.join(contextDir, 'instructions.md'), '# Instructions\n\nLegacy project guidance.\n');
69
+
70
+ store.ensureProjectContext(workspace, { source: 'test' });
71
+
72
+ assert.match(readContextFile(projectPath, 'project-instructions.md'), /Legacy project guidance\./);
73
+ assert.match(readContextFile(projectPath, 'instructions.md'), /Legacy project guidance\./);
62
74
  });
63
75
 
64
76
  test('appendChangeLog stamps runtime timestamps and prepends under the title', () => {
@@ -115,6 +127,23 @@ test('writeProjectState is latest-write-wins with revision metadata', () => {
115
127
  assert.notEqual(first.record.contextRevision, second.record.contextRevision);
116
128
  });
117
129
 
130
+ test('writeProjectGoals is latest-write-wins and separate from project state', () => {
131
+ const { projectPath, workspace } = makeProject('goals');
132
+ store.ensureProjectContext(workspace);
133
+
134
+ const first = store.writeProjectGoals(projectPath, '# Project Goals\n\nDraft goal.');
135
+ const second = store.writeProjectGoals(projectPath, '# Project Goals\n\nFinal goal.');
136
+ const record = store.getProjectContext(projectPath, { reconcile: false });
137
+
138
+ assert.equal(readContextFile(projectPath, 'project-goals.md'), '# Project Goals\n\nFinal goal.\n');
139
+ assert.equal(record.prompt.goals.includes('Final goal.'), true);
140
+ assert.equal(record.prompt.projectState, '');
141
+ assert.notEqual(
142
+ first.record.files.goals.revision,
143
+ second.record.files.goals.revision,
144
+ );
145
+ });
146
+
118
147
  test('reconcile is idempotent and catches out-of-band edits', () => {
119
148
  const { projectPath, workspace } = makeProject('reconcile');
120
149
  store.ensureProjectContext(workspace);
@@ -180,14 +209,17 @@ test('fresh threads get the full prompt block; resumed threads get deltas', asyn
180
209
  const { projectPath, workspace } = makeProject('prompt');
181
210
  store.ensureProjectContext(workspace);
182
211
  store.appendChangeLog(projectPath, '- Completed: baseline work.');
212
+ store.writeProjectGoals(projectPath, '# Project Goals\n\nShip the memory system.');
183
213
  store.writeProjectState(projectPath, '# Project State\n\nBaseline state.');
184
214
 
185
215
  const fresh = { sessionId: 'sess-prompt', cwd: projectPath, providerSessionId: null };
186
216
  const block = prompt.projectContextPromptBlock(fresh);
187
217
  assert.ok(block.includes('<project_context>'));
188
218
  assert.ok(block.includes('Baseline state.'));
219
+ assert.ok(block.includes('Ship the memory system.'));
189
220
  assert.ok(block.includes('baseline work.'));
190
221
  assert.ok(block.includes('toolbox__memories_append_change_log'));
222
+ assert.ok(block.includes('toolbox__memories_write_project_goals'));
191
223
  // Empty instructions are omitted entirely.
192
224
  assert.ok(!block.includes('<project_instructions'));
193
225
 
@@ -201,8 +233,16 @@ test('fresh threads get the full prompt block; resumed threads get deltas', asyn
201
233
  assert.ok(update.includes('<project_context_update>'));
202
234
  assert.ok(update.includes('same-second follow-up.'));
203
235
  assert.ok(!update.includes('Baseline state.'), 'unchanged state stays out of the delta');
236
+ assert.ok(!update.includes('Ship the memory system.'), 'unchanged goals stay out of the delta');
204
237
  assert.equal(prompt.projectContextTurnUpdateBlock(resumed), '', 'update settles');
205
238
 
239
+ // Goals rewrite -> full new goals in the delta.
240
+ store.writeProjectGoals(projectPath, '# Project Goals\n\nLaunch project memory.');
241
+ const goalsUpdate = prompt.projectContextTurnUpdateBlock(resumed);
242
+ assert.ok(goalsUpdate.includes('<project_goals'));
243
+ assert.ok(goalsUpdate.includes('Launch project memory.'));
244
+ assert.ok(!goalsUpdate.includes('new_change_log_entries'));
245
+
206
246
  // State rewrite -> full new state in the delta.
207
247
  store.writeProjectState(projectPath, '# Project State\n\nNew state.');
208
248
  const stateUpdate = prompt.projectContextTurnUpdateBlock(resumed);
@@ -235,8 +275,13 @@ test('memories tools resolve the session project and enforce registration', asyn
235
275
  const wrote = await byName.write_project_state.handler({ content: '# Project State\n\nTool state.' }, context);
236
276
  assert.ok(!wrote.isError);
237
277
 
278
+ const goals = await byName.write_project_goals.handler({ content: '# Project Goals\n\nTool goals.' }, context);
279
+ assert.ok(!goals.isError);
280
+ assert.ok(readContextFile(projectPath, 'project-goals.md').includes('Tool goals.'));
281
+
238
282
  const read = await byName.read_project_context.handler({}, context);
239
283
  assert.ok(read.content[0].text.includes('Tool state.'));
284
+ assert.ok(read.content[0].text.includes('Tool goals.'));
240
285
 
241
286
  const missing = await byName.append_change_log.handler({ body: '- x' }, { callerSessionId: 'no-cwd' });
242
287
  assert.ok(missing.isError);
@@ -245,7 +290,7 @@ test('memories tools resolve the session project and enforce registration', asyn
245
290
 
246
291
  test('system scope: instructions-only record, guarded writes, canonical save path', () => {
247
292
  const systemContextDir = path.join(process.env.AMALGM_DIR, 'context');
248
- const systemInstructionsPath = path.join(systemContextDir, 'instructions.md');
293
+ const systemInstructionsPath = path.join(systemContextDir, 'system-instructions.md');
249
294
 
250
295
  // The boot pass owns file creation for the system scope.
251
296
  store.startProjectContextService();
@@ -258,17 +303,18 @@ test('system scope: instructions-only record, guarded writes, canonical save pat
258
303
  assert.equal(system.name, 'Amalgm System');
259
304
  assert.equal(system.paths.instructions, systemInstructionsPath);
260
305
  assert.equal(system.paths.changeLog, undefined, 'system has no change log');
306
+ assert.equal(system.paths.goals, undefined, 'system has no project goals');
261
307
  assert.equal(system.files.projectState, undefined, 'system has no project state');
262
308
  assert.ok(fs.existsSync(systemInstructionsPath), 'boot/list ensured the file');
263
309
  assert.deepEqual(
264
310
  fs.readdirSync(systemContextDir).filter((name) => name.endsWith('.md')).sort(),
265
- ['instructions.md'],
266
- 'only instructions.md is created for the system scope',
311
+ ['system-instructions.md'],
312
+ 'only system-instructions.md is created for the system scope',
267
313
  );
268
314
 
269
315
  // The UI editor saves by file path — it must resolve to the system scope.
270
316
  const before = currentSeq();
271
- const written = store.writeInstructions(systemInstructionsPath, '# Instructions\n\nAlways be definitive.');
317
+ const written = store.writeInstructions(systemInstructionsPath, '# System Instructions\n\nAlways be definitive.');
272
318
  assert.equal(written.project.id, 'system');
273
319
  const events = listEventsAfter(before).filter((event) => event.resource === 'project_context');
274
320
  assert.equal(events.length, 1);
@@ -276,23 +322,25 @@ test('system scope: instructions-only record, guarded writes, canonical save pat
276
322
  assert.ok(events[0].value.prompt.instructions.includes('Always be definitive.'));
277
323
 
278
324
  // System scope rejects per-project docs.
279
- assert.throws(() => store.appendChangeLog(systemInstructionsPath, '- x'), /instructions only/);
280
- assert.throws(() => store.writeProjectState(systemContextDir, '# Project State'), /instructions only/);
325
+ assert.throws(() => store.appendChangeLog(systemInstructionsPath, '- x'), /system-instructions\.md only/);
326
+ assert.throws(() => store.writeProjectGoals(systemContextDir, '# Project Goals'), /system-instructions\.md only/);
327
+ assert.throws(() => store.writeProjectState(systemContextDir, '# Project State'), /system-instructions\.md only/);
281
328
  });
282
329
 
283
330
  test('system instructions prompt block reads the canonical file', () => {
284
331
  const { systemInstructionsBlock, systemInstructionsPath } = require('../../chat-core/tooling/system-instructions');
285
332
  const filePath = systemInstructionsPath();
333
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
286
334
 
287
- fs.writeFileSync(filePath, '# Instructions\n\nNo instructions for Amalgm System yet.\n');
335
+ fs.writeFileSync(filePath, '# System Instructions\n\nNo instructions for Amalgm System yet.\n');
288
336
  assert.equal(systemInstructionsBlock(), '', 'legacy placeholder counts as empty');
289
337
 
290
- fs.writeFileSync(filePath, '# Instructions\n\nPrefer pnpm everywhere.\n');
338
+ fs.writeFileSync(filePath, '# System Instructions\n\nPrefer pnpm everywhere.\n');
291
339
  const block = systemInstructionsBlock();
292
340
  assert.ok(block.includes('<system_instructions'));
293
341
  assert.ok(block.includes('Prefer pnpm everywhere.'));
294
342
 
295
- fs.writeFileSync(filePath, '# Instructions\n');
343
+ fs.writeFileSync(filePath, '# System Instructions\n');
296
344
  assert.equal(systemInstructionsBlock(), '', 'blank doc injects nothing');
297
345
  });
298
346
 
@@ -3,20 +3,22 @@
3
3
  /**
4
4
  * System-level instructions — the one machine-wide context doc.
5
5
  *
6
- * `<AMALGM_DIR>/context/instructions.md` is user-authored and applies to every
7
- * session on this computer. Project memory (instructions, change log, project
8
- * state) is handled separately by the project-context block. The legacy
9
- * active/passive memory blocks are gone — this and project context are the
10
- * definitive memory surfaces.
6
+ * `<AMALGM_DIR>/context/system-instructions.md` is user-authored and applies
7
+ * to every session on this computer. Project memory is handled separately by
8
+ * the project-context block. The legacy active/passive memory blocks are gone
9
+ * — this and project context are the definitive memory surfaces.
11
10
  */
12
11
 
13
12
  const fs = require('fs');
14
- const path = require('path');
15
13
  const { AMALGM_DIR } = require('../../amalgm-mcp/config');
16
- const { cappedDocContent, isEmptyContextDoc } = require('../../amalgm-mcp/project-context/paths');
14
+ const {
15
+ cappedDocContent,
16
+ isEmptyContextDoc,
17
+ systemContextPaths,
18
+ } = require('../../amalgm-mcp/project-context/paths');
17
19
 
18
20
  function systemInstructionsPath() {
19
- return path.join(path.resolve(AMALGM_DIR), 'context', 'instructions.md');
21
+ return systemContextPaths(AMALGM_DIR).instructions;
20
22
  }
21
23
 
22
24
  function escapeAttribute(value) {
@@ -33,9 +33,10 @@ function systemInstructionsBlockSafe() {
33
33
 
34
34
  /**
35
35
  * Definitive memory model: platform context, system instructions
36
- * (machine-wide, user-authored), project context (instructions + project
37
- * state + recent change log, maintained through the memories tool), and the
38
- * agent's own instructions. The legacy active/passive memory blocks are gone.
36
+ * (machine-wide, user-authored), project context (project instructions +
37
+ * goals + state + recent change log, maintained through the memories tool),
38
+ * and the agent's own instructions. The legacy active/passive memory blocks
39
+ * are gone.
39
40
  */
40
41
  function composeSystemPrompt(contract) {
41
42
  const parts = [];