helloagents 3.0.3-beta.1 → 3.0.8-beta.1

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 (75) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/README.md +157 -57
  5. package/README_CN.md +157 -57
  6. package/bootstrap-lite.md +125 -50
  7. package/bootstrap.md +169 -123
  8. package/cli.mjs +80 -427
  9. package/gemini-extension.json +1 -1
  10. package/hooks/hooks-claude.json +10 -0
  11. package/hooks/hooks.json +10 -0
  12. package/package.json +1 -1
  13. package/scripts/advisor-state.mjs +222 -0
  14. package/scripts/capability-registry.mjs +59 -0
  15. package/scripts/cli-codex-backup.mjs +59 -0
  16. package/scripts/cli-codex-config.mjs +94 -0
  17. package/scripts/cli-codex.mjs +90 -222
  18. package/scripts/cli-config.mjs +1 -0
  19. package/scripts/cli-doctor-render.mjs +28 -0
  20. package/scripts/cli-doctor.mjs +370 -0
  21. package/scripts/cli-host-detect.mjs +94 -0
  22. package/scripts/cli-lifecycle-hosts.mjs +123 -0
  23. package/scripts/cli-lifecycle.mjs +213 -0
  24. package/scripts/cli-messages.mjs +76 -52
  25. package/scripts/cli-toml.mjs +30 -0
  26. package/scripts/closeout-state.mjs +213 -0
  27. package/scripts/delivery-gate.mjs +256 -0
  28. package/scripts/guard-rules.mjs +147 -0
  29. package/scripts/guard.mjs +218 -168
  30. package/scripts/notify-context.mjs +78 -23
  31. package/scripts/notify-events.mjs +5 -1
  32. package/scripts/notify-route.mjs +111 -0
  33. package/scripts/notify-shared.mjs +0 -2
  34. package/scripts/notify-source.mjs +113 -0
  35. package/scripts/notify-ui.mjs +40 -6
  36. package/scripts/notify.mjs +137 -65
  37. package/scripts/plan-contract.mjs +210 -0
  38. package/scripts/project-storage.mjs +235 -0
  39. package/scripts/ralph-loop.mjs +9 -58
  40. package/scripts/replay-state.mjs +210 -0
  41. package/scripts/review-state.mjs +220 -0
  42. package/scripts/runtime-context.mjs +74 -0
  43. package/scripts/turn-state.mjs +173 -0
  44. package/scripts/verify-state.mjs +226 -0
  45. package/scripts/visual-state.mjs +244 -0
  46. package/scripts/workflow-core.mjs +165 -0
  47. package/scripts/workflow-plan-files.mjs +249 -0
  48. package/scripts/workflow-recommendation.mjs +335 -0
  49. package/scripts/workflow-state.mjs +113 -0
  50. package/skills/_meta/SKILL.md +1 -1
  51. package/skills/commands/auto/SKILL.md +48 -67
  52. package/skills/commands/build/SKILL.md +67 -0
  53. package/skills/commands/clean/SKILL.md +10 -8
  54. package/skills/commands/commit/SKILL.md +8 -4
  55. package/skills/commands/help/SKILL.md +18 -11
  56. package/skills/commands/idea/SKILL.md +55 -0
  57. package/skills/commands/init/SKILL.md +16 -8
  58. package/skills/commands/loop/SKILL.md +6 -5
  59. package/skills/commands/plan/SKILL.md +118 -0
  60. package/skills/commands/prd/SKILL.md +22 -15
  61. package/skills/commands/verify/SKILL.md +32 -9
  62. package/skills/commands/wiki/SKILL.md +11 -11
  63. package/skills/hello-review/SKILL.md +9 -0
  64. package/skills/hello-subagent/SKILL.md +5 -3
  65. package/skills/hello-ui/SKILL.md +36 -8
  66. package/skills/hello-verify/SKILL.md +12 -3
  67. package/skills/helloagents/SKILL.md +36 -20
  68. package/templates/DESIGN.md +25 -4
  69. package/templates/STATE.md +3 -0
  70. package/templates/plans/contract.json +48 -0
  71. package/templates/plans/plan.md +23 -0
  72. package/templates/plans/tasks.md +3 -3
  73. package/skills/commands/design/SKILL.md +0 -108
  74. package/skills/commands/review/SKILL.md +0 -16
  75. package/templates/plans/design.md +0 -14
