metame-cli 1.5.19 → 1.5.21

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 (40) hide show
  1. package/index.js +157 -80
  2. package/package.json +2 -2
  3. package/scripts/bin/bootstrap-worktree.sh +20 -0
  4. package/scripts/core/audit.js +190 -0
  5. package/scripts/core/handoff.js +780 -0
  6. package/scripts/core/handoff.test.js +1074 -0
  7. package/scripts/core/memory-model.js +183 -0
  8. package/scripts/core/memory-model.test.js +486 -0
  9. package/scripts/core/reactive-paths.js +44 -0
  10. package/scripts/core/reactive-paths.test.js +35 -0
  11. package/scripts/core/reactive-prompt.js +51 -0
  12. package/scripts/core/reactive-prompt.test.js +88 -0
  13. package/scripts/core/reactive-signal.js +40 -0
  14. package/scripts/core/reactive-signal.test.js +88 -0
  15. package/scripts/core/thread-chat-id.js +52 -0
  16. package/scripts/core/thread-chat-id.test.js +113 -0
  17. package/scripts/daemon-bridges.js +92 -38
  18. package/scripts/daemon-claude-engine.js +373 -444
  19. package/scripts/daemon-command-router.js +82 -8
  20. package/scripts/daemon-engine-runtime.js +7 -10
  21. package/scripts/daemon-reactive-lifecycle.js +100 -33
  22. package/scripts/daemon-session-commands.js +133 -43
  23. package/scripts/daemon-session-store.js +300 -82
  24. package/scripts/daemon-team-dispatch.js +16 -16
  25. package/scripts/daemon.js +21 -175
  26. package/scripts/deploy-manifest.js +90 -0
  27. package/scripts/docs/maintenance-manual.md +14 -11
  28. package/scripts/docs/pointer-map.md +13 -4
  29. package/scripts/feishu-adapter.js +31 -27
  30. package/scripts/hooks/intent-engine.js +6 -3
  31. package/scripts/hooks/intent-memory-recall.js +1 -0
  32. package/scripts/hooks/intent-perpetual.js +1 -1
  33. package/scripts/memory-extract.js +5 -97
  34. package/scripts/memory-gc.js +35 -90
  35. package/scripts/memory-migrate-v2.js +304 -0
  36. package/scripts/memory-nightly-reflect.js +40 -41
  37. package/scripts/memory.js +340 -859
  38. package/scripts/migrate-reactive-paths.js +122 -0
  39. package/scripts/signal-capture.js +4 -0
  40. package/scripts/sync-plugin.js +56 -0
