helloagents 3.0.2-beta.1 → 3.0.7

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 (72) 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 +147 -45
  5. package/README_CN.md +148 -46
  6. package/bootstrap-lite.md +104 -46
  7. package/bootstrap.md +143 -112
  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 +2 -12
  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 +100 -0
  17. package/scripts/cli-codex.mjs +34 -156
  18. package/scripts/cli-config.mjs +1 -0
  19. package/scripts/cli-doctor-render.mjs +28 -0
  20. package/scripts/cli-doctor.mjs +367 -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/closeout-state.mjs +213 -0
  26. package/scripts/delivery-gate.mjs +256 -0
  27. package/scripts/guard-rules.mjs +122 -0
  28. package/scripts/guard.mjs +190 -168
  29. package/scripts/notify-context.mjs +77 -17
  30. package/scripts/notify-events.mjs +5 -1
  31. package/scripts/notify-route.mjs +111 -0
  32. package/scripts/notify-shared.mjs +0 -2
  33. package/scripts/notify-source.mjs +113 -0
  34. package/scripts/notify-ui.mjs +40 -6
  35. package/scripts/notify.mjs +120 -59
  36. package/scripts/plan-contract.mjs +210 -0
  37. package/scripts/project-storage.mjs +235 -0
  38. package/scripts/ralph-loop.mjs +9 -58
  39. package/scripts/replay-state.mjs +210 -0
  40. package/scripts/review-state.mjs +220 -0
  41. package/scripts/runtime-context.mjs +74 -0
  42. package/scripts/verify-state.mjs +226 -0
  43. package/scripts/visual-state.mjs +244 -0
  44. package/scripts/workflow-core.mjs +165 -0
  45. package/scripts/workflow-plan-files.mjs +249 -0
  46. package/scripts/workflow-recommendation.mjs +335 -0
  47. package/scripts/workflow-state.mjs +113 -0
  48. package/skills/commands/auto/SKILL.md +37 -71
  49. package/skills/commands/build/SKILL.md +67 -0
  50. package/skills/commands/clean/SKILL.md +10 -8
  51. package/skills/commands/commit/SKILL.md +8 -4
  52. package/skills/commands/help/SKILL.md +19 -11
  53. package/skills/commands/idea/SKILL.md +55 -0
  54. package/skills/commands/init/SKILL.md +6 -3
  55. package/skills/commands/loop/SKILL.md +6 -5
  56. package/skills/commands/plan/SKILL.md +116 -0
  57. package/skills/commands/prd/SKILL.md +20 -15
  58. package/skills/commands/verify/SKILL.md +32 -9
  59. package/skills/commands/wiki/SKILL.md +59 -0
  60. package/skills/hello-review/SKILL.md +9 -0
  61. package/skills/hello-subagent/SKILL.md +4 -3
  62. package/skills/hello-ui/SKILL.md +36 -8
  63. package/skills/hello-verify/SKILL.md +10 -2
  64. package/skills/helloagents/SKILL.md +24 -13
  65. package/templates/DESIGN.md +25 -4
  66. package/templates/STATE.md +3 -0
  67. package/templates/plans/contract.json +48 -0
  68. package/templates/plans/plan.md +23 -0
  69. package/templates/plans/tasks.md +3 -3
  70. package/skills/commands/design/SKILL.md +0 -108
  71. package/skills/commands/review/SKILL.md +0 -16
  72. 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,14 @@ 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';
11
+ import { resolveNotificationSource } from './notify-source.mjs';
12
+ import { buildCompactionContext, buildInjectContext, buildRouteInstruction, buildSemanticRouteInstruction, resolveCanonicalCommandSkill } from './notify-context.mjs';
12
13
  import { claimsTaskComplete, shouldIgnoreCodexNotifyClient, shouldIgnoreFormattedSubagent } from './notify-events.mjs';
13
- import { readSettings, readStdinJson, output, suppressedOutput, emptySuppress, versionCheckBackground } from './notify-shared.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 { getWorkflowRecommendation } from './workflow-state.mjs';
14
19
 
15
20
  const __filename = fileURLToPath(import.meta.url);
16
21
  const __dirname = dirname(__filename);