@@ -0,0 +1,111 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ export function resolveBootstrapFile(cwd, installMode) {
5
+ const isActivated = existsSync(join(cwd, '.helloagents'))
6
+ return (installMode === 'global' || isActivated) ? 'bootstrap.md' : 'bootstrap-lite.md'
7
+ }
8
+
9
+ function shouldBypassRoute(prompt) {
10
+ return !prompt || /^\[子代理任务\]/.test(prompt)
11
+ }
12
+
13
+ function buildHelpExtraRules(skillName) {
14
+ if (skillName !== 'help') return ''
15
+ return ' 这是 HelloAGENTS 的帮助命令,不是宿主 CLI 的内置帮助。仅显示 HelloAGENTS 的帮助和当前设置;优先使用当前上下文中已注入的“当前用户设置”,只有上下文不存在该信息时才尝试读取 ~/.helloagents/helloagents.json;自动激活技能说明仅在全局模式或已激活项目中生效。不要调用宿主 CLI 的帮助工具(如 cli_help 或 /help),不要使用子代理,不要读取项目文件;若受工作区限制无法读取配置,必须明确说明并按已知默认值或已注入设置展示。'
16
+ }
17
+
18
+ function routeExplicitCommand({
19
+ prompt,
20
+ cwd,
21
+ host,
22
+ pkgRoot,
23
+ settings,
24
+ resolveCanonicalCommandSkill,
25
+ writeRouteContext,
26
+ appendReplayEvent,
27
+ buildRouteInstruction,
28
+ suppress,
29
+ }) {
30
+ const cmdMatch = prompt.match(/^~(\w+)/)
31
+ if (!cmdMatch) return false
32
+
33
+ const skillName = cmdMatch[1]
34
+ const canonicalSkillName = resolveCanonicalCommandSkill(skillName)
35
+ writeRouteContext({
36
+ cwd,
37
+ skillName: canonicalSkillName,
38
+ sourceSkillName: skillName,
39
+ })
40
+ appendReplayEvent(cwd, {
41
+ host,
42
+ event: 'command_route_selected',
43
+ source: 'route',
44
+ skillName: canonicalSkillName,
45
+ sourceSkillName: skillName,
46
+ })
47
+ suppress(buildRouteInstruction({
48
+ skillName,
49
+ extraRules: buildHelpExtraRules(skillName),
50
+ cwd,
51
+ pkgRoot,
52
+ host,
53
+ settings,
54
+ }))
55
+ return true
56
+ }
57
+
58
+ export function handleRouteCommand({
59
+ payload,
60
+ host,
61
+ pkgRoot,
62
+ settings,
63
+ buildRouteInstruction,
64
+ buildSemanticRouteInstruction,
65
+ resolveCanonicalCommandSkill,
66
+ writeRouteContext,
67
+ clearRouteContext,
68
+ appendReplayEvent,
69
+ getWorkflowRecommendation,
70
+ suppress,
71
+ emptySuppress,
72
+ }) {
73
+ const prompt = (payload.prompt || '').trim()
74
+ const cwd = payload.cwd || process.cwd()
75
+ if (shouldBypassRoute(prompt)) {
76
+ clearRouteContext()
77
+ emptySuppress()
78
+ return
79
+ }
80
+
81
+ if (routeExplicitCommand({
82
+ prompt,
83
+ cwd,
84
+ host,
85
+ pkgRoot,
86
+ settings,
87
+ resolveCanonicalCommandSkill,
88
+ writeRouteContext,
89
+ appendReplayEvent,
90
+ buildRouteInstruction,
91
+ suppress,
92
+ })) {
93
+ return
94
+ }
95
+
96
+ const bootstrapFile = resolveBootstrapFile(cwd, settings.install_mode)
97
+ if (bootstrapFile === 'bootstrap.md') {
98
+ clearRouteContext()
99
+ appendReplayEvent(cwd, {
100
+ host,
101
+ event: 'semantic_route_prompted',
102
+ source: 'route',
103
+ recommendation: getWorkflowRecommendation(cwd),
104
+ })
105
+ suppress(buildSemanticRouteInstruction(cwd))
106
+ return
107
+ }
108
+
109
+ clearRouteContext()
110
+ emptySuppress()
111
+ }
@@ -43,5 +43,3 @@ export function suppressedOutput(hookEventName, additionalContext) {
43
43
  export function emptySuppress() {
44
44
  output({ suppressOutput: true });
45
45
  }
46
-
47
- export function versionCheckBackground() {}
@@ -0,0 +1,113 @@
1
+ import { basename, normalize, resolve } from 'node:path'
2
+
3
+ const HOST_LABELS = {
4
+ codex: 'Codex',
5
+ claude: 'Claude Code',
6
+ gemini: 'Gemini',
7
+ }
8
+
9
+ const PAYLOAD_SESSION_KEYS = [
10
+ 'sessionId',
11
+ 'session_id',
12
+ 'session',
13
+ 'conversationId',
14
+ 'conversation_id',
15
+ 'conversation',
16
+ 'threadId',
17
+ 'thread_id',
18
+ 'thread',
19
+ 'windowId',
20
+ 'window_id',
21
+ 'window',
22
+ 'tabId',
23
+ 'tab_id',
24
+ 'tab',
25
+ 'requestId',
26
+ 'request_id',
27
+ ]
28
+
29
+ const ENV_SESSION_KEYS = [
30
+ 'HELLOAGENTS_NOTIFY_SESSION_ID',
31
+ 'WT_SESSION',
32
+ 'TERM_SESSION_ID',
33
+ 'KITTY_WINDOW_ID',
34
+ 'ALACRITTY_WINDOW_ID',
35
+ 'WINDOWID',
36
+ 'WEZTERM_PANE',
37
+ 'TAB_ID',
38
+ ]
39
+
40
+ function normalizePath(filePath = '') {
41
+ if (!filePath) return ''
42
+ try {
43
+ return normalize(resolve(filePath))
44
+ } catch {
45
+ return filePath
46
+ }
47
+ }
48
+
49
+ function readStringCandidate(input, key) {
50
+ if (!input || typeof input !== 'object') return ''
51
+ const value = input[key]
52
+ if (typeof value === 'string') return value.trim()
53
+ if (typeof value === 'number') return String(value)
54
+ return ''
55
+ }
56
+
57
+ function resolveProjectLabel(cwd = '') {
58
+ const normalized = normalizePath(cwd)
59
+ if (!normalized) return ''
60
+ const label = basename(normalized)
61
+ return label || normalized.replace(/\\/g, '/')
62
+ }
63
+
64
+ function sanitizeSessionToken(value = '') {
65
+ const raw = String(value).trim().replace(/^[#:\s]+/, '')
66
+ const segments = raw
67
+ .split(/[^a-zA-Z0-9]+/)
68
+ .filter(Boolean)
69
+ const cleaned = segments.length > 1
70
+ ? segments[segments.length - 1]
71
+ : raw.replace(/[^a-zA-Z0-9_-]/g, '')
72
+
73
+ if (!cleaned) return ''
74
+ if (/^\d+$/.test(cleaned)) return cleaned
75
+ return cleaned.slice(0, 8)
76
+ }
77
+
78
+ function resolveSessionToken(payload, env, ppid) {
79
+ for (const key of PAYLOAD_SESSION_KEYS) {
80
+ const value = sanitizeSessionToken(readStringCandidate(payload, key))
81
+ if (value) return value
82
+ }
83
+
84
+ for (const key of ENV_SESSION_KEYS) {
85
+ const value = sanitizeSessionToken(env?.[key] || '')
86
+ if (value) return value
87
+ }
88
+
89
+ return ppid ? String(ppid) : ''
90
+ }
91
+
92
+ export function resolveNotificationSource({
93
+ host = '',
94
+ cwd = '',
95
+ payload = {},
96
+ env = process.env,
97
+ ppid = process.ppid,
98
+ } = {}) {
99
+ const hostLabel = HOST_LABELS[host] || 'Agent'
100
+ const projectLabel = resolveProjectLabel(readStringCandidate(payload, 'cwd') || cwd)
101
+ const sessionToken = resolveSessionToken(payload, env, ppid)
102
+ const parts = [hostLabel]
103
+
104
+ if (projectLabel) parts.push(projectLabel)
105
+ if (sessionToken) parts.push(`会话 ${sessionToken}`)
106
+
107
+ return {
108
+ hostLabel,
109
+ projectLabel,
110
+ sessionToken,
111
+ sourceLabel: parts.join(' · '),
112
+ }
113
+ }
@@ -19,6 +19,36 @@ const NOTIFY_MESSAGES = {
19
19
 
20
20
  const WIN_APPID = 'HelloAgents.Notification';
21
21
 
22
+ function escapeToastText(value = '') {
23
+ return String(value)
24
+ .replace(/&/g, '&')
25
+ .replace(/</g, '&lt;')
26
+ .replace(/>/g, '&gt;');
27
+ }
28
+
29
+ function escapeAppleScriptText(value = '') {
30
+ return String(value).replace(/"/g, '\\"');
31
+ }
32
+
33
+ export function buildDesktopNotificationContent(event, extra) {
34
+ const options = extra && typeof extra === 'object'
35
+ ? extra
36
+ : { message: extra || '' };
37
+ const message = options.message || NOTIFY_MESSAGES[event] || event;
38
+ const title = options.title || 'HelloAgents 通知';
39
+ const sourceLabel = options.sourceLabel || '';
40
+ const body = sourceLabel ? `${sourceLabel}\n${message}` : message;
41
+ const toastLines = sourceLabel ? [sourceLabel, message] : [message];
42
+
43
+ return {
44
+ title,
45
+ message,
46
+ sourceLabel,
47
+ body,
48
+ toastLines,
49
+ };
50
+ }
51
+
22
52
  function resolveWav(pkgRoot, event) {
23
53
  const p = join(pkgRoot, 'assets', 'sounds', `${event}.wav`);
24
54
  return existsSync(p) ? p : null;
@@ -53,14 +83,15 @@ function ensureWinAppId(pkgRoot) {
53
83
  }
54
84
 
55
85
  export function desktopNotify(pkgRoot, event, extra) {
56
- const msg = extra || NOTIFY_MESSAGES[event] || event;
57
- const title = 'HelloAgents 通知';
86
+ const notification = buildDesktopNotificationContent(event, extra);
58
87
  try {
59
88
  if (PLAT === 'win32') {
60
89
  ensureWinAppId(pkgRoot);
61
- const safeMsg = msg.replace(/'/g, "''");
62
90
  const iconPath = join(pkgRoot, 'assets', 'icons', 'icon.png').replace(/\//g, '\\');
63
91
  const iconXml = existsSync(iconPath) ? `<image placement="appLogoOverride" src="${iconPath}" />` : '';
92
+ const textXml = notification.toastLines
93
+ .map((line) => `<text>${escapeToastText(line)}</text>`)
94
+ .join('\n ');
64
95
  const ps = `
65
96
  [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
66
97
  [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom, ContentType = WindowsRuntime] | Out-Null
@@ -69,7 +100,7 @@ $xml = @"
69
100
  <visual>
70
101
  <binding template="ToastGeneric">
71
102
  ${iconXml}
72
- <text>${safeMsg}</text>
103
+ ${textXml}
73
104
  </binding>
74
105
  </visual>
75
106
  </toast>
@@ -81,11 +112,14 @@ $toast = [Windows.UI.Notifications.ToastNotification]::new($doc)
81
112
  `.trim();
82
113
  spawnSync('powershell', ['-NoProfile', '-c', ps], { stdio: 'ignore', windowsHide: true });
83
114
  } else if (PLAT === 'darwin') {
115
+ const subtitle = notification.sourceLabel
116
+ ? ` subtitle "${escapeAppleScriptText(notification.sourceLabel)}"`
117
+ : '';
84
118
  spawnSync('osascript', ['-e',
85
- `display notification "${msg.replace(/"/g, '\\"')}" with title "${title}"`],
119
+ `display notification "${escapeAppleScriptText(notification.message)}" with title "${escapeAppleScriptText(notification.title)}"${subtitle}`],
86
120
  { stdio: 'ignore' });
87
121
  } else {
88
- const result = spawnSync('notify-send', [title, msg], { stdio: 'ignore' });
122
+ const result = spawnSync('notify-send', [notification.title, notification.body], { stdio: 'ignore' });
89
123
  if (result.status !== 0) process.stderr.write('\x07');
90
124
  }
91
125
  } catch { process.stderr.write('\x07'); }
@@ -8,9 +8,15 @@ import { spawnSync } from 'node:child_process';
8
8
  import { fileURLToPath } from 'node:url';
9
9
  import { homedir } from 'node:os';
10
10
  import { playSound as _playSound, desktopNotify as _desktopNotify } from './notify-ui.mjs';
11
- import { buildCompactionContext, buildInjectContext, buildRouteInstruction, detectNewProjectRoute } from './notify-context.mjs';
12
- import { claimsTaskComplete, shouldIgnoreCodexNotifyClient, shouldIgnoreFormattedSubagent } from './notify-events.mjs';
13
- import { readSettings, readStdinJson, output, suppressedOutput, emptySuppress, versionCheckBackground } from './notify-shared.mjs';
11
+ import { resolveNotificationSource } from './notify-source.mjs';
12
+ import { buildCompactionContext, buildInjectContext, buildRouteInstruction, buildSemanticRouteInstruction, resolveCanonicalCommandSkill } from './notify-context.mjs';
13
+ import { claimsTaskComplete, shouldIgnoreCodexNotifyClient } from './notify-events.mjs';
14
+ import { handleRouteCommand, resolveBootstrapFile } from './notify-route.mjs';
15
+ import { readSettings, readStdinJson, output, suppressedOutput, emptySuppress } from './notify-shared.mjs';
16
+ import { clearRouteContext, writeRouteContext } from './runtime-context.mjs';
17
+ import { appendReplayEvent, startReplaySession } from './replay-state.mjs';
18
+ import { clearTurnState, readTurnState } from './turn-state.mjs';
19
+ import { getWorkflowRecommendation } from './workflow-state.mjs';
14
20
 
15
21
  const __filename = fileURLToPath(import.meta.url);
16
22
  const __dirname = dirname(__filename);
@@ -31,6 +37,18 @@ const EVENT_NAME = {
31
37
  const playSound = (event) => _playSound(PKG_ROOT, event);
32
38
  const desktopNotify = (event, extra) => _desktopNotify(PKG_ROOT, event, extra);
33
39
 
40
+ function buildNotifyExtra(payload = {}, options = {}) {
41
+ const source = resolveNotificationSource({
42
+ host: HOST,
43
+ cwd: payload.cwd || process.cwd(),
44
+ payload,
45
+ });
46
+ return {
47
+ message: options.message || '',
48
+ sourceLabel: source.sourceLabel,
49
+ };
50
+ }
51
+
34
52
  function getSettings() {
35
53
  return readSettings(CONFIG_FILE);
36
54
  }
@@ -41,7 +59,8 @@ function runRalphLoop(payload) {
41
59
  try {
42
60
  const rlPath = join(__dirname, 'ralph-loop.mjs');
43
61
  if (!existsSync(rlPath)) return false;
44
- const result = spawnSync(process.execPath, [rlPath, ...(IS_GEMINI ? ['--gemini'] : [])], {
62
+ const hostFlag = IS_GEMINI ? ['--gemini'] : HOST === 'codex' ? ['--codex'] : [];
63
+ const result = spawnSync(process.execPath, [rlPath, ...hostFlag], {
45
64
  input: JSON.stringify(payload),
46
65
  encoding: 'utf-8',
47
66
  timeout: 120_000,
@@ -49,6 +68,12 @@ function runRalphLoop(payload) {
49
68
  if (result.stdout) {
50
69
  const rlOut = JSON.parse(result.stdout);
51
70
  if (rlOut.decision === 'block') {
71
+ appendReplayEvent(payload.cwd || process.cwd(), {
72
+ host: HOST,
73
+ event: 'verify_gate_blocked',
74
+ source: 'ralph-loop',
75
+ reason: rlOut.reason || '',
76
+ });
52
77
  output(rlOut);
53
78
  return true;
54
79
  }
@@ -57,17 +82,52 @@ function runRalphLoop(payload) {
57
82
  return false;
58
83
  }
59
84
 
60
- function resolveBootstrapFile(cwd, settings) {
61
- const isGlobal = settings.install_mode === 'global';
62
- const isActivated = existsSync(join(cwd, '.helloagents'));
63
- return (isGlobal || isActivated) ? 'bootstrap.md' : 'bootstrap-lite.md';
85
+ function runDeliveryGate(payload) {
86
+ try {
87
+ const gatePath = join(__dirname, 'delivery-gate.mjs');
88
+ if (!existsSync(gatePath)) return false;
89
+ const result = spawnSync(process.execPath, [gatePath], {
90
+ input: JSON.stringify(payload),
91
+ encoding: 'utf-8',
92
+ timeout: 30_000,
93
+ });
94
+ if (result.stdout) {
95
+ const gateOut = JSON.parse(result.stdout);
96
+ if (gateOut.decision === 'block') {
97
+ appendReplayEvent(payload.cwd || process.cwd(), {
98
+ host: HOST,
99
+ event: 'delivery_gate_blocked',
100
+ source: 'delivery-gate',
101
+ reason: gateOut.reason || '',
102
+ });
103
+ output(gateOut);
104
+ return true;
105
+ }
106
+ }
107
+ } catch {}
108
+ return false;
109
+ }
110
+
111
+ function readCompletionText(payload = {}) {
112
+ return payload['last-assistant-message']
113
+ || payload.last_assistant_message
114
+ || payload.lastAssistantMessage
115
+ || '';
116
+ }
117
+
118
+ function shouldRunDeliveryGate(cwd, lastMsg) {
119
+ const turnState = readTurnState(cwd);
120
+ if (turnState?.role === 'main') {
121
+ return turnState.kind === 'complete';
122
+ }
123
+ return claimsTaskComplete(lastMsg);
64
124
  }
65
125
 
66
126
  function cmdPreCompact() {
67
127
  const payload = readStdinJson();
68
128
  const cwd = payload.cwd || process.cwd();
69
129
  const settings = getSettings();
70
- const bootstrapFile = resolveBootstrapFile(cwd, settings);
130
+ const bootstrapFile = resolveBootstrapFile(cwd, settings.install_mode);
71
131
  const context = buildCompactionContext({
72
132
  payload,
73
133
  pkgRoot: PKG_ROOT,
@@ -75,50 +135,36 @@ function cmdPreCompact() {
75
135
  bootstrapFile,
76
136
  host: HOST,
77
137
  });
138
+ appendReplayEvent(cwd, {
139
+ host: HOST,
140
+ event: 'pre_compact_snapshot',
141
+ source: 'pre-compact',
142
+ details: {
143
+ bootstrapFile,
144
+ installMode: settings.install_mode || '',
145
+ },
146
+ });
78
147
  suppressedOutput(EVENT_NAME.PreCompact, context);
79
148
  }
80
149
 
81
150
  function cmdRoute() {
82
151
  const payload = readStdinJson();
83
- const prompt = (payload.prompt || '').trim();
84
- const cwd = payload.cwd || process.cwd();
85
- const settings = getSettings();
86
- if (!prompt) {
87
- emptySuppress();
88
- return;
89
- }
90
- if (/^\[子代理任务\]/.test(prompt)) {
91
- emptySuppress();
92
- return;
93
- }
94
-
95
- const cmdMatch = prompt.match(/^~(\w+)/);
96
- if (cmdMatch) {
97
- const skillName = cmdMatch[1];
98
- const extraRules = skillName === 'help'
99
- ? ' 这是 HelloAGENTS 的帮助命令,不是宿主 CLI 的内置帮助。适用条件:仅显示 HelloAGENTS 的帮助和当前设置;优先使用当前上下文中已注入的“当前用户设置”,只有上下文不存在该信息时才尝试读取 ~/.helloagents/helloagents.json;自动激活技能说明仅在全局模式,或当前项目已存在 .helloagents/(例如执行过 ~wiki 或 ~init)时生效。排除条件:不要调用宿主 CLI 的帮助工具(如 cli_help 或 /help),不要使用子代理,不要读取项目文件;若受工作区限制无法读取配置,必须明确说明并按已知默认值或已注入设置展示;纯标准模式未激活项目不会自动触发。'
100
- : '';
101
- suppressedOutput(EVENT_NAME.UserPromptSubmit, buildRouteInstruction({
102
- skillName,
103
- extraRules,
104
- cwd,
105
- pkgRoot: PKG_ROOT,
106
- host: HOST,
107
- settings,
108
- }));
109
- return;
110
- }
111
-
112
- const bootstrapFile = resolveBootstrapFile(cwd, settings);
113
- if (bootstrapFile === 'bootstrap.md') {
114
- const routeMessage = detectNewProjectRoute(prompt);
115
- if (routeMessage) {
116
- suppressedOutput(EVENT_NAME.UserPromptSubmit, routeMessage);
117
- return;
118
- }
119
- }
120
-
121
- emptySuppress();
152
+ clearTurnState(payload.cwd || process.cwd());
153
+ handleRouteCommand({
154
+ payload,
155
+ host: HOST,
156
+ pkgRoot: PKG_ROOT,
157
+ settings: getSettings(),
158
+ buildRouteInstruction,
159
+ buildSemanticRouteInstruction,
160
+ resolveCanonicalCommandSkill,
161
+ writeRouteContext,
162
+ clearRouteContext,
163
+ appendReplayEvent,
164
+ getWorkflowRecommendation,
165
+ suppress: (context) => suppressedOutput(EVENT_NAME.UserPromptSubmit, context),
166
+ emptySuppress,
167
+ });
122
168
  }
123
169
 
124
170
  function cmdInject() {
@@ -126,13 +172,29 @@ function cmdInject() {
126
172
  const source = payload.source || 'startup';
127
173
  const cwd = payload.cwd || process.cwd();
128
174
  const settings = getSettings();
129
- const bootstrapFile = resolveBootstrapFile(cwd, settings);
175
+ const bootstrapFile = resolveBootstrapFile(cwd, settings.install_mode);
130
176
 
131
177
  let bootstrap = '';
132
178
  try {
133
179
  bootstrap = readFileSync(join(PKG_ROOT, bootstrapFile), 'utf-8');
134
180
  } catch {}
135
181
 
182
+ startReplaySession(cwd, {
183
+ host: HOST,
184
+ source,
185
+ bootstrapFile,
186
+ installMode: settings.install_mode || '',
187
+ });
188
+ appendReplayEvent(cwd, {
189
+ host: HOST,
190
+ event: 'session_injected',
191
+ source,
192
+ details: {
193
+ bootstrapFile,
194
+ installMode: settings.install_mode || '',
195
+ activatedProject: existsSync(join(cwd, '.helloagents')),
196
+ },
197
+ });
136
198
  const context = buildInjectContext({
137
199
  source,
138
200
  bootstrap,
@@ -141,22 +203,31 @@ function cmdInject() {
141
203
  host: HOST,
142
204
  cwd,
143
205
  });
206
+ clearRouteContext();
207
+ clearTurnState(cwd);
144
208
  suppressedOutput(EVENT_NAME.SessionStart, context || undefined);
145
- versionCheckBackground();
146
209
  }
147
210
 
148
211
  function cmdStop() {
149
212
  const payload = readStdinJson();
213
+ const lastMsg = readCompletionText(payload);
214
+ const cwd = payload.cwd || process.cwd();
215
+ clearRouteContext();
150
216
  if (runRalphLoop(payload)) {
151
217
  playSound('warning');
152
- desktopNotify('warning');
218
+ desktopNotify('warning', buildNotifyExtra(payload));
219
+ return;
220
+ }
221
+ if (shouldRunDeliveryGate(cwd, lastMsg) && runDeliveryGate(payload)) {
222
+ playSound('warning');
223
+ desktopNotify('warning', buildNotifyExtra(payload));
153
224
  return;
154
225
  }
155
226
 
156
227
  const settings = getSettings();
157
228
  const level = settings.notify_level ?? 0;
158
229
  if (level === 2 || level === 3) playSound('complete');
159
- if (level === 1 || level === 3) desktopNotify('complete');
230
+ if (level === 1 || level === 3) desktopNotify('complete', buildNotifyExtra(payload));
160
231
  emptySuppress();
161
232
  }
162
233
 
@@ -165,7 +236,7 @@ function cmdSound() {
165
236
  }
166
237
 
167
238
  function cmdDesktop() {
168
- desktopNotify(process.argv[3] || 'complete');
239
+ desktopNotify(process.argv[3] || 'complete', buildNotifyExtra({ cwd: process.cwd() }));
169
240
  }
170
241
 
171
242
  function cmdCodexNotify() {
@@ -178,30 +249,32 @@ function cmdCodexNotify() {
178
249
 
179
250
  if (type === 'approval-requested') {
180
251
  playSound('confirm');
181
- desktopNotify('confirm');
252
+ desktopNotify('confirm', buildNotifyExtra(data));
182
253
  return;
183
254
  }
184
255
  if (type !== 'agent-turn-complete') return;
185
256
 
186
- const lastMsg = data['last-assistant-message'] || '';
187
- const settings = getSettings();
188
- if (shouldIgnoreFormattedSubagent(lastMsg, settings.output_format !== false)) return;
189
-
190
257
  const cwd = data.cwd || process.cwd();
191
- if (claimsTaskComplete(lastMsg) && runRalphLoop({ cwd })) {
258
+ const turnState = readTurnState(cwd);
259
+ if (!turnState || turnState.role !== 'main') return;
260
+
261
+ const settings = getSettings();
262
+ if (turnState.kind === 'complete' && runRalphLoop({ cwd })) {
192
263
  playSound('warning');
193
- desktopNotify('warning');
264
+ desktopNotify('warning', buildNotifyExtra(data));
265
+ return;
266
+ }
267
+ if (turnState.kind === 'complete' && runDeliveryGate({ cwd })) {
268
+ playSound('warning');
269
+ desktopNotify('warning', buildNotifyExtra(data));
194
270
  return;
195
271
  }
196
272
 
197
273
  const level = settings.notify_level ?? 0;
198
274
  if (level === 2 || level === 3) playSound('complete');
199
- if (level === 1 || level === 3) desktopNotify('complete');
200
- versionCheckBackground();
275
+ if (level === 1 || level === 3) desktopNotify('complete', buildNotifyExtra(data));
201
276
  }
202
277
 
203
- function cmdVersionCheck() {}
204
-
205
278
  const cmd = process.argv[2] || '';
206
279
 
207
280
  switch (cmd) {
@@ -212,7 +285,6 @@ switch (cmd) {
212
285
  case 'sound': cmdSound(); break;
213
286
  case 'desktop': cmdDesktop(); break;
214
287
  case 'codex-notify': cmdCodexNotify(); break;
215
- case 'version-check': cmdVersionCheck(); break;
216
288
  default:
217
289
  process.stderr.write(`notify.mjs: unknown command "${cmd}"\n`);
218
290
  process.exit(1);