@@ -0,0 +1,44 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+
5
+ /**
6
+ * Resolve new per-project reactive file paths.
7
+ *
8
+ * Structure: ~/.metame/reactive/<key>/{memory,l2cache,state,events,latest}.{md,jsonl}
9
+ *
10
+ * @param {string} projectKey
11
+ * @param {string} metameDir - e.g. ~/.metame
12
+ * @returns {{ dir: string, memory: string, l2cache: string, state: string, events: string, latest: string }}
13
+ */
14
+ function resolveReactivePaths(projectKey, metameDir) {
15
+ const dir = path.join(metameDir, 'reactive', projectKey);
16
+ return {
17
+ dir,
18
+ memory: path.join(dir, 'memory.md'),
19
+ l2cache: path.join(dir, 'l2cache.md'),
20
+ state: path.join(dir, 'state.md'),
21
+ events: path.join(dir, 'events.jsonl'),
22
+ latest: path.join(dir, 'latest.md'),
23
+ };
24
+ }
25
+
26
+ /**
27
+ * Resolve legacy (pre-migration) flat paths for a project.
28
+ * Used by the migration script to locate old files.
29
+ *
30
+ * @param {string} projectKey
31
+ * @param {string} metameDir
32
+ * @returns {{ memory: string, l2cache: string, state: string, events: string, latest: string }}
33
+ */
34
+ function resolveLegacyPaths(projectKey, metameDir) {
35
+ return {
36
+ memory: path.join(metameDir, 'memory', 'now', `${projectKey}_memory.md`),
37
+ l2cache: path.join(metameDir, 'memory', 'now', `${projectKey}_l2cache.md`),
38
+ state: path.join(metameDir, 'memory', 'now', `${projectKey}.md`),
39
+ events: path.join(metameDir, 'events', `${projectKey}.jsonl`),
40
+ latest: path.join(metameDir, 'memory', 'agents', `${projectKey}_latest.md`),
41
+ };
42
+ }
43
+
44
+ module.exports = { resolveReactivePaths, resolveLegacyPaths };
@@ -0,0 +1,35 @@
1
+ 'use strict';
2
+
3
+ const { describe, it } = require('node:test');
4
+ const assert = require('node:assert');
5
+ const path = require('path');
6
+ const { resolveReactivePaths, resolveLegacyPaths } = require('./reactive-paths');
7
+
8
+ describe('resolveReactivePaths', () => {
9
+ it('returns correct directory structure', () => {
10
+ const p = resolveReactivePaths('scientist', '/home/user/.metame');
11
+ assert.equal(p.dir, path.join('/home/user/.metame', 'reactive', 'scientist'));
12
+ assert.equal(p.memory, path.join(p.dir, 'memory.md'));
13
+ assert.equal(p.l2cache, path.join(p.dir, 'l2cache.md'));
14
+ assert.equal(p.state, path.join(p.dir, 'state.md'));
15
+ assert.equal(p.events, path.join(p.dir, 'events.jsonl'));
16
+ assert.equal(p.latest, path.join(p.dir, 'latest.md'));
17
+ });
18
+
19
+ it('works with different keys', () => {
20
+ const p = resolveReactivePaths('my_project', '/tmp/meta');
21
+ assert.equal(p.dir, path.join('/tmp/meta', 'reactive', 'my_project'));
22
+ assert.equal(p.events, path.join('/tmp/meta', 'reactive', 'my_project', 'events.jsonl'));
23
+ });
24
+ });
25
+
26
+ describe('resolveLegacyPaths', () => {
27
+ it('returns flat legacy paths', () => {
28
+ const p = resolveLegacyPaths('scientist', '/home/user/.metame');
29
+ assert.equal(p.memory, path.join('/home/user/.metame', 'memory', 'now', 'scientist_memory.md'));
30
+ assert.equal(p.l2cache, path.join('/home/user/.metame', 'memory', 'now', 'scientist_l2cache.md'));
31
+ assert.equal(p.state, path.join('/home/user/.metame', 'memory', 'now', 'scientist.md'));
32
+ assert.equal(p.events, path.join('/home/user/.metame', 'events', 'scientist.jsonl'));
33
+ assert.equal(p.latest, path.join('/home/user/.metame', 'memory', 'agents', 'scientist_latest.md'));
34
+ });
35
+ });
@@ -0,0 +1,51 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * core/reactive-prompt.js — Pure function to wrap reactive prompts
5
+ *
6
+ * Injects reactive mode instructions, working memory, and retry warnings
7
+ * into agent prompts. Zero I/O, zero side effects.
8
+ */
9
+
10
+ /**
11
+ * Build a reactive-mode prompt wrapper around the original prompt.
12
+ *
13
+ * @param {string} originalPrompt - The original prompt text
14
+ * @param {object} opts
15
+ * @param {number} opts.depth - Current reactive depth
16
+ * @param {number} opts.maxDepth - Maximum allowed depth
17
+ * @param {string} opts.completionSignal - Signal string for mission completion
18
+ * @param {string} [opts.workingMemory] - Optional working memory content
19
+ * @param {boolean} [opts.isRetry] - Whether this is a retry after no signal
20
+ * @returns {string} Wrapped prompt string
21
+ */
22
+ function buildReactivePrompt(originalPrompt, opts) {
23
+ const { depth, maxDepth, completionSignal, workingMemory, isRetry } = opts;
24
+
25
+ const parts = [];
26
+
27
+ parts.push(`[REACTIVE MODE] depth ${depth}/${maxDepth}`);
28
+ parts.push('Rules:');
29
+ parts.push('1. After completing the current step, you MUST output NEXT_DISPATCH: <target> "<prompt>" to trigger the next step');
30
+ parts.push(`2. Only output ${completionSignal} when ALL objectives are achieved`);
31
+ parts.push('3. Do NOT exit silently — failing to output a signal means the task chain breaks');
32
+
33
+ if (isRetry) {
34
+ parts.push('');
35
+ parts.push('Warning: you did not output any signal in the previous round, causing task interruption. Check progress and continue.');
36
+ }
37
+
38
+ if (workingMemory && workingMemory.trim()) {
39
+ parts.push('');
40
+ parts.push('[Working Memory]');
41
+ parts.push(workingMemory.trim());
42
+ }
43
+
44
+ parts.push('');
45
+ parts.push('---');
46
+ parts.push(originalPrompt);
47
+
48
+ return parts.join('\n');
49
+ }
50
+
51
+ module.exports = { buildReactivePrompt };
@@ -0,0 +1,88 @@
1
+ 'use strict';
2
+
3
+ const { describe, it } = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const { buildReactivePrompt } = require('./reactive-prompt');
6
+
7
+ describe('buildReactivePrompt', () => {
8
+ it('wraps prompt with reactive mode header (no memory, no retry)', () => {
9
+ const result = buildReactivePrompt('Do the task', {
10
+ depth: 3,
11
+ maxDepth: 50,
12
+ completionSignal: 'MISSION_COMPLETE',
13
+ });
14
+ assert.ok(result.includes('[REACTIVE MODE] depth 3/50'));
15
+ assert.ok(result.includes('NEXT_DISPATCH'));
16
+ assert.ok(result.includes('MISSION_COMPLETE'));
17
+ assert.ok(result.includes('Do the task'));
18
+ assert.ok(!result.includes('[Working Memory]'));
19
+ assert.ok(!result.includes('Warning:'));
20
+ });
21
+
22
+ it('injects working memory when provided', () => {
23
+ const result = buildReactivePrompt('Do the task', {
24
+ depth: 1,
25
+ maxDepth: 10,
26
+ completionSignal: 'DONE',
27
+ workingMemory: '## Recent Decisions\n- chose option A',
28
+ });
29
+ assert.ok(result.includes('[Working Memory]'));
30
+ assert.ok(result.includes('## Recent Decisions'));
31
+ assert.ok(result.includes('chose option A'));
32
+ });
33
+
34
+ it('includes retry warning when isRetry is true', () => {
35
+ const result = buildReactivePrompt('Check progress', {
36
+ depth: 5,
37
+ maxDepth: 50,
38
+ completionSignal: 'MISSION_COMPLETE',
39
+ isRetry: true,
40
+ });
41
+ assert.ok(result.includes('Warning:'));
42
+ assert.ok(result.includes('previous round'));
43
+ });
44
+
45
+ it('does not inject Working Memory block when workingMemory is empty string', () => {
46
+ const result = buildReactivePrompt('Do stuff', {
47
+ depth: 2,
48
+ maxDepth: 20,
49
+ completionSignal: 'MISSION_COMPLETE',
50
+ workingMemory: '',
51
+ });
52
+ assert.ok(!result.includes('[Working Memory]'));
53
+ });
54
+
55
+ it('does not inject Working Memory block when workingMemory is whitespace only', () => {
56
+ const result = buildReactivePrompt('Do stuff', {
57
+ depth: 2,
58
+ maxDepth: 20,
59
+ completionSignal: 'MISSION_COMPLETE',
60
+ workingMemory: ' \n ',
61
+ });
62
+ assert.ok(!result.includes('[Working Memory]'));
63
+ });
64
+
65
+ it('does not inject Working Memory block when workingMemory is undefined', () => {
66
+ const result = buildReactivePrompt('Do stuff', {
67
+ depth: 2,
68
+ maxDepth: 20,
69
+ completionSignal: 'MISSION_COMPLETE',
70
+ workingMemory: undefined,
71
+ });
72
+ assert.ok(!result.includes('[Working Memory]'));
73
+ });
74
+
75
+ it('includes both retry warning and working memory when both present', () => {
76
+ const result = buildReactivePrompt('Continue', {
77
+ depth: 4,
78
+ maxDepth: 50,
79
+ completionSignal: 'MISSION_COMPLETE',
80
+ workingMemory: 'Some context',
81
+ isRetry: true,
82
+ });
83
+ assert.ok(result.includes('Warning:'));
84
+ assert.ok(result.includes('[Working Memory]'));
85
+ assert.ok(result.includes('Some context'));
86
+ assert.ok(result.includes('Continue'));
87
+ });
88
+ });
@@ -0,0 +1,40 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * core/reactive-signal.js — Pure function for no-signal decision logic
5
+ *
6
+ * Determines the next action when a reactive agent completes output:
7
+ * proceed, retry (if no signal detected), or pause (after max retries).
8
+ * Zero I/O, zero side effects.
9
+ */
10
+
11
+ /**
12
+ * Calculate the next action based on signal presence and retry count.
13
+ *
14
+ * @param {object} params
15
+ * @param {boolean} params.hasSignals - Whether any signals were detected in output
16
+ * @param {boolean} params.isComplete - Whether completion signal was detected
17
+ * @param {number} params.noSignalCount - Current consecutive no-signal count
18
+ * @param {number} params.maxRetries - Maximum retries before pausing
19
+ * @returns {{ action: 'proceed'|'retry'|'pause', nextNoSignalCount: number, pauseReason?: string }}
20
+ */
21
+ function calculateNextAction({ hasSignals, isComplete, noSignalCount, maxRetries }) {
22
+ if (isComplete) {
23
+ return { action: 'proceed', nextNoSignalCount: 0 };
24
+ }
25
+
26
+ if (hasSignals) {
27
+ return { action: 'proceed', nextNoSignalCount: 0 };
28
+ }
29
+
30
+ // No signals detected
31
+ const nextCount = noSignalCount + 1;
32
+
33
+ if (nextCount >= maxRetries) {
34
+ return { action: 'pause', nextNoSignalCount: nextCount, pauseReason: 'no_signal_repeated' };
35
+ }
36
+
37
+ return { action: 'retry', nextNoSignalCount: nextCount };
38
+ }
39
+
40
+ module.exports = { calculateNextAction };
@@ -0,0 +1,88 @@
1
+ 'use strict';
2
+
3
+ const { describe, it } = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const { calculateNextAction } = require('./reactive-signal');
6
+
7
+ describe('calculateNextAction', () => {
8
+ it('returns proceed with count=0 when isComplete', () => {
9
+ const result = calculateNextAction({
10
+ hasSignals: false,
11
+ isComplete: true,
12
+ noSignalCount: 2,
13
+ maxRetries: 3,
14
+ });
15
+ assert.equal(result.action, 'proceed');
16
+ assert.equal(result.nextNoSignalCount, 0);
17
+ assert.equal(result.pauseReason, undefined);
18
+ });
19
+
20
+ it('returns proceed with count=0 when hasSignals (not complete)', () => {
21
+ const result = calculateNextAction({
22
+ hasSignals: true,
23
+ isComplete: false,
24
+ noSignalCount: 1,
25
+ maxRetries: 3,
26
+ });
27
+ assert.equal(result.action, 'proceed');
28
+ assert.equal(result.nextNoSignalCount, 0);
29
+ });
30
+
31
+ it('returns retry when no signals and count+1 < maxRetries', () => {
32
+ const result = calculateNextAction({
33
+ hasSignals: false,
34
+ isComplete: false,
35
+ noSignalCount: 0,
36
+ maxRetries: 3,
37
+ });
38
+ assert.equal(result.action, 'retry');
39
+ assert.equal(result.nextNoSignalCount, 1);
40
+ assert.equal(result.pauseReason, undefined);
41
+ });
42
+
43
+ it('returns retry when count+1 is still less than maxRetries', () => {
44
+ const result = calculateNextAction({
45
+ hasSignals: false,
46
+ isComplete: false,
47
+ noSignalCount: 1,
48
+ maxRetries: 3,
49
+ });
50
+ assert.equal(result.action, 'retry');
51
+ assert.equal(result.nextNoSignalCount, 2);
52
+ });
53
+
54
+ it('returns pause when count+1 >= maxRetries', () => {
55
+ const result = calculateNextAction({
56
+ hasSignals: false,
57
+ isComplete: false,
58
+ noSignalCount: 2,
59
+ maxRetries: 3,
60
+ });
61
+ assert.equal(result.action, 'pause');
62
+ assert.equal(result.nextNoSignalCount, 3);
63
+ assert.equal(result.pauseReason, 'no_signal_repeated');
64
+ });
65
+
66
+ it('returns pause when count already exceeds maxRetries', () => {
67
+ const result = calculateNextAction({
68
+ hasSignals: false,
69
+ isComplete: false,
70
+ noSignalCount: 5,
71
+ maxRetries: 3,
72
+ });
73
+ assert.equal(result.action, 'pause');
74
+ assert.equal(result.nextNoSignalCount, 6);
75
+ assert.equal(result.pauseReason, 'no_signal_repeated');
76
+ });
77
+
78
+ it('isComplete takes priority over hasSignals', () => {
79
+ const result = calculateNextAction({
80
+ hasSignals: true,
81
+ isComplete: true,
82
+ noSignalCount: 2,
83
+ maxRetries: 3,
84
+ });
85
+ assert.equal(result.action, 'proceed');
86
+ assert.equal(result.nextNoSignalCount, 0);
87
+ });
88
+ });
@@ -0,0 +1,52 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * thread-chat-id.js — Pure utilities for Feishu topic-based session isolation.
5
+ *
6
+ * Composite ID format: "thread:{chatId}:{threadId}"
7
+ * chatId = Feishu group chat ID (e.g. "oc_xxx")
8
+ * threadId = topic root message ID (e.g. "om_yyy")
9
+ *
10
+ * Zero dependencies. Zero side effects.
11
+ */
12
+
13
+ const PREFIX = 'thread:';
14
+
15
+ function buildThreadChatId(chatId, threadId) {
16
+ const c = String(chatId || '').trim();
17
+ const t = String(threadId || '').trim();
18
+ if (!c || !t) return c || '';
19
+ return `${PREFIX}${c}:${t}`;
20
+ }
21
+
22
+ function parseThreadChatId(compositeId) {
23
+ const id = String(compositeId || '');
24
+ if (!id.startsWith(PREFIX)) return null;
25
+ const firstColon = PREFIX.length;
26
+ const secondColon = id.indexOf(':', firstColon);
27
+ if (secondColon === -1) return null;
28
+ const chatId = id.slice(firstColon, secondColon);
29
+ const threadId = id.slice(secondColon + 1);
30
+ if (!chatId || !threadId) return null;
31
+ return { chatId, threadId };
32
+ }
33
+
34
+ function isThreadChatId(id) {
35
+ return typeof id === 'string' && id.startsWith(PREFIX) && parseThreadChatId(id) !== null;
36
+ }
37
+
38
+ /**
39
+ * Extract the raw Feishu chat ID regardless of whether the input
40
+ * is a composite thread ID or a plain chat ID.
41
+ */
42
+ function rawChatId(id) {
43
+ const parsed = parseThreadChatId(id);
44
+ return parsed ? parsed.chatId : String(id || '');
45
+ }
46
+
47
+ module.exports = {
48
+ buildThreadChatId,
49
+ parseThreadChatId,
50
+ isThreadChatId,
51
+ rawChatId,
52
+ };
@@ -0,0 +1,113 @@
1
+ 'use strict';
2
+
3
+ const { describe, it } = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const {
6
+ buildThreadChatId,
7
+ parseThreadChatId,
8
+ isThreadChatId,
9
+ rawChatId,
10
+ } = require('./thread-chat-id');
11
+
12
+ describe('buildThreadChatId', () => {
13
+ it('builds composite ID from chatId and threadId', () => {
14
+ assert.equal(buildThreadChatId('oc_123', 'om_abc'), 'thread:oc_123:om_abc');
15
+ });
16
+
17
+ it('trims whitespace', () => {
18
+ assert.equal(buildThreadChatId(' oc_123 ', ' om_abc '), 'thread:oc_123:om_abc');
19
+ });
20
+
21
+ it('returns plain chatId when threadId is empty', () => {
22
+ assert.equal(buildThreadChatId('oc_123', ''), 'oc_123');
23
+ assert.equal(buildThreadChatId('oc_123', null), 'oc_123');
24
+ assert.equal(buildThreadChatId('oc_123', undefined), 'oc_123');
25
+ });
26
+
27
+ it('returns empty string when chatId is empty', () => {
28
+ assert.equal(buildThreadChatId('', 'om_abc'), '');
29
+ assert.equal(buildThreadChatId(null, 'om_abc'), '');
30
+ });
31
+
32
+ it('returns empty string when both are empty', () => {
33
+ assert.equal(buildThreadChatId('', ''), '');
34
+ });
35
+ });
36
+
37
+ describe('parseThreadChatId', () => {
38
+ it('parses valid composite ID', () => {
39
+ assert.deepEqual(parseThreadChatId('thread:oc_123:om_abc'), {
40
+ chatId: 'oc_123',
41
+ threadId: 'om_abc',
42
+ });
43
+ });
44
+
45
+ it('handles threadId containing colons', () => {
46
+ assert.deepEqual(parseThreadChatId('thread:oc_123:om_abc:extra'), {
47
+ chatId: 'oc_123',
48
+ threadId: 'om_abc:extra',
49
+ });
50
+ });
51
+
52
+ it('returns null for plain chatId', () => {
53
+ assert.equal(parseThreadChatId('oc_123'), null);
54
+ });
55
+
56
+ it('returns null for empty string', () => {
57
+ assert.equal(parseThreadChatId(''), null);
58
+ });
59
+
60
+ it('returns null for null/undefined', () => {
61
+ assert.equal(parseThreadChatId(null), null);
62
+ assert.equal(parseThreadChatId(undefined), null);
63
+ });
64
+
65
+ it('returns null when prefix present but missing parts', () => {
66
+ assert.equal(parseThreadChatId('thread:'), null);
67
+ assert.equal(parseThreadChatId('thread:oc_123'), null);
68
+ assert.equal(parseThreadChatId('thread::om_abc'), null);
69
+ assert.equal(parseThreadChatId('thread:oc_123:'), null);
70
+ });
71
+ });
72
+
73
+ describe('isThreadChatId', () => {
74
+ it('returns true for valid composite ID', () => {
75
+ assert.equal(isThreadChatId('thread:oc_123:om_abc'), true);
76
+ });
77
+
78
+ it('returns false for plain chatId', () => {
79
+ assert.equal(isThreadChatId('oc_123'), false);
80
+ });
81
+
82
+ it('returns false for malformed thread prefix', () => {
83
+ assert.equal(isThreadChatId('thread:'), false);
84
+ assert.equal(isThreadChatId('thread:oc_123'), false);
85
+ });
86
+
87
+ it('returns false for non-string', () => {
88
+ assert.equal(isThreadChatId(null), false);
89
+ assert.equal(isThreadChatId(123), false);
90
+ });
91
+ });
92
+
93
+ describe('rawChatId', () => {
94
+ it('extracts chatId from composite', () => {
95
+ assert.equal(rawChatId('thread:oc_123:om_abc'), 'oc_123');
96
+ });
97
+
98
+ it('returns plain chatId as-is', () => {
99
+ assert.equal(rawChatId('oc_123'), 'oc_123');
100
+ });
101
+
102
+ it('returns empty string for null', () => {
103
+ assert.equal(rawChatId(null), '');
104
+ });
105
+ });
106
+
107
+ describe('round-trip', () => {
108
+ it('build then parse preserves values', () => {
109
+ const built = buildThreadChatId('oc_foo', 'om_bar');
110
+ const parsed = parseThreadChatId(built);
111
+ assert.deepEqual(parsed, { chatId: 'oc_foo', threadId: 'om_bar' });
112
+ });
113
+ });