@@ -31,6 +36,18 @@ const EVENT_NAME = {
31
36
  const playSound = (event) => _playSound(PKG_ROOT, event);
32
37
  const desktopNotify = (event, extra) => _desktopNotify(PKG_ROOT, event, extra);
33
38
 
39
+ function buildNotifyExtra(payload = {}, options = {}) {
40
+ const source = resolveNotificationSource({
41
+ host: HOST,
42
+ cwd: payload.cwd || process.cwd(),
43
+ payload,
44
+ });
45
+ return {
46
+ message: options.message || '',
47
+ sourceLabel: source.sourceLabel,
48
+ };
49
+ }
50
+
34
51
  function getSettings() {
35
52
  return readSettings(CONFIG_FILE);
36
53
  }
@@ -41,7 +58,8 @@ function runRalphLoop(payload) {
41
58
  try {
42
59
  const rlPath = join(__dirname, 'ralph-loop.mjs');
43
60
  if (!existsSync(rlPath)) return false;
44
- const result = spawnSync(process.execPath, [rlPath, ...(IS_GEMINI ? ['--gemini'] : [])], {
61
+ const hostFlag = IS_GEMINI ? ['--gemini'] : HOST === 'codex' ? ['--codex'] : [];
62
+ const result = spawnSync(process.execPath, [rlPath, ...hostFlag], {
45
63
  input: JSON.stringify(payload),
46
64
  encoding: 'utf-8',
47
65
  timeout: 120_000,
@@ -49,6 +67,12 @@ function runRalphLoop(payload) {
49
67
  if (result.stdout) {
50
68
  const rlOut = JSON.parse(result.stdout);
51
69
  if (rlOut.decision === 'block') {
70
+ appendReplayEvent(payload.cwd || process.cwd(), {
71
+ host: HOST,
72
+ event: 'verify_gate_blocked',
73
+ source: 'ralph-loop',
74
+ reason: rlOut.reason || '',
75
+ });
52
76
  output(rlOut);
53
77
  return true;
54
78
  }
@@ -57,17 +81,44 @@ function runRalphLoop(payload) {
57
81
  return false;
58
82
  }
59
83
 
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';
84
+ function runDeliveryGate(payload) {
85
+ try {
86
+ const gatePath = join(__dirname, 'delivery-gate.mjs');
87
+ if (!existsSync(gatePath)) return false;
88
+ const result = spawnSync(process.execPath, [gatePath], {
89
+ input: JSON.stringify(payload),
90
+ encoding: 'utf-8',
91
+ timeout: 30_000,
92
+ });
93
+ if (result.stdout) {
94
+ const gateOut = JSON.parse(result.stdout);
95
+ if (gateOut.decision === 'block') {
96
+ appendReplayEvent(payload.cwd || process.cwd(), {
97
+ host: HOST,
98
+ event: 'delivery_gate_blocked',
99
+ source: 'delivery-gate',
100
+ reason: gateOut.reason || '',
101
+ });
102
+ output(gateOut);
103
+ return true;
104
+ }
105
+ }
106
+ } catch {}
107
+ return false;
108
+ }
109
+
110
+ function readCompletionText(payload = {}) {
111
+ return payload['last-assistant-message']
112
+ || payload.last_assistant_message
113
+ || payload.lastAssistantMessage
114
+ || '';
64
115
  }
65
116
 
66
117
  function cmdPreCompact() {
67
118
  const payload = readStdinJson();
68
119
  const cwd = payload.cwd || process.cwd();
69
120
  const settings = getSettings();
70
- const bootstrapFile = resolveBootstrapFile(cwd, settings);
121
+ const bootstrapFile = resolveBootstrapFile(cwd, settings.install_mode);
71
122
  const context = buildCompactionContext({
72
123
  payload,
73
124
  pkgRoot: PKG_ROOT,
@@ -75,50 +126,35 @@ function cmdPreCompact() {
75
126
  bootstrapFile,
76
127
  host: HOST,
77
128
  });
129
+ appendReplayEvent(cwd, {
130
+ host: HOST,
131
+ event: 'pre_compact_snapshot',
132
+ source: 'pre-compact',
133
+ details: {
134
+ bootstrapFile,
135
+ installMode: settings.install_mode || '',
136
+ },
137
+ });
78
138
  suppressedOutput(EVENT_NAME.PreCompact, context);
79
139
  }
80
140
 
81
141
  function cmdRoute() {
82
142
  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;自动激活技能说明仅在全局模式或当前项目已通过 ~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();
143
+ handleRouteCommand({
144
+ payload,
145
+ host: HOST,
146
+ pkgRoot: PKG_ROOT,
147
+ settings: getSettings(),
148
+ buildRouteInstruction,
149
+ buildSemanticRouteInstruction,
150
+ resolveCanonicalCommandSkill,
151
+ writeRouteContext,
152
+ clearRouteContext,
153
+ appendReplayEvent,
154
+ getWorkflowRecommendation,
155
+ suppress: (context) => suppressedOutput(EVENT_NAME.UserPromptSubmit, context),
156
+ emptySuppress,
157
+ });
122
158
  }
123
159
 
124
160
  function cmdInject() {
@@ -126,13 +162,29 @@ function cmdInject() {
126
162
  const source = payload.source || 'startup';
127
163
  const cwd = payload.cwd || process.cwd();
128
164
  const settings = getSettings();
129
- const bootstrapFile = resolveBootstrapFile(cwd, settings);
165
+ const bootstrapFile = resolveBootstrapFile(cwd, settings.install_mode);
130
166
 
131
167
  let bootstrap = '';
132
168
  try {
133
169
  bootstrap = readFileSync(join(PKG_ROOT, bootstrapFile), 'utf-8');
134
170
  } catch {}
135
171
 
172
+ startReplaySession(cwd, {
173
+ host: HOST,
174
+ source,
175
+ bootstrapFile,
176
+ installMode: settings.install_mode || '',
177
+ });
178
+ appendReplayEvent(cwd, {
179
+ host: HOST,
180
+ event: 'session_injected',
181
+ source,
182
+ details: {
183
+ bootstrapFile,
184
+ installMode: settings.install_mode || '',
185
+ activatedProject: existsSync(join(cwd, '.helloagents')),
186
+ },
187
+ });
136
188
  const context = buildInjectContext({
137
189
  source,
138
190
  bootstrap,
@@ -141,22 +193,30 @@ function cmdInject() {
141
193
  host: HOST,
142
194
  cwd,
143
195
  });
196
+ clearRouteContext();
144
197
  suppressedOutput(EVENT_NAME.SessionStart, context || undefined);
145
- versionCheckBackground();
146
198
  }
147
199
 
148
200
  function cmdStop() {
149
201
  const payload = readStdinJson();
202
+ const lastMsg = readCompletionText(payload);
203
+ const cwd = payload.cwd || process.cwd();
204
+ clearRouteContext();
150
205
  if (runRalphLoop(payload)) {
151
206
  playSound('warning');
152
- desktopNotify('warning');
207
+ desktopNotify('warning', buildNotifyExtra(payload));
208
+ return;
209
+ }
210
+ if (claimsTaskComplete(lastMsg) && runDeliveryGate(payload)) {
211
+ playSound('warning');
212
+ desktopNotify('warning', buildNotifyExtra(payload));
153
213
  return;
154
214
  }
155
215
 
156
216
  const settings = getSettings();
157
217
  const level = settings.notify_level ?? 0;
158
218
  if (level === 2 || level === 3) playSound('complete');
159
- if (level === 1 || level === 3) desktopNotify('complete');
219
+ if (level === 1 || level === 3) desktopNotify('complete', buildNotifyExtra(payload));
160
220
  emptySuppress();
161
221
  }
162
222
 
@@ -165,7 +225,7 @@ function cmdSound() {
165
225
  }
166
226
 
167
227
  function cmdDesktop() {
168
- desktopNotify(process.argv[3] || 'complete');
228
+ desktopNotify(process.argv[3] || 'complete', buildNotifyExtra({ cwd: process.cwd() }));
169
229
  }
170
230
 
171
231
  function cmdCodexNotify() {
@@ -178,7 +238,7 @@ function cmdCodexNotify() {
178
238
 
179
239
  if (type === 'approval-requested') {
180
240
  playSound('confirm');
181
- desktopNotify('confirm');
241
+ desktopNotify('confirm', buildNotifyExtra(data));
182
242
  return;
183
243
  }
184
244
  if (type !== 'agent-turn-complete') return;
@@ -190,18 +250,20 @@ function cmdCodexNotify() {
190
250
  const cwd = data.cwd || process.cwd();
191
251
  if (claimsTaskComplete(lastMsg) && runRalphLoop({ cwd })) {
192
252
  playSound('warning');
193
- desktopNotify('warning');
253
+ desktopNotify('warning', buildNotifyExtra(data));
254
+ return;
255
+ }
256
+ if (claimsTaskComplete(lastMsg) && runDeliveryGate({ cwd })) {
257
+ playSound('warning');
258
+ desktopNotify('warning', buildNotifyExtra(data));
194
259
  return;
195
260
  }
196
261
 
197
262
  const level = settings.notify_level ?? 0;
198
263
  if (level === 2 || level === 3) playSound('complete');
199
- if (level === 1 || level === 3) desktopNotify('complete');
200
- versionCheckBackground();
264
+ if (level === 1 || level === 3) desktopNotify('complete', buildNotifyExtra(data));
201
265
  }
202
266
 
203
- function cmdVersionCheck() {}
204
-
205
267
  const cmd = process.argv[2] || '';
206
268
 
207
269
  switch (cmd) {
@@ -212,7 +274,6 @@ switch (cmd) {
212
274
  case 'sound': cmdSound(); break;
213
275
  case 'desktop': cmdDesktop(); break;
214
276
  case 'codex-notify': cmdCodexNotify(); break;
215
- case 'version-check': cmdVersionCheck(); break;
216
277
  default:
217
278
  process.stderr.write(`notify.mjs: unknown command "${cmd}"\n`);
218
279
  process.exit(1);