hyacinth-ai 0.9.14 → 0.9.16

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.
@@ -54,7 +54,7 @@ function checkEncoding() {
54
54
  }
55
55
  function checkPersona() {
56
56
  const personaDir = path.join(os.homedir(), '.agent', 'prompts', 'persona');
57
- const files = ['SOUL.md', 'IDENTITY.md', 'USER.md', 'BOOTSTRAP.md'];
57
+ const files = ['SOUL.md', 'IDENTITY.md', 'USER.md'];
58
58
  const existing = [];
59
59
  const missing = [];
60
60
  for (const f of files) {
@@ -64,20 +64,15 @@ function checkPersona() {
64
64
  else
65
65
  missing.push(f);
66
66
  }
67
- const hasBootstrap = existing.includes('BOOTSTRAP.md');
68
67
  return {
69
68
  label: 'Persona 文件',
70
- ok: existing.length >= 3 && !hasBootstrap,
71
- detail: hasBootstrap
72
- ? `BOOTSTRAP.md 仍存在 — 需要完成身份初始化(${existing.length}/4 文件存在)`
73
- : missing.length > 0
74
- ? `缺失: ${missing.join(', ')}`
75
- : `完整 (${existing.length}/4 文件,bootstrap 已完成)`,
69
+ ok: existing.length >= 3,
70
+ detail: missing.length > 0
71
+ ? `缺失: ${missing.join(', ')}`
72
+ : `完整 (${existing.length}/3 文件)`,
76
73
  fix: missing.length > 0
77
74
  ? '运行 hyacinth setup 完成初始化'
78
- : hasBootstrap
79
- ? '启动 TUI 并完成身份对话,或运行 hyacinth setup'
80
- : undefined,
75
+ : undefined,
81
76
  };
82
77
  }
83
78
  function checkDeps() {
@@ -267,7 +262,6 @@ export async function runDoctor(opts = {}) {
267
262
  console.log('\n── 自动修复 ──');
268
263
  // 修复 persona 文件
269
264
  const personaDir = path.join(os.homedir(), '.agent', 'prompts', 'persona');
270
- const bootstrapPath = path.join(personaDir, 'BOOTSTRAP.md');
271
265
  if (!fs.existsSync(personaDir)) {
272
266
  fs.mkdirSync(personaDir, { recursive: true });
273
267
  }
@@ -279,10 +273,6 @@ export async function runDoctor(opts = {}) {
279
273
  personaCreated++;
280
274
  }
281
275
  }
282
- // 确保 BOOTSTRAP.md 存在(触发 bootstrap 流程)
283
- if (!fs.existsSync(bootstrapPath) && personaCreated > 0) {
284
- fs.writeFileSync(bootstrapPath, '# Bootstrap\n请与用户对话,了解其偏好后填写 IDENTITY.md 和 USER.md。', 'utf-8');
285
- }
286
276
  if (personaCreated > 0) {
287
277
  console.log(` ✅ 创建了 ${personaCreated} 个 Persona 文件`);
288
278
  }
@@ -20,7 +20,7 @@ import { ConfigManager, API_KEY_MAP, DEFAULT_MAX_CONTEXT_TOKENS } from '../setup
20
20
  import { getModelContextWindow } from '../setup/model-defaults.js';
21
21
  import { SetupWizard } from '../setup/wizard.js';
22
22
  import { PROVIDER_MODELS } from '../setup/model-defaults.js';
23
- import { DEFAULT_PERSONA_DIR, ensurePersonaFiles, getBootstrapStatus } from '../setup/persona-bootstrap.js';
23
+ import { DEFAULT_PERSONA_DIR, ensurePersonaFiles } from '../setup/persona-bootstrap.js';
24
24
  import { createLogger } from '../logging/logger.js';
25
25
  import { getDefaultConfig } from '../runtime/defaults.js';
26
26
  const logger = createLogger('cli');
@@ -184,7 +184,6 @@ export async function runCli() {
184
184
  // 初始化 persona 文件
185
185
  const personaDir = DEFAULT_PERSONA_DIR;
186
186
  await ensurePersonaFiles(personaDir);
187
- const bootstrapStatus = await getBootstrapStatus(personaDir);
188
187
  // 如果用户选择进入 TUI,自动启动
189
188
  if (result.enterTui && result.config.provider) {
190
189
  await configManager.loadEnvKeys();
@@ -195,7 +194,6 @@ export async function runCli() {
195
194
  maxContext: String(result.config.maxContext),
196
195
  tui: true,
197
196
  skipSetup: true,
198
- bootstrapStatus,
199
197
  });
200
198
  }
201
199
  });
