stella-timeline-plugin 2.0.0 → 2.0.2

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.
@@ -1,21 +1,296 @@
1
- import { spawnSync } from 'node:child_process';
1
+ import { execSync, spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs';
2
3
  import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
3
6
 
7
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
4
8
  const runtimeTempDir = process.platform === 'win32' ? os.tmpdir() : '/tmp';
5
9
 
6
- const result = spawnSync(
7
- process.execPath,
8
- ['node_modules/jest/bin/jest.js', '--runInBand', '--runTestsByPath', 'src/integration/openclaw-runtime.smoke.test.ts'],
9
- {
10
- stdio: 'inherit',
10
+ function runCapture(command) {
11
+ try {
12
+ return execSync(command, {
13
+ encoding: 'utf8',
14
+ stdio: ['ignore', 'pipe', 'ignore'],
15
+ env: {
16
+ ...process.env,
17
+ TMPDIR: runtimeTempDir,
18
+ TMP: runtimeTempDir,
19
+ TEMP: runtimeTempDir,
20
+ },
21
+ }).trim();
22
+ } catch {
23
+ return '';
24
+ }
25
+ }
26
+
27
+ function resolveOnPath(binaryName) {
28
+ const command = process.platform === 'win32' ? 'where' : 'which';
29
+ const result = spawnSync(command, [binaryName], {
30
+ encoding: 'utf8',
11
31
  env: {
12
32
  ...process.env,
13
33
  TMPDIR: runtimeTempDir,
14
34
  TMP: runtimeTempDir,
15
35
  TEMP: runtimeTempDir,
16
- OPENCLAW_RUNTIME_SMOKE: '1',
17
36
  },
37
+ });
38
+ if (result.status !== 0) return '';
39
+ return result.stdout
40
+ .split(/\r?\n/)
41
+ .map((line) => line.trim())
42
+ .find(Boolean) || '';
43
+ }
44
+
45
+ function listJsFiles(dirPath) {
46
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
47
+ const files = [];
48
+ for (const entry of entries) {
49
+ const fullPath = path.join(dirPath, entry.name);
50
+ if (entry.isDirectory()) {
51
+ files.push(...listJsFiles(fullPath));
52
+ continue;
53
+ }
54
+ if (entry.isFile() && entry.name.endsWith('.js')) {
55
+ files.push(fullPath);
56
+ }
57
+ }
58
+ return files;
59
+ }
60
+
61
+ function candidateDistDirs() {
62
+ const dirs = new Set();
63
+ const explicit = process.env.OPENCLAW_RUNTIME_MODULE?.trim();
64
+ if (explicit && fs.existsSync(explicit)) {
65
+ dirs.add(path.dirname(explicit));
66
+ }
67
+
68
+ const npmRoot = runCapture(process.platform === 'win32' ? 'npm.cmd root -g' : 'npm root -g');
69
+ if (npmRoot) {
70
+ dirs.add(path.join(npmRoot, 'openclaw', 'dist'));
71
+ }
72
+
73
+ const npmPrefix = runCapture(process.platform === 'win32' ? 'npm.cmd prefix -g' : 'npm prefix -g');
74
+ if (npmPrefix) {
75
+ dirs.add(path.join(npmPrefix, 'node_modules', 'openclaw', 'dist'));
76
+ dirs.add(path.join(npmPrefix, 'lib', 'node_modules', 'openclaw', 'dist'));
77
+ }
78
+
79
+ const openClawBin = resolveOnPath('openclaw');
80
+ if (openClawBin) {
81
+ const binDir = path.dirname(openClawBin);
82
+ dirs.add(path.join(binDir, 'node_modules', 'openclaw', 'dist'));
83
+ dirs.add(path.join(binDir, '..', 'node_modules', 'openclaw', 'dist'));
84
+ dirs.add(path.join(binDir, '..', 'lib', 'node_modules', 'openclaw', 'dist'));
85
+ }
86
+
87
+ if (process.platform === 'win32') {
88
+ const appData = process.env.APPDATA?.trim();
89
+ if (appData) {
90
+ dirs.add(path.join(appData, 'npm', 'node_modules', 'openclaw', 'dist'));
91
+ }
92
+ }
93
+
94
+ return [...dirs];
95
+ }
96
+
97
+ function findRuntimeModuleInDist(distDir) {
98
+ if (!fs.existsSync(distDir)) return '';
99
+ const candidates = listJsFiles(distDir);
100
+ const aliasAware = candidates.find((filePath) => {
101
+ const content = fs.readFileSync(filePath, 'utf8');
102
+ return content.includes('loadOpenClawPlugins as') && content.includes('resolvePluginTools as');
103
+ });
104
+ if (aliasAware) return aliasAware;
105
+
106
+ const direct = candidates.find((filePath) => {
107
+ const content = fs.readFileSync(filePath, 'utf8');
108
+ return content.includes('function loadOpenClawPlugins') && content.includes('function resolvePluginTools');
109
+ });
110
+ if (direct) return direct;
111
+
112
+ return candidates.find((filePath) => /^reply-.*\.js$/.test(path.basename(filePath))) || '';
113
+ }
114
+
115
+ function findOpenClawRuntimeModule() {
116
+ const explicit = process.env.OPENCLAW_RUNTIME_MODULE?.trim();
117
+ if (explicit && fs.existsSync(explicit)) return explicit;
118
+
119
+ for (const distDir of candidateDistDirs()) {
120
+ const runtimeModule = findRuntimeModuleInDist(distDir);
121
+ if (runtimeModule) return runtimeModule;
122
+ }
123
+
124
+ const nvmNodeDir = path.join(os.homedir(), '.nvm', 'versions', 'node');
125
+ if (fs.existsSync(nvmNodeDir)) {
126
+ for (const entry of fs.readdirSync(nvmNodeDir, { withFileTypes: true })) {
127
+ if (!entry.isDirectory()) continue;
128
+ const distDir = path.join(nvmNodeDir, entry.name, 'lib', 'node_modules', 'openclaw', 'dist');
129
+ const runtimeModule = findRuntimeModuleInDist(distDir);
130
+ if (runtimeModule) return runtimeModule;
131
+ }
132
+ }
133
+
134
+ return '';
135
+ }
136
+
137
+ function findCompatibleNodeBin(runtimeModulePath) {
138
+ const explicit = process.env.OPENCLAW_NODE_BIN?.trim();
139
+ if (explicit && fs.existsSync(explicit)) return explicit;
140
+
141
+ const normalized = path.normalize(runtimeModulePath);
142
+ const unixMarker = `${path.sep}lib${path.sep}node_modules${path.sep}openclaw${path.sep}dist${path.sep}`;
143
+ const unixIndex = normalized.lastIndexOf(unixMarker);
144
+ if (unixIndex !== -1) {
145
+ const prefixDir = normalized.slice(0, unixIndex);
146
+ const unixNode = path.join(prefixDir, 'bin', 'node');
147
+ if (fs.existsSync(unixNode)) return unixNode;
148
+ const winNode = path.join(prefixDir, 'node.exe');
149
+ if (fs.existsSync(winNode)) return winNode;
150
+ }
151
+
152
+ return process.execPath;
153
+ }
154
+
155
+ function parseLastJsonObject(raw) {
156
+ const jsonLine = raw
157
+ .split(/\r?\n/)
158
+ .map((line) => line.trim())
159
+ .filter((line) => line.startsWith('{') && line.endsWith('}'))
160
+ .slice(-1)[0];
161
+ if (!jsonLine) {
162
+ throw new Error(`OpenClaw smoke script did not emit a JSON payload.\n${raw}`);
163
+ }
164
+ return JSON.parse(jsonLine);
165
+ }
166
+
167
+ function copyRecursive(sourcePath, targetPath) {
168
+ const stat = fs.statSync(sourcePath);
169
+ if (stat.isDirectory()) {
170
+ fs.mkdirSync(targetPath, { recursive: true });
171
+ for (const entry of fs.readdirSync(sourcePath)) {
172
+ copyRecursive(path.join(sourcePath, entry), path.join(targetPath, entry));
173
+ }
174
+ return;
175
+ }
176
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
177
+ fs.copyFileSync(sourcePath, targetPath);
178
+ }
179
+
180
+ function setSafePermissions(targetPath) {
181
+ if (process.platform === 'win32' || !fs.existsSync(targetPath)) return;
182
+ const stat = fs.statSync(targetPath);
183
+ fs.chmodSync(targetPath, stat.isDirectory() ? 0o755 : 0o644);
184
+ if (stat.isDirectory()) {
185
+ for (const entry of fs.readdirSync(targetPath)) {
186
+ setSafePermissions(path.join(targetPath, entry));
187
+ }
188
+ }
189
+ }
190
+
191
+ function stagePluginForSmoke() {
192
+ const stagedRoot = fs.mkdtempSync(path.join(runtimeTempDir, 'stella-timeline-plugin-smoke-'));
193
+ for (const entry of ['package.json', 'openclaw.plugin.json', 'dist']) {
194
+ const sourcePath = path.join(repoRoot, entry);
195
+ if (!fs.existsSync(sourcePath)) {
196
+ throw new Error(`OpenClaw smoke staging missing required path: ${sourcePath}`);
197
+ }
198
+ copyRecursive(sourcePath, path.join(stagedRoot, entry));
199
+ }
200
+ setSafePermissions(stagedRoot);
201
+ return stagedRoot;
202
+ }
203
+
204
+ const runtimeModule = findOpenClawRuntimeModule();
205
+ if (!runtimeModule) {
206
+ console.log('Skipping OpenClaw smoke: OpenClaw runtime not found on this machine.');
207
+ process.exit(0);
208
+ }
209
+
210
+ const openClawNodeBin = findCompatibleNodeBin(runtimeModule);
211
+ const stagedPluginRoot = stagePluginForSmoke();
212
+ const script = `
213
+ import fs from 'node:fs';
214
+ import { pathToFileURL } from 'node:url';
215
+
216
+ const runtimeModulePath = ${JSON.stringify(runtimeModule)};
217
+ const repoRoot = ${JSON.stringify(repoRoot)};
218
+ const pluginRoot = ${JSON.stringify(stagedPluginRoot)};
219
+ const runtimeSource = fs.readFileSync(runtimeModulePath, 'utf8');
220
+ const alias = (symbolName) => runtimeSource.match(new RegExp('\\\\b' + symbolName + ' as ([\\\\w$]+)'))?.[1] || symbolName;
221
+ const runtime = await import(pathToFileURL(runtimeModulePath).href);
222
+ const loadOpenClawPlugins = runtime[alias('loadOpenClawPlugins')]
223
+ || runtime.loadOpenClawPlugins
224
+ || runtime.default?.[alias('loadOpenClawPlugins')]
225
+ || runtime.default?.loadOpenClawPlugins;
226
+ const resolvePluginTools = runtime[alias('resolvePluginTools')]
227
+ || runtime.resolvePluginTools
228
+ || runtime.default?.[alias('resolvePluginTools')]
229
+ || runtime.default?.resolvePluginTools;
230
+
231
+ if (typeof loadOpenClawPlugins !== 'function' || typeof resolvePluginTools !== 'function') {
232
+ throw new Error('Unable to resolve OpenClaw runtime exports from ' + runtimeModulePath);
233
+ }
234
+
235
+ const config = {
236
+ plugins: {
237
+ allow: ['stella-timeline-plugin'],
238
+ load: { paths: [pluginRoot] },
239
+ entries: { 'stella-timeline-plugin': { enabled: true } },
240
+ },
241
+ tools: {
242
+ profile: 'coding',
243
+ alsoAllow: ['stella-timeline-plugin'],
244
+ },
245
+ };
246
+
247
+ const registry = loadOpenClawPlugins({ config, workspaceDir: repoRoot });
248
+ const plugin = registry.plugins.find((entry) => entry.id === 'stella-timeline-plugin');
249
+ const resolvedTools = resolvePluginTools({
250
+ context: { config, workspaceDir: repoRoot, sandboxed: true },
251
+ existingToolNames: new Set(),
252
+ toolAllowlist: ['stella-timeline-plugin'],
253
+ });
254
+
255
+ console.log(JSON.stringify({
256
+ runtimeModulePath,
257
+ plugin: plugin ? {
258
+ status: plugin.status,
259
+ toolNames: plugin.toolNames,
260
+ } : null,
261
+ resolvedToolNames: resolvedTools.map((tool) => tool.name),
262
+ }));
263
+ `;
264
+
265
+ const result = spawnSync(openClawNodeBin, ['--input-type=module', '-e', script], {
266
+ cwd: repoRoot,
267
+ encoding: 'utf8',
268
+ env: {
269
+ ...process.env,
270
+ TMPDIR: runtimeTempDir,
271
+ TMP: runtimeTempDir,
272
+ TEMP: runtimeTempDir,
18
273
  },
19
- );
20
-
21
- process.exit(result.status ?? 1);
274
+ });
275
+
276
+ if (result.status !== 0) {
277
+ const output = `${result.stdout || ''}${result.stderr || ''}`.trim();
278
+ console.error(output || `OpenClaw smoke failed with exit code ${result.status}`);
279
+ process.exit(result.status ?? 1);
280
+ }
281
+
282
+ const payload = parseLastJsonObject(result.stdout || '');
283
+ if (!payload.plugin) {
284
+ console.error(`OpenClaw smoke did not load stella-timeline-plugin.\n${result.stdout || ''}`);
285
+ process.exit(1);
286
+ }
287
+ if (payload.plugin.status !== 'loaded') {
288
+ console.error(`OpenClaw smoke loaded stella-timeline-plugin with unexpected status: ${payload.plugin.status}`);
289
+ process.exit(1);
290
+ }
291
+ if (!Array.isArray(payload.resolvedToolNames) || !payload.resolvedToolNames.includes('timeline_resolve')) {
292
+ console.error(`OpenClaw smoke did not resolve timeline_resolve into runtime tools.\n${JSON.stringify(payload)}`);
293
+ process.exit(1);
294
+ }
295
+
296
+ console.log(`OpenClaw smoke passed using ${runtimeModule}`);
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from 'node:fs';
3
+ import os from 'node:os';
3
4
  import path from 'node:path';
4
5
  import {
5
6
  buildAgentsContract,
@@ -10,11 +11,17 @@ import {
10
11
  resolveCanonicalRootPath,
11
12
  } from './workspace-contract.mjs';
12
13
 
14
+ const PLUGIN_ID = 'stella-timeline-plugin';
15
+ const DEFAULT_OPENCLAW_CONFIG = path.join(os.homedir(), '.openclaw', 'openclaw.json');
16
+
13
17
  function parseArgs(argv) {
14
18
  const options = {
15
19
  workspace: path.resolve(process.cwd()),
16
20
  canonicalRootName: 'memory',
17
21
  createMemoryRoot: true,
22
+ pluginDir: path.resolve(process.cwd()),
23
+ openclawConfig: DEFAULT_OPENCLAW_CONFIG,
24
+ skipOpenclawConfig: false,
18
25
  };
19
26
 
20
27
  for (let i = 0; i < argv.length; i += 1) {
@@ -31,6 +38,18 @@ function parseArgs(argv) {
31
38
  options.createMemoryRoot = false;
32
39
  continue;
33
40
  }
41
+ if (arg === '--plugin-dir') {
42
+ options.pluginDir = path.resolve(argv[++i] || '');
43
+ continue;
44
+ }
45
+ if (arg === '--openclaw-config') {
46
+ options.openclawConfig = path.resolve(argv[++i] || '');
47
+ continue;
48
+ }
49
+ if (arg === '--skip-openclaw-config') {
50
+ options.skipOpenclawConfig = true;
51
+ continue;
52
+ }
34
53
  if (arg === '--help' || arg === '-h') {
35
54
  printHelp();
36
55
  process.exit(0);
@@ -43,9 +62,17 @@ function parseArgs(argv) {
43
62
 
44
63
  function printHelp() {
45
64
  console.log([
46
- 'Usage: openclaw-timeline-setup [--workspace <dir>] [--canonical-root-name <name>] [--no-create-memory-root]',
65
+ 'Usage: openclaw-timeline-setup [options]',
47
66
  '',
48
- 'Idempotently appends the required Timeline contract blocks to AGENTS.md and SOUL.md.',
67
+ 'Idempotently sets up the Timeline plugin: patches AGENTS.md, SOUL.md, and openclaw.json.',
68
+ '',
69
+ 'Options:',
70
+ ' --workspace <dir> Workspace to patch AGENTS.md / SOUL.md (default: cwd)',
71
+ ' --canonical-root-name <name> Memory root folder name (default: memory)',
72
+ ' --no-create-memory-root Skip creating the memory root directory',
73
+ ' --plugin-dir <dir> Plugin source directory to register in openclaw.json (default: cwd)',
74
+ ' --openclaw-config <path> Path to openclaw.json (default: ~/.openclaw/openclaw.json)',
75
+ ' --skip-openclaw-config Skip patching openclaw.json',
49
76
  ].join('\n'));
50
77
  }
51
78
 
@@ -75,6 +102,47 @@ function writeFile(filePath, content) {
75
102
  fs.writeFileSync(filePath, content, 'utf8');
76
103
  }
77
104
 
105
+ function mergeUniqueArray(existing, value) {
106
+ if (!Array.isArray(existing)) return [value];
107
+ return existing.includes(value) ? existing : [...existing, value];
108
+ }
109
+
110
+ function patchOpenclawConfig(configPath, pluginDir) {
111
+ if (!fs.existsSync(configPath)) {
112
+ return { changed: false, skipped: true, reason: `${configPath} not found` };
113
+ }
114
+
115
+ let config;
116
+ try {
117
+ config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
118
+ } catch {
119
+ return { changed: false, skipped: true, reason: `${configPath} is not valid JSON` };
120
+ }
121
+
122
+ const original = JSON.stringify(config);
123
+
124
+ // plugins.allow
125
+ config.plugins = config.plugins || {};
126
+ config.plugins.allow = mergeUniqueArray(config.plugins.allow, PLUGIN_ID);
127
+
128
+ // plugins.load.paths
129
+ config.plugins.load = config.plugins.load || {};
130
+ config.plugins.load.paths = mergeUniqueArray(config.plugins.load.paths, pluginDir);
131
+
132
+ // plugins.entries.<id>.enabled
133
+ config.plugins.entries = config.plugins.entries || {};
134
+ config.plugins.entries[PLUGIN_ID] = config.plugins.entries[PLUGIN_ID] || {};
135
+ config.plugins.entries[PLUGIN_ID].enabled = true;
136
+
137
+ const updated = JSON.stringify(config);
138
+ if (original === updated) {
139
+ return { changed: false, skipped: false };
140
+ }
141
+
142
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
143
+ return { changed: true, skipped: false };
144
+ }
145
+
78
146
  function main() {
79
147
  const options = parseArgs(process.argv.slice(2));
80
148
  const agentsPath = path.join(options.workspace, 'AGENTS.md');
@@ -108,6 +176,15 @@ function main() {
108
176
  `${options.createMemoryRoot ? 'ensured' : 'skipped'} ${canonicalRootPath}`,
109
177
  ];
110
178
 
179
+ if (!options.skipOpenclawConfig) {
180
+ const configResult = patchOpenclawConfig(options.openclawConfig, options.pluginDir);
181
+ if (configResult.skipped) {
182
+ updates.push(`skipped ${options.openclawConfig} (${configResult.reason})`);
183
+ } else {
184
+ updates.push(`${configResult.changed ? 'updated' : 'kept'} ${options.openclawConfig}`);
185
+ }
186
+ }
187
+
111
188
  console.log(updates.join('\n'));
112
189
  }
113
190
 
@@ -1,111 +1,111 @@
1
- ---
2
- name: timeline
3
- description: 当问题在询问当前状态、过去某个具体时间点、过去一段时间,或下游技能需要时间状态事实时,先进入 timeline skill,再把原始自然语言 query 交给 Timeline;禁止只根据人设直接回答。
4
- ---
5
-
6
- # Timeline
7
-
8
- 遇到和时间状态或回忆有关的请求时,先调用 Timeline,再组织回答。不要先按人设猜,也不要先直接读 `memory/` 自己下结论。
9
-
10
- ## 这个 skill 负责什么
11
-
12
- 这个 skill 只负责两件事:
13
-
14
- 1. 判断这次请求是否属于 Timeline 应接管的时间现实问题。
15
- 2. 把用户原话或下游技能的自然语言问题,原样交给 `timeline_resolve`。
16
-
17
- 它不直接生成事实,也不负责自己归一化时间,更不负责绕过 Timeline 直接回答。
18
- 当目标时间缺少既有事实时,生成与写入的决定交给 Timeline 内部处理。
19
-
20
- ## 强制规则
21
-
22
- 1. 涉及时间现实的问题,先调用 `timeline_resolve`,后回答。
23
- 2. 不要只根据 `SOUL.md`、`IDENTITY.md`、`MEMORY.md` 直接回答“现在在做什么”或“最近发生了什么”。
24
- 3. `SOUL` / `IDENTITY` / `MEMORY` 只用于帮助理解和表述,不能替代 Timeline 事实。
25
- 4. 不要在调用前自行把 query 改写成结构化时间参数。
26
- 5. 时间分类与时间归一化统一交给 Timeline 内部处理。
27
- 6. `timeline_resolve` 返回既有事实时,优先复用;不要改写它。
28
- 7. 如果用户是普通闲聊而不是严格只读校验,允许 Timeline 生成缺失事实并写入 canon
29
-
30
- ## 调用格式
31
-
32
- 默认主路径:
33
-
34
- ```json
35
- {
36
- "query": "用户的原话"
37
- }
38
- ```
39
-
40
- 不要向 `timeline_resolve` 传时间点、时间范围、请求类型、模式或调试字段。它的公开入口就是自然语言 query
41
-
42
- ## 场景拆分
43
-
44
- ### 场景 A:当前状态
45
-
46
- 抽象描述:
47
-
48
- - 用户在问“此刻”或“当前这一小段时间”的状态。
49
- - 目标是回答现在在哪里、在做什么、是什么状态。
50
-
51
- 示例:
52
-
53
- - “你在干嘛”
54
- - “你现在在哪”
55
- - “你现在什么状态”
56
- - “你刚才不是在打球吗,现在还在吗”
57
- - 下游技能要取当前状态,例如自拍、场景描述、状态卡片
58
-
59
- 处理方式:
60
-
61
- - `timeline_resolve`
62
- - `query` 就是用户原话
63
- - 不要自己先判断它是不是 `now`
64
-
65
- ### 场景 B:过去某个具体时间点
66
-
67
- 抽象描述:
68
-
69
- - 用户在问某个足够明确的时间点。
70
- - 目标是围绕明确时间锚点回忆事实。
71
-
72
- 示例:
73
-
74
- - “昨晚八点你在做什么”
75
- - “你昨天上午十点在哪”
76
- - “你昨晚八点是不是还在看电视”
77
- - “上周六晚上九点你在忙什么”
78
-
79
- 处理方式:
80
-
81
- - `timeline_resolve`
82
- - `query` 就是用户原话
83
- - 不要自己把时间点归一化后再传
84
-
85
- ### 场景 C:过去一段时间
86
-
87
- 抽象描述:
88
-
89
- - 用户在问一个时间段内的整体情况,而不是某个点。
90
- - 目标是组织一段时间里的回忆、活动概览或最近动态。
91
-
92
- 示例:
93
-
94
- - “最近有什么有趣的事吗”
95
- - “你最近都在忙什么”
96
- - “这几天怎么样”
97
- - “你今天都忙了什么”
98
- - “昨晚在做什么”
99
-
100
- 处理方式:
101
-
102
- - `timeline_resolve`
103
- - `query` 就是用户原话
104
- - “最近”“昨晚”“今天都”这类自然语言范围,统一交给 Timeline 内部 planner 理解
105
-
106
- ## 回答要求
107
-
108
- - 对用户说人话,不要提 `timeline_resolve`。
109
- - 语气自然,像真人在回忆或描述当下。
110
- - 如果 Timeline 返回的是空窗口或失败,不要假装已有确定事实;按可用结果谨慎表达。
111
- - 除非用户明确要求看原始结果,否则不要输出 JSON
1
+ ---
2
+ name: timeline
3
+ description: When a request asks about the current state, a specific past time, a past range, or a downstream skill needs time-state facts, enter the timeline skill first and pass the raw natural-language query to Timeline. Never answer from persona alone.
4
+ ---
5
+
6
+ # Timeline
7
+
8
+ When a request is about time-state facts or recall, call Timeline first and then compose the reply. Do not guess from persona first, and do not read `memory/` directly and make your own conclusion.
9
+
10
+ ## Scope
11
+
12
+ This skill does only two things:
13
+
14
+ 1. Decide whether the request is a time-grounded reality question that Timeline should own.
15
+ 2. Pass the user's exact wording, or the downstream skill's natural-language question, straight into `timeline_resolve`.
16
+
17
+ It does not generate facts directly, normalize time on its own, or bypass Timeline and answer directly.
18
+ When the target time lacks an existing fact, Timeline decides whether to generate and write.
19
+
20
+ ## Hard Rules
21
+
22
+ 1. For time-reality questions, call `timeline_resolve` first and answer second.
23
+ 2. Do not answer "what are you doing now" or "what happened recently" from `SOUL.md`, `IDENTITY.md`, or `MEMORY.md` alone.
24
+ 3. `SOUL` / `IDENTITY` / `MEMORY` help with interpretation and phrasing, but they cannot replace Timeline facts.
25
+ 4. Do not rewrite the query into structured time parameters before the call.
26
+ 5. Let Timeline own time classification and time normalization.
27
+ 6. If `timeline_resolve` returns an existing fact, reuse it as-is.
28
+ 7. If the user is doing ordinary conversation rather than strict read-only verification, Timeline may generate missing facts and write canon.
29
+
30
+ ## Call Shape
31
+
32
+ Default path:
33
+
34
+ ```json
35
+ {
36
+ "query": "the user's exact words"
37
+ }
38
+ ```
39
+
40
+ Do not pass time points, time ranges, request types, modes, or debug fields into `timeline_resolve`. Its public interface is the natural-language query.
41
+
42
+ ## Scenarios
43
+
44
+ ### Scenario A: Current State
45
+
46
+ Abstract shape:
47
+
48
+ - The user is asking about "right now" or the current short stretch of time.
49
+ - The goal is to answer where she is, what she is doing, or what state she is in.
50
+
51
+ Examples:
52
+
53
+ - “你在干嘛”
54
+ - “你现在在哪”
55
+ - “你现在什么状态”
56
+ - “你刚才不是在打球吗,现在还在吗”
57
+ - A downstream skill needs the current state, such as a selfie, scene description, or status card
58
+
59
+ Handling:
60
+
61
+ - Call `timeline_resolve`
62
+ - Set `query` to the user's exact wording
63
+ - Do not decide `now` yourself before calling
64
+
65
+ ### Scenario B: A Specific Past Time
66
+
67
+ Abstract shape:
68
+
69
+ - The user is asking about a sufficiently specific point in time.
70
+ - The goal is to recall facts around that time anchor.
71
+
72
+ Examples:
73
+
74
+ - “昨晚八点你在做什么”
75
+ - “你昨天上午十点在哪”
76
+ - “你昨晚八点是不是还在看电视”
77
+ - “上周六晚上九点你在忙什么”
78
+
79
+ Handling:
80
+
81
+ - Call `timeline_resolve`
82
+ - Set `query` to the user's exact wording
83
+ - Do not normalize the time point yourself before passing it through
84
+
85
+ ### Scenario C: A Past Range
86
+
87
+ Abstract shape:
88
+
89
+ - The user is asking about the overall state of a time range rather than a single point.
90
+ - The goal is to organize recall, an activity overview, or recent updates from that range.
91
+
92
+ Examples:
93
+
94
+ - “最近有什么有趣的事吗”
95
+ - “你最近都在忙什么”
96
+ - “这几天怎么样”
97
+ - “你今天都忙了什么”
98
+ - “昨晚在做什么”
99
+
100
+ Handling:
101
+
102
+ - Call `timeline_resolve`
103
+ - Set `query` to the user's exact wording
104
+ - Let Timeline's internal planner interpret natural-language ranges such as “最近”, “昨晚”, or “今天都”
105
+
106
+ ## Reply Requirements
107
+
108
+ - Speak naturally to the user and do not mention `timeline_resolve`.
109
+ - Keep the tone human, like natural recall or present-moment description.
110
+ - If Timeline returns an empty window or a failure, do not pretend you have certain facts; phrase the answer cautiously from what is available.
111
+ - Do not output JSON unless the user explicitly asks for raw results.