@@ -686,7 +684,6 @@ async function executeAction(prompt, options) {
686
684
  const startModel = options.startModel;
687
685
  const skipSetup = options.skipSetup;
688
686
  const localModelName = options.localModel;
689
- const bootstrapStatus = options.bootstrapStatus;
690
687
  // 加载 .env 中的 API Key
691
688
  const configManager = new ConfigManager(process.cwd());
692
689
  await configManager.loadEnvKeys();
@@ -794,7 +791,7 @@ async function executeAction(prompt, options) {
794
791
  continuationMessage = fsSync.readFileSync(continuationFile, 'utf-8').trim() || undefined;
795
792
  fsSync.unlinkSync(continuationFile);
796
793
  }
797
- await runTui(provider, sessionId, shouldContinue ?? false, maxTurns, maxContext, maxMessages, skipSetup, DEFAULT_PERSONA_DIR, bootstrapStatus, localModelProvider, continuationMessage);
794
+ await runTui(provider, sessionId, shouldContinue ?? false, maxTurns, maxContext, maxMessages, skipSetup, DEFAULT_PERSONA_DIR, localModelProvider, continuationMessage);
798
795
  return;
799
796
  }
800
797
  // Session + 模块初始化(由工厂统一处理)
@@ -813,10 +810,6 @@ async function executeAction(prompt, options) {
813
810
  sessionDir = agentResult.sessionDir;
814
811
  // 注入 LifecycleSupervisor,实现运行时 provider 切换时自动管理本地模型进程
815
812
  loop.setLifecycleSupervisor(supervisor);
816
- // Bootstrap Flow 已在 factory 中自动激活,发送空消息启动引导对话
817
- if (loop.bootstrapActive) {
818
- await loop.run('');
819
- }
820
813
  // 检测重启续工指令
821
814
  if (!prompt) {
822
815
  const continuationFile = path.join(os.homedir(), '.agent', '.restart-continuation');
@@ -12,7 +12,6 @@ import { AgentLoop } from '../orchestrator/loop.js';
12
12
  import type { Provider } from '../provider/interface.js';
13
13
  import type { DependencyAnalyzer } from '../dependency/analyzer.js';
14
14
  import type { OutputHandler } from '../orchestrator/loop.js';
15
- import type { BootstrapStatus } from '../setup/persona-bootstrap.js';
16
15
  import { ModelRouter } from '../provider/model-router.js';
17
16
  import { HeartbeatScheduler } from '../schedule/scheduler.js';
18
17
  import { HotReloadManager } from '../hot-reload/index.js';
@@ -31,10 +30,8 @@ export interface CreateAgentOptions {
31
30
  shouldContinue?: boolean;
32
31
  /** 每个 session 的最大消息数 */
33
32
  maxMessages?: number;
34
- /** Bootstrap 工作区目录 */
33
+ /** Persona 文件目录 */
35
34
  personaDir?: string;
36
- /** Bootstrap 状态 */
37
- bootstrapStatus?: BootstrapStatus;
38
35
  /** 本地模型 Provider(用于压缩通道,不影响主对话) */
39
36
  localModelProvider?: Provider;
40
37
  /** 渠道信息(注入到 System Prompt 的 environment section) */
@@ -27,7 +27,7 @@ import { MemoryStore } from '../memory/memory-store.js';
27
27
  import { AgentLoop } from '../orchestrator/loop.js';
28
28
  import { PluginManager } from '../plugins/index.js';
29
29
  import { initDependencyAnalyzer } from '../dependency/index.js';
30
- import { ensureGlobalPersonaFiles, ensureGlobalPromptDir, getBootstrapStatus } from '../setup/persona-bootstrap.js';
30
+ import { ensureGlobalPersonaFiles, ensureGlobalPromptDir } from '../setup/persona-bootstrap.js';
31
31
  import { ConfigManager } from '../setup/config.js';
32
32
  import { RuntimeConfigCenter } from '../runtime/config-center.js';
33
33
  import { getDefaultConfig } from '../runtime/defaults.js';
@@ -45,7 +45,7 @@ import { injectConfigCenter } from '../provider/local-config.js';
45
45
  import { getModelContextWindow } from '../setup/model-defaults.js';
46
46
  import { modelCatalog } from '../provider/catalog.js';
47
47
  import { TurnRecorder, TurnStore, createRollbackStatusTool, createRollbackTool } from '../rollback/index.js';
48
- import { MachineRegistry, BootstrapFlow, TodoFlow, SpecFlow } from '../machine/index.js';
48
+ import { MachineRegistry, TodoFlow, SpecFlow } from '../machine/index.js';
49
49
  import { createAskUserTool } from '../tools/ask-user.js';
50
50
  import { createFlowStartTool, createFlowAddTool, createFlowCompleteTool } from '../tools/flow.js';
51
51
  import { createTriggerCompressionTool } from '../tools/compression.js';
@@ -73,7 +73,7 @@ const logger = createLogger('factory');
73
73
  //
74
74
  // ─────────────────────────────────────────────────────────────────────
75
75
  export async function createAgent(options, supervisor) {
76
- const { cwd, provider, maxTurns, maxContext, outputHandler, sessionId, shouldContinue, maxMessages = 10000, personaDir, bootstrapStatus, localModelProvider, channelsInfo } = options;
76
+ const { cwd, provider, maxTurns, maxContext, outputHandler, sessionId, shouldContinue, maxMessages = 10000, personaDir, localModelProvider, channelsInfo } = options;
77
77
  // ── 配置预加载(需在 session 创建前读取 startup.defaultMode)──────
78
78
  const configManager = new ConfigManager(cwd);
79
79
  const config = await configManager.load();
@@ -118,7 +118,6 @@ export async function createAgent(options, supervisor) {
118
118
  // ── Global Persona Bootstrap ──────────────────────────────────────
119
119
  const personaSetup = await ensureGlobalPersonaFiles();
120
120
  const effectivePersonaDir = personaDir ?? personaSetup.personaDir;
121
- const effectiveBootstrapStatus = bootstrapStatus ?? (await getBootstrapStatus(effectivePersonaDir));
122
121
  // ── 内置 Prompt 同步 ─────────────────────────────────────────────
123
122
  // 将所有内置 prompt 同步到 ~/.agent/prompts/ 下,
124
123
  // 使得 loadPrompt() 的查找优先级正确:外部覆盖 > 内置兜底。
@@ -130,8 +129,7 @@ export async function createAgent(options, supervisor) {
130
129
  await ensureGlobalPromptDir('precise');
131
130
  await ensureGlobalPromptDir('environment');
132
131
  // root 级文件 summary.md 由 loadPrompt 递归搜索找到,暂不单独同步
133
- // ── 注册 Flow(Bootstrap + TODO + Spec)──────────────────────────
134
- flowRegistry.register(new BootstrapFlow(effectivePersonaDir));
132
+ // ── 注册 Flow(TODO + Spec)────────────────────────────────
135
133
  flowRegistry.register(new TodoFlow());
136
134
  flowRegistry.register(new SpecFlow());
137
135
  // ── Provider Config Loader(必须在 getDefaultConfig 之前,确保 providerDefault 读到 JSON) ──
@@ -523,10 +521,6 @@ export async function createAgent(options, supervisor) {
523
521
  // 注入旁路 Provider 引用(供模式切换时 setBypassUserId 使用)
524
522
  if (bypassProvider)
525
523
  loop.setBypassProvider(bypassProvider);
526
- // ── Bootstrap Flow 自动激活(首次安装后自动运行) ──
527
- if (effectiveBootstrapStatus === 'pending') {
528
- flowRegistry.activate('bootstrap');
529
- }
530
524
  // 注册后台进程注册表到 LifecycleSupervisor(优雅关闭时自动清理)
531
525
  if (supervisor) {
532
526
  supervisor.registerBackgroundRegistry(backgroundRegistry);
@@ -11,6 +11,5 @@
11
11
  * editor — user input area
12
12
  */
13
13
  import type { Provider } from '../provider/interface.js';
14
- import type { BootstrapStatus } from '../setup/persona-bootstrap.js';
15
- export declare function runTui(provider: Provider, sessionId: string | undefined, shouldContinue: boolean, maxTurns: number, maxContext: number, maxMessages?: number, skipSetup?: boolean, personaDir?: string, bootstrapStatus?: BootstrapStatus, localModelProvider?: Provider, continuationMessage?: string): Promise<void>;
14
+ export declare function runTui(provider: Provider, sessionId: string | undefined, shouldContinue: boolean, maxTurns: number, maxContext: number, maxMessages?: number, skipSetup?: boolean, personaDir?: string, localModelProvider?: Provider, continuationMessage?: string): Promise<void>;
16
15
  //# sourceMappingURL=tui.d.ts.map
@@ -23,7 +23,7 @@ import { RuntimeConfigCenter } from '../runtime/config-center.js';
23
23
  import { ProviderManager } from '../provider/manager.js';
24
24
  import { LifecycleSupervisor } from '../lifecycle/index.js';
25
25
  import { createAgent } from './factory.js';
26
- import { getBootstrapStatus, DEFAULT_PERSONA_DIR } from '../setup/persona-bootstrap.js';
26
+ import { DEFAULT_PERSONA_DIR } from '../setup/persona-bootstrap.js';
27
27
  import { createLogger } from '../logging/logger.js';
28
28
  import { TuiChannel } from '../channels/builtin/tui-channel.js';
29
29
  import { ChannelManager } from '../channels/manager.js';
@@ -197,7 +197,7 @@ function replayEvents(chatLog, sessionDir) {
197
197
  });
198
198
  }
199
199
  // ─── Main TUI ─────────────────────────────────────────────────────────────
200
- export async function runTui(provider, sessionId, shouldContinue, maxTurns, maxContext, maxMessages = 10000, skipSetup, personaDir = DEFAULT_PERSONA_DIR, bootstrapStatus, localModelProvider, continuationMessage) {
200
+ export async function runTui(provider, sessionId, shouldContinue, maxTurns, maxContext, maxMessages = 10000, skipSetup, personaDir = DEFAULT_PERSONA_DIR, localModelProvider, continuationMessage) {
201
201
  // ── Force UTF-8 console encoding on Windows ──
202
202
  if (process.platform === 'win32') {
203
203
  try {
@@ -716,8 +716,6 @@ export async function runTui(provider, sessionId, shouldContinue, maxTurns, maxC
716
716
  });
717
717
  },
718
718
  };
719
- // ── Bootstrap detection ──
720
- const resolvedBootstrapStatus = bootstrapStatus ?? (await getBootstrapStatus(personaDir));
721
719
  // ── Local Model (llama.cpp) ──
722
720
  try {
723
721
  const loadedModels = await supervisor.loadAndStartModels(process.cwd());
@@ -793,7 +791,6 @@ export async function runTui(provider, sessionId, shouldContinue, maxTurns, maxC
793
791
  shouldContinue,
794
792
  maxMessages,
795
793
  personaDir,
796
- bootstrapStatus: resolvedBootstrapStatus,
797
794
  localModelProvider,
798
795
  channel: 'tui',
799
796
  });
@@ -821,7 +818,6 @@ export async function runTui(provider, sessionId, shouldContinue, maxTurns, maxC
821
818
  setScheduler: () => { },
822
819
  notifyTaskFired: async () => { },
823
820
  subscribeConfig: () => { },
824
- isBootstrapPending: () => false,
825
821
  shutdown: async () => { },
826
822
  };
827
823
  // 远程模式下 agent 为 undefined,这些变量仅用于本地 TUI 功能(斜杠命令等),用 any 避免 null 检查
@@ -1049,22 +1045,6 @@ export async function runTui(provider, sessionId, shouldContinue, maxTurns, maxC
1049
1045
  chatLog.addSystem('');
1050
1046
  chatLog.addSystem(theme.dim('Persona: ') + theme.accent(personaDir));
1051
1047
  chatLog.addSystem('');
1052
- // ── Bootstrap ──
1053
- if (resolvedBootstrapStatus === 'pending') {
1054
- chatLog.addSystem(theme.warning('\u256d' + BOX_H.repeat(56) + '\u256e'));
1055
- chatLog.addSystem(theme.warning('\u2502 \uD83D\uDD27 Bootstrap Init'));
1056
- chatLog.addSystem(theme.warning('\u2502 AI will ask you setup questions to configure the persona.'));
1057
- chatLog.addSystem(theme.warning('\u2570' + BOX_H.repeat(56) + '\u256f'));
1058
- chatLog.addSystem('');
1059
- try {
1060
- // Flow 已在 factory.ts 中自动激活,发送空消息让模型根据 flow_injection 引导对话
1061
- await loop.run('');
1062
- }
1063
- catch (error) {
1064
- const message = error instanceof Error ? error.message : String(error);
1065
- chatLog.addSystem(theme.errorBright('[Bootstrap Error] ') + theme.error(message));
1066
- }
1067
- }
1068
1048
  chatLog.addSystem(theme.dim('Type ') +
1069
1049
  theme.success('exit') +
1070
1050
  theme.dim(' to quit. ') +
@@ -3,6 +3,5 @@ export { MachineRunner } from './runner.js';
3
3
  export { MachineRegistry } from './registry.js';
4
4
  export type { FlowController } from './flows/types.js';
5
5
  export { TodoFlow } from './flows/todo.js';
6
- export { BootstrapFlow } from './flows/bootstrap.js';
7
6
  export { SpecFlow } from './flows/spec.js';
8
7
  //# sourceMappingURL=index.d.ts.map
@@ -4,6 +4,5 @@
4
4
  export { MachineRunner } from './runner.js';
5
5
  export { MachineRegistry } from './registry.js';
6
6
  export { TodoFlow } from './flows/todo.js';
7
- export { BootstrapFlow } from './flows/bootstrap.js';
8
7
  export { SpecFlow } from './flows/spec.js';
9
8
  //# sourceMappingURL=index.js.map
@@ -77,46 +77,45 @@ export class MachineRunner {
77
77
  if (this.status !== 'active') {
78
78
  return { ok: false, reason: `Machine "${this.definition.id}" is not active (status: ${this.status}).` };
79
79
  }
80
- // 1. 找匹配的转移
81
- const transition = this.definition.transitions.find((t) => {
80
+ // 1. 找匹配的转移(按优先级——跳过 guard 不通过的)
81
+ let transition;
82
+ for (const t of this.definition.transitions) {
82
83
  const fromMatch = Array.isArray(t.from)
83
84
  ? t.from.includes(this.currentState)
84
85
  : t.from === this.currentState;
85
- return fromMatch && t.event === event;
86
- });
86
+ if (fromMatch && t.event === event) {
87
+ if (t.guard) {
88
+ const guardResult = t.guard(this.context);
89
+ if (!guardResult.ok)
90
+ continue; // guard 不通过,试下一个
91
+ }
92
+ transition = t;
93
+ break;
94
+ }
95
+ }
87
96
  if (!transition) {
88
97
  return {
89
98
  ok: false,
90
99
  reason: `Event "${event}" is not valid in state "${this.currentState}" of machine "${this.definition.id}". Available events: [${this.getAvailableEvents().join(', ')}]`,
91
100
  };
92
101
  }
93
- // 2. Guard 检查
94
- if (transition.guard) {
95
- const guardResult = transition.guard(this.context);
96
- if (!guardResult.ok) {
97
- return {
98
- ok: false,
99
- reason: guardResult.reason ?? `Guard rejected transition from "${this.currentState}" to "${transition.to}" via "${event}".`,
100
- };
101
- }
102
- }
103
- // 3. onExit 副作用(fire-and-forget)
102
+ // 2. onExit 副作用(fire-and-forget)
104
103
  const oldState = this.currentState;
105
104
  const oldDef = this.definition.states[oldState];
106
105
  safeFire(oldDef?.onExit, this.context);
107
- // 4. onTransition 副作用(同步执行——可阻塞)
106
+ // 3. onTransition 副作用(同步执行——可阻塞)
108
107
  transition.onTransition?.(this.context);
109
- // 5. 更新状态
108
+ // 4. 更新状态
110
109
  this.currentState = transition.to;
111
- // 6. onEnter 副作用(fire-and-forget)
110
+ // 5. onEnter 副作用(fire-and-forget)
112
111
  const newDef = this.definition.states[this.currentState];
113
112
  safeFire(newDef?.onEnter, this.context);
114
- // 7. 记录历史
113
+ // 6. 记录历史
115
114
  this.transitionHistory.push({ from: oldState, to: transition.to, event });
116
115
  if (this.transitionHistory.length > MAX_HISTORY) {
117
116
  this.transitionHistory = this.transitionHistory.slice(-MAX_HISTORY);
118
117
  }
119
- // 8. 终端状态检查
118
+ // 7. 终端状态检查
120
119
  const isTerminal = this.definition.terminalStates?.includes(this.currentState) ?? false;
121
120
  if (isTerminal) {
122
121
  this.status = 'completed';
@@ -213,8 +213,6 @@ export declare class AgentLoop {
213
213
  /** Get current turn/mode info for status display */
214
214
  getTurnInfo(turnCount: number, tokensUsed: number): TurnInfo;
215
215
  private getRecentToolCallNames;
216
- /** Bootstrap Flow 是否活跃(供 CLI/TUI 判断是否需要发送空消息启动引导) */
217
- get bootstrapActive(): boolean;
218
216
  /** 就地切换到指定 session,无需重启进程 */
219
217
  switchSession(newSessionDir: string): Promise<void>;
220
218
  /** 设置调度器 */
@@ -411,10 +411,6 @@ export class AgentLoop {
411
411
  getRecentToolCallNames() {
412
412
  return this.recentToolNames;
413
413
  }
414
- /** Bootstrap Flow 是否活跃(供 CLI/TUI 判断是否需要发送空消息启动引导) */
415
- get bootstrapActive() {
416
- return this.flowRegistry.getActive()?.id === 'bootstrap';
417
- }
418
414
  /** 就地切换到指定 session,无需重启进程 */
419
415
  async switchSession(newSessionDir) {
420
416
  this.sessionDir = newSessionDir;
@@ -1,9 +1,7 @@
1
- declare const PERSONA_FILE_NAMES: readonly ["BOOTSTRAP.md", "SOUL.md", "IDENTITY.md", "USER.md", "PartnerSoul.md", "PartnerMemory.md"];
1
+ declare const PERSONA_FILE_NAMES: readonly ["SOUL.md", "IDENTITY.md", "USER.md", "PartnerSoul.md", "PartnerMemory.md"];
2
2
  export type PersonaFileName = (typeof PERSONA_FILE_NAMES)[number];
3
- export type BootstrapStatus = 'pending' | 'complete';
4
3
  export interface PersonaState {
5
4
  version: number;
6
- bootstrapSeededAt?: string;
7
5
  setupCompletedAt?: string;
8
6
  }
9
7
  export interface PersonaFile {
@@ -22,29 +20,25 @@ export declare const DEFAULT_PERSONA_DIR: string;
22
20
  /**
23
21
  * 确保内置 prompt 目录同步到 ~/.agent/prompts/{name}/。
24
22
  *
25
- * 和 persona 不同(有 bootstrap 状态机),这只是一个简单的文件复制:
23
+ * 这只是一个简单的文件复制:
26
24
  * 首次安装时复制,已存在则跳过。
27
25
  *
28
26
  * 用于 attention.md 等非 persona 的内置 prompt 文件。
29
27
  */
30
28
  export declare function ensureGlobalPromptDir(name: string, configHome?: string): Promise<string[]>;
31
29
  export declare function ensurePersonaFiles(personaDir: string): Promise<{
32
- status: BootstrapStatus;
30
+ needsSetup: boolean;
33
31
  filesCreated: string[];
34
32
  state: PersonaState;
35
33
  }>;
36
34
  export declare function ensureGlobalPersonaFiles(configHome?: string): Promise<{
37
- status: BootstrapStatus;
35
+ needsSetup: boolean;
38
36
  filesCreated: string[];
39
37
  state: PersonaState;
40
38
  personaDir: string;
41
39
  }>;
42
40
  export declare function validatePersonaFiles(personaDir: string): Promise<PersonaValidationResult>;
43
41
  export declare function validatePersonaFilesSync(personaDir: string): PersonaValidationResult;
44
- export declare function isBootstrapComplete(personaDir: string): Promise<boolean>;
45
- export declare function getBootstrapStatus(personaDir: string): Promise<BootstrapStatus>;
46
- export declare function markBootstrapComplete(personaDir: string): Promise<void>;
47
- export declare function markBootstrapCompleteSync(personaDir: string): void;
48
42
  export declare function loadPersonaFiles(personaDir: string, cwdFallback?: string): Promise<PersonaFile[]>;
49
43
  export declare function getPersonaState(personaDir: string): Promise<PersonaState>;
50
44
  export {};
@@ -11,7 +11,6 @@ const PERSONA_STATE_DIR = '.state';
11
11
  const PERSONA_STATE_FILE = 'persona-state.json';
12
12
  const PERSONA_STATE_VERSION = 1;
13
13
  const PERSONA_FILE_NAMES = [
14
- 'BOOTSTRAP.md',
15
14
  'SOUL.md',
16
15
  'IDENTITY.md',
17
16
  'USER.md',
@@ -97,7 +96,7 @@ async function fileContentDiffersFromTemplate(filePath, template) {
97
96
  /**
98
97
  * 确保内置 prompt 目录同步到 ~/.agent/prompts/{name}/。
99
98
  *
100
- * 和 persona 不同(有 bootstrap 状态机),这只是一个简单的文件复制:
99
+ * 这只是一个简单的文件复制:
101
100
  * 首次安装时复制,已存在则跳过。
102
101
  *
103
102
  * 用于 attention.md 等非 persona 的内置 prompt 文件。
@@ -150,9 +149,7 @@ export async function ensurePersonaFiles(personaDir) {
150
149
  await fsp.mkdir(personaDir, { recursive: true });
151
150
  let state = await readState(personaDir);
152
151
  const now = new Date().toISOString();
153
- // 自动补检测:如果 persona 文件已被填写(内容 ≠ 模板)但 state 文件未反映
154
- //(例如 markBootstrapComplete 因格式验证过严而失败),
155
- // 在重建 BOOTSTRAP.md 之前标记为完成。这是修复"每次启动都重新引导"的 bug。
152
+ // 自动检测:如果 persona 文件已被填写(内容 ≠ 模板)但 state 文件未反映,自动标记完成
156
153
  if (!state.setupCompletedAt) {
157
154
  try {
158
155
  const identityPath = path.join(personaDir, 'IDENTITY.md');
@@ -173,8 +170,6 @@ export async function ensurePersonaFiles(personaDir) {
173
170
  const alreadyComplete = !!state.setupCompletedAt;
174
171
  const filesCreated = [];
175
172
  for (const fileName of PERSONA_FILE_NAMES) {
176
- if (alreadyComplete && fileName === 'BOOTSTRAP.md')
177
- continue;
178
173
  const filePath = path.join(personaDir, fileName);
179
174
  const template = loadTemplateContent(fileName);
180
175
  const created = await writeFileIfMissing(filePath, template);
@@ -183,38 +178,27 @@ export async function ensurePersonaFiles(personaDir) {
183
178
  logger.info('Created persona file', { file: fileName, dir: personaDir });
184
179
  }
185
180
  }
186
- const bootstrapPath = path.join(personaDir, 'BOOTSTRAP.md');
187
- let bootstrapExists = true;
188
- try {
189
- await fsp.access(bootstrapPath);
190
- }
191
- catch {
192
- bootstrapExists = false;
193
- }
194
- if (bootstrapExists && !state.bootstrapSeededAt) {
195
- state = { ...state, bootstrapSeededAt: now };
196
- }
181
+ // 二次检查:如果文件已被修改但 state 尚未记录
197
182
  if (!state.setupCompletedAt) {
198
- const identityPath = path.join(personaDir, 'IDENTITY.md');
199
- const userPath = path.join(personaDir, 'USER.md');
200
- const identityTemplate = loadTemplateContent('IDENTITY.md');
201
- const userTemplate = loadTemplateContent('USER.md');
202
- const identityChanged = await fileContentDiffersFromTemplate(identityPath, identityTemplate);
203
- const userChanged = await fileContentDiffersFromTemplate(userPath, userTemplate);
204
- if ((identityChanged || userChanged) && !bootstrapExists) {
205
- state = { ...state, setupCompletedAt: now };
206
- logger.info('Persona setup detected as complete (user-modified files)');
207
- }
208
- else if (!bootstrapExists && state.bootstrapSeededAt && !state.setupCompletedAt) {
183
+ try {
184
+ const identityPath = path.join(personaDir, 'IDENTITY.md');
185
+ const userPath = path.join(personaDir, 'USER.md');
186
+ const identityTemplate = loadTemplateContent('IDENTITY.md');
187
+ const userTemplate = loadTemplateContent('USER.md');
188
+ const identityChanged = await fileContentDiffersFromTemplate(identityPath, identityTemplate);
189
+ const userChanged = await fileContentDiffersFromTemplate(userPath, userTemplate);
209
190
  if (identityChanged || userChanged) {
210
191
  state = { ...state, setupCompletedAt: now };
211
- logger.info('Persona setup auto-completed (seeded + files modified + bootstrap deleted)');
192
+ logger.info('Persona setup detected as complete (user-modified files)');
212
193
  }
213
194
  }
195
+ catch {
196
+ // 文件可能尚未创建
197
+ }
214
198
  }
215
199
  await writeState(personaDir, state);
216
- const status = state.setupCompletedAt ? 'complete' : 'pending';
217
- return { status, filesCreated, state };
200
+ const needsSetup = !state.setupCompletedAt;
201
+ return { needsSetup, filesCreated, state };
218
202
  }
219
203
  export async function ensureGlobalPersonaFiles(configHome) {
220
204
  const personaDir = getGlobalPersonaDir(configHome);
@@ -281,98 +265,6 @@ export function validatePersonaFilesSync(personaDir) {
281
265
  templateFiles,
282
266
  };
283
267
  }
284
- export async function isBootstrapComplete(personaDir) {
285
- const state = await readState(personaDir);
286
- if (!state.setupCompletedAt)
287
- return false;
288
- const validation = await validatePersonaFiles(personaDir);
289
- return validation.complete;
290
- }
291
- export async function getBootstrapStatus(personaDir) {
292
- const state = await readState(personaDir);
293
- if (state.setupCompletedAt) {
294
- const validation = await validatePersonaFiles(personaDir);
295
- return validation.complete ? 'complete' : 'pending';
296
- }
297
- const bootstrapPath = path.join(personaDir, 'BOOTSTRAP.md');
298
- try {
299
- await fsp.access(bootstrapPath);
300
- return 'pending';
301
- }
302
- catch {
303
- return 'complete';
304
- }
305
- }
306
- export async function markBootstrapComplete(personaDir) {
307
- const validation = await validatePersonaFiles(personaDir);
308
- if (!validation.complete) {
309
- const details = [
310
- validation.missing.length > 0 ? `missing: ${validation.missing.join(', ')}` : '',
311
- validation.templateFiles.length > 0 ? `templates: ${validation.templateFiles.join(', ')}` : '',
312
- ].filter(Boolean).join('; ');
313
- throw new Error(`Persona bootstrap is incomplete${details ? ` (${details})` : ''}.`);
314
- }
315
- const bootstrapPath = path.join(personaDir, 'BOOTSTRAP.md');
316
- try {
317
- await fsp.unlink(bootstrapPath);
318
- logger.info('Deleted BOOTSTRAP.md');
319
- }
320
- catch (err) {
321
- const code = err.code;
322
- if (code !== 'ENOENT')
323
- throw err;
324
- }
325
- const state = await readState(personaDir);
326
- state.setupCompletedAt = new Date().toISOString();
327
- await writeState(personaDir, state);
328
- logger.info('Bootstrap marked as complete');
329
- }
330
- export function markBootstrapCompleteSync(personaDir) {
331
- const validation = validatePersonaFilesSync(personaDir);
332
- if (!validation.complete) {
333
- const details = [
334
- validation.missing.length > 0 ? `missing: ${validation.missing.join(', ')}` : '',
335
- validation.templateFiles.length > 0 ? `templates: ${validation.templateFiles.join(', ')}` : '',
336
- ].filter(Boolean).join('; ');
337
- throw new Error(`Persona bootstrap is incomplete${details ? ` (${details})` : ''}.`);
338
- }
339
- const bootstrapPath = path.join(personaDir, 'BOOTSTRAP.md');
340
- try {
341
- fs.unlinkSync(bootstrapPath);
342
- logger.info('Deleted BOOTSTRAP.md');
343
- }
344
- catch (err) {
345
- const code = err.code;
346
- if (code !== 'ENOENT')
347
- throw err;
348
- }
349
- const state = readStateSync(personaDir);
350
- state.setupCompletedAt = new Date().toISOString();
351
- writeStateSync(personaDir, state);
352
- logger.info('Bootstrap marked as complete');
353
- }
354
- function readStateSync(personaDir) {
355
- const statePath = resolveStatePath(personaDir);
356
- try {
357
- const raw = fs.readFileSync(statePath, 'utf-8');
358
- const parsed = JSON.parse(raw);
359
- if (parsed && typeof parsed === 'object' && parsed.version === PERSONA_STATE_VERSION) {
360
- return parsed;
361
- }
362
- return { version: PERSONA_STATE_VERSION };
363
- }
364
- catch (err) {
365
- const code = err.code;
366
- if (code === 'ENOENT')
367
- return { version: PERSONA_STATE_VERSION };
368
- throw err;
369
- }
370
- }
371
- function writeStateSync(personaDir, state) {
372
- const stateDir = path.join(personaDir, PERSONA_STATE_DIR);
373
- fs.mkdirSync(stateDir, { recursive: true });
374
- fs.writeFileSync(resolveStatePath(personaDir), JSON.stringify(state, null, 2) + '\n', 'utf-8');
375
- }
376
268
  export async function loadPersonaFiles(personaDir, cwdFallback) {
377
269
  const files = [];
378
270
  for (const fileName of PERSONA_FILE_NAMES) {
@@ -406,8 +298,6 @@ export async function loadPersonaFiles(personaDir, cwdFallback) {
406
298
  if (cwdFallback) {
407
299
  for (let i = 0; i < files.length; i++) {
408
300
  const f = files[i];
409
- if (f.name === 'BOOTSTRAP.md')
410
- continue;
411
301
  const template = loadTemplateContent(f.name);
412
302
  if (f.content === template || !f.content) {
413
303
  const cwdPath = path.join(cwdFallback, f.name);
@@ -121,7 +121,7 @@ export class SetupWizard {
121
121
  if (ws.filesCreated.length > 0) {
122
122
  p.log.info(`已在 ${personaDir} 创建 ${ws.filesCreated.length} 个模板文件`);
123
123
  }
124
- if (ws.status === 'pending') {
124
+ if (ws.needsSetup) {
125
125
  p.log.info(pc.cyan('首次启动时将进行个性化引导对话'));
126
126
  }
127
127
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyacinth-ai",
3
- "version": "0.9.14",
3
+ "version": "0.9.16",
4
4
  "description": "Hyacinth 🪻 — multi-provider AI agent with tool system and context management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",