helloagents 3.0.8-beta.1 → 3.0.10-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 (49) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +6 -6
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/README.md +18 -9
  5. package/README_CN.md +42 -33
  6. package/bootstrap-lite.md +18 -17
  7. package/bootstrap.md +22 -21
  8. package/gemini-extension.json +1 -1
  9. package/package.json +1 -1
  10. package/scripts/capability-registry.mjs +5 -5
  11. package/scripts/cli-codex-config.mjs +8 -2
  12. package/scripts/cli-codex.mjs +7 -3
  13. package/scripts/cli-doctor.mjs +6 -1
  14. package/scripts/cli-host-detect.mjs +18 -2
  15. package/scripts/cli-lifecycle.mjs +11 -0
  16. package/scripts/cli-messages.mjs +3 -3
  17. package/scripts/cli-toml-values.mjs +25 -0
  18. package/scripts/cli-toml.mjs +15 -0
  19. package/scripts/delivery-gate.mjs +5 -4
  20. package/scripts/guard.mjs +7 -6
  21. package/scripts/notify-context.mjs +32 -30
  22. package/scripts/notify-events.mjs +0 -8
  23. package/scripts/notify-gates.mjs +128 -0
  24. package/scripts/notify-route.mjs +5 -2
  25. package/scripts/notify-source.mjs +3 -60
  26. package/scripts/notify-ui.mjs +3 -0
  27. package/scripts/notify.mjs +72 -83
  28. package/scripts/project-storage.mjs +107 -15
  29. package/scripts/session-token.mjs +73 -0
  30. package/scripts/workflow-core.mjs +16 -8
  31. package/scripts/workflow-plan-files.mjs +17 -6
  32. package/scripts/workflow-recommendation.mjs +4 -4
  33. package/scripts/workflow-state.mjs +13 -13
  34. package/skills/commands/auto/SKILL.md +13 -13
  35. package/skills/commands/build/SKILL.md +5 -5
  36. package/skills/commands/clean/SKILL.md +6 -6
  37. package/skills/commands/commit/SKILL.md +2 -2
  38. package/skills/commands/help/SKILL.md +2 -2
  39. package/skills/commands/idea/SKILL.md +5 -5
  40. package/skills/commands/init/SKILL.md +4 -4
  41. package/skills/commands/loop/SKILL.md +4 -4
  42. package/skills/commands/plan/SKILL.md +13 -13
  43. package/skills/commands/prd/SKILL.md +13 -13
  44. package/skills/commands/verify/SKILL.md +3 -3
  45. package/skills/commands/wiki/SKILL.md +5 -5
  46. package/skills/hello-subagent/SKILL.md +1 -1
  47. package/skills/hello-ui/SKILL.md +3 -3
  48. package/skills/helloagents/SKILL.md +3 -2
  49. package/templates/plans/contract.json +2 -2
@@ -27,7 +27,13 @@ export function isManagedCodexModelInstruction(line = '') {
27
27
  }
28
28
 
29
29
  export function isManagedCodexNotify(line = '') {
30
- return line.includes('codex-notify')
30
+ const value = String(line || '').replace(/\\/g, '/')
31
+ return value.includes(CODEX_MANAGED_TOML_COMMENT)
32
+ || (
33
+ value.includes('codex-notify')
34
+ && value.includes('/scripts/notify.mjs')
35
+ && /(^|[/\\])helloagents([/\\]|-|$)|[/\\]plugins[/\\]helloagents[/\\]/i.test(value)
36
+ )
31
37
  }
32
38
 
33
39
  export function isManagedCodexBackupInstruction(line = '') {
@@ -51,7 +57,7 @@ function formatManagedCodexNotifyValue(notifyScriptPath) {
51
57
  }
52
58
 
53
59
  function formatManagedCodexNotifyLine(notifyScriptPath) {
54
- return `notify = ${formatManagedCodexNotifyValue(notifyScriptPath)}`
60
+ return `notify = ${formatManagedCodexNotifyValue(notifyScriptPath)} ${CODEX_MANAGED_TOML_COMMENT}`
55
61
  }
56
62
 
57
63
  function removeTopLevelLinesBeingReplaced(toml, lines) {
@@ -20,6 +20,7 @@ import {
20
20
  } from './cli-codex-config.mjs';
21
21
  import {
22
22
  readTopLevelTomlLine,
23
+ readTopLevelTomlBlock,
23
24
  readTomlKeyInSection,
24
25
  removeTomlKeyInSection,
25
26
  ensureTomlKeyInSection,
@@ -98,7 +99,8 @@ function removeCodexMarketplaceEntry(marketplaceFile) {
98
99
  }
99
100
  if (!removedHelloagents) return false;
100
101
  if (!nextPlugins.length) {
101
- removeIfExists(marketplaceFile);
102
+ marketplace.plugins = [];
103
+ safeWrite(marketplaceFile, JSON.stringify(marketplace, null, 2) + '\n');
102
104
  return true;
103
105
  }
104
106
  marketplace.plugins = nextPlugins;
@@ -130,7 +132,7 @@ function cleanupCodexManagedConfig(configPath, { removePluginConfig = false } =
130
132
  let toml = safeRead(configPath) || '';
131
133
 
132
134
  const currentModelInstructions = readTopLevelTomlLine(toml, 'model_instructions_file');
133
- const currentNotify = readTopLevelTomlLine(toml, 'notify');
135
+ const currentNotify = readTopLevelTomlBlock(toml, 'notify');
134
136
  const currentCodexHooks = readTomlKeyInSection(toml, '[features]', 'codex_hooks');
135
137
 
136
138
  const shouldRestoreModelInstructions = isManagedCodexModelInstruction(currentModelInstructions);
@@ -153,7 +155,7 @@ function cleanupCodexManagedConfig(configPath, { removePluginConfig = false } =
153
155
  }
154
156
 
155
157
  const backupModelInstructions = readTopLevelTomlLine(backupToml, 'model_instructions_file');
156
- const backupNotify = readTopLevelTomlLine(backupToml, 'notify');
158
+ const backupNotify = readTopLevelTomlBlock(backupToml, 'notify');
157
159
  const backupCodexHooks = readTomlKeyInSection(backupToml, '[features]', 'codex_hooks');
158
160
 
159
161
  toml = restoreCodexTopLevelConfig(toml, {
@@ -248,6 +250,7 @@ export function installCodexGlobal(home, pkgRoot) {
248
250
 
249
251
  copyEntries(pkgRoot, pluginRoot, CODEX_RUNTIME_ENTRIES);
250
252
  copyEntries(pkgRoot, installedPluginRoot, CODEX_RUNTIME_ENTRIES);
253
+ createLink(pluginRoot, join(codexDir, 'helloagents'));
251
254
  writeCodexRuntimeCarrier(
252
255
  join(pluginRoot, CODEX_RUNTIME_CARRIER),
253
256
  join(pluginRoot, 'bootstrap.md'),
@@ -286,6 +289,7 @@ export function uninstallCodexGlobal(home) {
286
289
  removeIfExists(pluginCacheRoot);
287
290
  removeCodexMarketplaceEntry(marketplaceFile);
288
291
  removeMarkedContent(join(codexDir, 'AGENTS.md'));
292
+ removeLink(join(codexDir, 'helloagents'));
289
293
 
290
294
  const toml = cleanupCodexManagedConfig(configPath, { removePluginConfig: true });
291
295
  if (toml.trim()) safeWrite(configPath, toml);
@@ -230,6 +230,7 @@ function appendCodexStandbyIssues(issues, checks) {
230
230
  function appendCodexGlobalIssues(issues, checks, pluginVersion, cacheVersion) {
231
231
  if (!checks.carrierMarker) issues.push(buildDoctorIssue('global-home-carrier-missing', 'global `~/.codex/AGENTS.md` 缺少 HelloAGENTS 规则内容', 'Global `~/.codex/AGENTS.md` is missing the HelloAGENTS carrier'))
232
232
  if (checks.carrierMarker && !checks.carrierContentMatch) issues.push(buildDoctorIssue('global-home-carrier-drift', 'global `~/.codex/AGENTS.md` 与当前 bootstrap.md 不一致', 'Global `~/.codex/AGENTS.md` differs from the current bootstrap.md'))
233
+ if (!checks.globalHomeLink) issues.push(buildDoctorIssue('global-read-root-link-missing', 'global `~/.codex/helloagents` 链接缺失或未指向当前插件根目录', 'Global `~/.codex/helloagents` link is missing or does not point to the current plugin root'))
233
234
  if (!checks.pluginRoot) issues.push(buildDoctorIssue('global-plugin-root-missing', 'global 插件根目录缺失', 'Global plugin root is missing'))
234
235
  if (!checks.pluginCache) issues.push(buildDoctorIssue('global-plugin-cache-missing', 'global 插件缓存目录缺失', 'Global plugin cache directory is missing'))
235
236
  if (checks.pluginRoot && !checks.pluginCarrierMatch) issues.push(buildDoctorIssue('global-plugin-carrier-drift', 'global 插件根目录中的 AGENTS.md 与当前 bootstrap.md 不一致', 'Global plugin AGENTS.md differs from the current bootstrap.md'))
@@ -258,6 +259,9 @@ function inspectCodexDoctor(settings) {
258
259
  const marketplace = safeJson(join(runtime.home, '.agents', 'plugins', 'marketplace.json')) || {}
259
260
  const pluginVersion = safeJson(join(pluginRoot, 'package.json'))?.version || ''
260
261
  const cacheVersion = safeJson(join(pluginCacheRoot, 'package.json'))?.version || ''
262
+ const homeLinkTarget = safeRealTarget(join(codexDir, 'helloagents'))
263
+ const pkgRootTarget = safeRealTarget(runtime.pkgRoot) || normalizePath(runtime.pkgRoot)
264
+ const pluginRootTarget = safeRealTarget(pluginRoot) || normalizePath(pluginRoot)
261
265
  const standbyNotifyPath = normalizePath(join(runtime.pkgRoot, 'scripts', 'notify.mjs'))
262
266
  const globalNotifyPath = normalizePath(join(pluginRoot, 'scripts', 'notify.mjs'))
263
267
  const managedHomeCarrierPath = normalizePath(join(codexDir, 'AGENTS.md'))
@@ -268,7 +272,8 @@ function inspectCodexDoctor(settings) {
268
272
  const checks = {
269
273
  carrierMarker: (safeRead(join(codexDir, 'AGENTS.md')) || '').includes('HELLOAGENTS_START'),
270
274
  carrierContentMatch: extractManagedCarrierContent(join(codexDir, 'AGENTS.md')) === readBootstrapContent(expectedHomeCarrier),
271
- homeLink: safeRealTarget(join(codexDir, 'helloagents')) === runtime.pkgRoot,
275
+ homeLink: homeLinkTarget === pkgRootTarget,
276
+ globalHomeLink: homeLinkTarget === pluginRootTarget,
272
277
  modelInstructionsFile: !!modelInstructionsLine,
273
278
  modelInstructionsPathMatch: !!modelInstructionsLine
274
279
  && normalizePath(modelInstructionsLine).includes(`"${managedHomeCarrierPath}"`),
@@ -1,4 +1,4 @@
1
- import { existsSync } from 'node:fs'
1
+ import { existsSync, realpathSync } from 'node:fs'
2
2
  import { join } from 'node:path'
3
3
 
4
4
  import {
@@ -27,6 +27,18 @@ function hasHelloagentsSettings(filePath) {
27
27
  return JSON.stringify(safeJson(filePath) || {}).includes('helloagents')
28
28
  }
29
29
 
30
+ function normalizePath(value = '') {
31
+ return String(value || '').replace(/\\/g, '/').toLowerCase()
32
+ }
33
+
34
+ function safeRealTarget(linkPath) {
35
+ try {
36
+ return normalizePath(realpathSync(linkPath))
37
+ } catch {
38
+ return ''
39
+ }
40
+ }
41
+
30
42
  function detectClaudeMode(home) {
31
43
  const claudeDir = join(home, '.claude')
32
44
  if (
@@ -53,19 +65,23 @@ function detectGeminiMode(home) {
53
65
 
54
66
  function detectCodexMode(home) {
55
67
  const codexDir = join(home, '.codex')
68
+ const codexHomeLink = join(codexDir, 'helloagents')
56
69
  const codexConfig = safeRead(join(codexDir, 'config.toml')) || ''
57
70
  const marketplace = safeRead(join(home, '.agents', 'plugins', 'marketplace.json')) || ''
71
+ const globalPluginRoot = normalizePath(join(home, 'plugins', CODEX_PLUGIN_NAME))
72
+ const codexHomeLinkTarget = safeRealTarget(codexHomeLink)
58
73
  if (
59
74
  existsSync(join(home, 'plugins', CODEX_PLUGIN_NAME))
60
75
  || existsSync(join(codexDir, 'plugins', 'cache', CODEX_MARKETPLACE_NAME, CODEX_PLUGIN_NAME))
61
76
  || marketplace.includes(`"name": "${CODEX_PLUGIN_NAME}"`)
62
77
  || codexConfig.includes(CODEX_PLUGIN_KEY)
63
78
  || codexConfig.includes(`/plugins/${CODEX_PLUGIN_NAME}/scripts/notify.mjs`)
79
+ || codexHomeLinkTarget === globalPluginRoot
64
80
  ) {
65
81
  return 'global'
66
82
  }
67
83
  if (
68
- existsSync(join(codexDir, 'helloagents'))
84
+ (existsSync(codexHomeLink) && codexHomeLinkTarget !== globalPluginRoot)
69
85
  || hasHelloagentsMarker(join(codexDir, 'AGENTS.md'))
70
86
  || codexConfig.includes('codex-notify')
71
87
  || codexConfig.includes('HelloAGENTS')
@@ -181,6 +181,17 @@ function runAllHostsLifecycle(action, explicitMode) {
181
181
  }
182
182
 
183
183
  const settings = readSettings(true)
184
+ if (action === 'update' && !explicitMode) {
185
+ for (const host of HOSTS) {
186
+ const mode = resolveHostMode(host, '', settings)
187
+ const result = runHostLifecycle(runtime, action, host, mode)
188
+ if (!result.skipped) setTrackedHostMode(settings, host, mode)
189
+ }
190
+ writeSettings(settings)
191
+ runtime.printInstallMsg(settings.install_mode || DEFAULTS.install_mode, 'refresh')
192
+ return
193
+ }
194
+
184
195
  const mode = resolveInstallMode(explicitMode, settings)
185
196
  if (explicitMode) settings.install_mode = explicitMode
186
197
  installAllHosts(runtime, mode)
@@ -44,8 +44,8 @@ function renderInstallMessage(context, mode, state) {
44
44
  }
45
45
  return msg(
46
46
  refresh
47
- ? ' global 模式已刷新。\n Claude Code / Gemini 请保持插件已安装;Codex 原生本地插件链路已重装并同步最新文件。'
48
- : ' 所有项目将自动启用完整 HelloAGENTS 规则。\n Claude Code / Gemini 请手动安装插件;Codex 已自动走原生本地插件链路。',
47
+ ? ' global 模式已刷新。\n Claude Code / Gemini 请保持插件已安装;Codex 原生本地插件已重装并同步最新文件。'
48
+ : ' 所有项目将自动启用完整 HelloAGENTS 规则。\n Claude Code / Gemini 请手动安装插件;Codex 已自动安装原生本地插件。',
49
49
  refresh
50
50
  ? ' Global mode refreshed.\n Keep Claude Code / Gemini plugins installed; Codex native local-plugin files were reinstalled and synced.'
51
51
  : ' All projects will use full HelloAGENTS rules.\n Install Claude Code / Gemini plugins manually; Codex now uses the native local-plugin path automatically.',
@@ -92,7 +92,7 @@ ${msg('单 CLI 管理', 'Scoped CLI management')}:
92
92
  ${msg('诊断', 'Diagnostics')}:
93
93
  helloagents doctor
94
94
  helloagents doctor codex --json
95
- ${msg('检查 carrier、链接、hooks、配置注入、Codex 插件链路、受管 model_instructions_file 指向与版本漂移', 'Checks carriers, links, hooks, config injections, the Codex plugin chain, managed model_instructions_file targeting, and version drift')}
95
+ ${msg('检查 carrier、链接、hooks、配置注入、Codex 插件安装、受管 model_instructions_file 指向与版本漂移', 'Checks carriers, links, hooks, config injections, Codex plugin installation, managed model_instructions_file targeting, and version drift')}
96
96
 
97
97
  ${msg('卸载', 'Uninstall')}:
98
98
  helloagents cleanup ${msg('(推荐先执行,显式清理所有 CLI 注入/链接)', '(recommended first, explicitly cleans CLI injections/links)')}
@@ -0,0 +1,25 @@
1
+ export function getTomlArrayDepthDelta(text) {
2
+ let depth = 0
3
+ let quoted = false
4
+ let escaped = false
5
+
6
+ for (const char of String(text || '')) {
7
+ if (escaped) {
8
+ escaped = false
9
+ continue
10
+ }
11
+ if (char === '\\' && quoted) {
12
+ escaped = true
13
+ continue
14
+ }
15
+ if (char === '"') {
16
+ quoted = !quoted
17
+ continue
18
+ }
19
+ if (quoted) continue
20
+ if (char === '[') depth += 1
21
+ if (char === ']') depth -= 1
22
+ }
23
+
24
+ return depth
25
+ }
@@ -3,6 +3,8 @@
3
3
  * Targets the small subset of TOML structures used by Codex CLI config.
4
4
  */
5
5
 
6
+ import { getTomlArrayDepthDelta } from './cli-toml-values.mjs'
7
+
6
8
  export function isTomlTableHeader(line) {
7
9
  const trimmed = String(line || '').trim();
8
10
  return trimmed.startsWith('[') && trimmed.endsWith(']');
@@ -62,6 +64,19 @@ function findTopLevelTomlBlock(text, key) {
62
64
  const closeIndex = normalized.indexOf('"""', openIndex + 3);
63
65
  end = closeIndex >= 0 ? closeIndex + 3 : normalized.length;
64
66
  }
67
+ if (value.startsWith('[')) {
68
+ let depth = getTomlArrayDepthDelta(firstLine.slice(firstLine.indexOf('=') + 1));
69
+ let lineStart = firstLineEnd + (normalized[firstLineEnd] === '\n' ? 1 : 0);
70
+
71
+ while (depth > 0 && lineStart < normalized.length) {
72
+ const lineEndIndex = normalized.indexOf('\n', lineStart);
73
+ const nextLineEnd = lineEndIndex >= 0 ? lineEndIndex : normalized.length;
74
+ const nextLine = normalized.slice(lineStart, nextLineEnd);
75
+ depth += getTomlArrayDepthDelta(nextLine);
76
+ end = nextLineEnd;
77
+ lineStart = nextLineEnd + 1;
78
+ }
79
+ }
65
80
 
66
81
  while (end < normalized.length && normalized[end] === '\n') {
67
82
  end += 1;
@@ -205,10 +205,11 @@ function main() {
205
205
  data = JSON.parse(readFileSync(0, 'utf-8'))
206
206
  } catch {}
207
207
  const cwd = data.cwd || process.cwd()
208
- const snapshot = getWorkflowSnapshot(cwd)
209
- const recommendation = getWorkflowRecommendation(cwd)
208
+ const workflowOptions = { payload: data }
209
+ const snapshot = getWorkflowSnapshot(cwd, workflowOptions)
210
+ const recommendation = getWorkflowRecommendation(cwd, workflowOptions)
210
211
  const verificationStatus = getVerifyEvidenceStatus(cwd)
211
- const deliveryAction = getDeliveryAction(cwd)
212
+ const deliveryAction = getDeliveryAction(cwd, workflowOptions)
212
213
  const gatePlans = selectGatePlans(snapshot)
213
214
  const reviewStatus = getReviewEvidenceStatus(cwd, {
214
215
  required: deliveryAction?.phase === 'verify' && deliveryAction?.mode === 'review-first',
@@ -248,7 +249,7 @@ function main() {
248
249
 
249
250
  process.stdout.write(JSON.stringify({
250
251
  decision: 'block',
251
- reason: buildBlockReason(issues, recommendation, buildDeliveryGateHint(cwd)),
252
+ reason: buildBlockReason(issues, recommendation, buildDeliveryGateHint(cwd, workflowOptions)),
252
253
  suppressOutput: true,
253
254
  }))
254
255
  }
package/scripts/guard.mjs CHANGED
@@ -62,15 +62,16 @@ function emitGuardEvent(cwd, event, source, reason, details = {}) {
62
62
  })
63
63
  }
64
64
 
65
- function buildHighRiskGate(matches, cwd) {
66
- const stateSyncHint = buildStateSyncHint(cwd)
65
+ function buildHighRiskGate(matches, cwd, payload = {}) {
66
+ const workflowOptions = { payload }
67
+ const stateSyncHint = buildStateSyncHint(cwd, workflowOptions)
67
68
  if (stateSyncHint) {
68
69
  return {
69
70
  reason: `[HelloAGENTS Guard] Blocked T3 command until project recovery state is synced.\n${stateSyncHint}`,
70
71
  }
71
72
  }
72
73
 
73
- const recommendation = getWorkflowRecommendation(cwd)
74
+ const recommendation = getWorkflowRecommendation(cwd, workflowOptions)
74
75
  if (!recommendation) return null
75
76
  if (matches.some((match) => match.gate === 'post-verify')) {
76
77
  return {
@@ -123,7 +124,7 @@ function buildPostWriteWarnings(data) {
123
124
  const filePath = data.tool_input?.file_path || ''
124
125
  return [
125
126
  ...(detectIdeaBoundaryContext(data)?.zeroSideEffect
126
- ? ['~idea 本轮要求只读探索;检测到写入工具落地,请回退到探索输出或升级到 ~plan / ~build / ~prd / ~auto 后再修改文件']
127
+ ? ['~idea 本轮要求只读探索;检测到写入文件的工具调用,请回到探索输出,或升级到 ~plan / ~build / ~prd / ~auto 后再修改文件']
127
128
  : []),
128
129
  ...scanUnrequestedFiles(filePath, data.tool_name),
129
130
  ...(content ? [...scanForSecrets(content), ...scanDangerousPackages(content, filePath)] : []),
@@ -172,7 +173,7 @@ function handleHighRiskCommand(data, command) {
172
173
  if (warnings.length === 0) return []
173
174
 
174
175
  const cwd = data.cwd || process.cwd()
175
- const gate = buildHighRiskGate(warnings, cwd)
176
+ const gate = buildHighRiskGate(warnings, cwd, data)
176
177
  if (gate) {
177
178
  emitHookPayload({
178
179
  hookSpecificOutput: {
@@ -194,7 +195,7 @@ function handleHighRiskCommand(data, command) {
194
195
  function emitShellWarnings(data, command, highRiskWarnings, shellSafetyWarnings) {
195
196
  const sections = []
196
197
  if (highRiskWarnings.length > 0) {
197
- sections.push(`⚠️ [HelloAGENTS 高风险链路提醒] 检测到高风险命令:\n${highRiskWarnings.map((warning) => ` - ${warning}`).join('\n')}\n请确认已完成相应规划/审查并获得必要授权。`)
198
+ sections.push(`⚠️ [HelloAGENTS 高风险操作提醒] 检测到高风险命令:\n${highRiskWarnings.map((warning) => ` - ${warning}`).join('\n')}\n请确认已完成相应规划/审查并获得必要授权。`)
198
199
  }
199
200
  if (shellSafetyWarnings.length > 0) {
200
201
  sections.push(`⚠️ [HelloAGENTS Shell 安全提醒] 检测到建议调整的命令写法:\n${shellSafetyWarnings.map((warning) => ` - ${warning}`).join('\n')}\n当前仅提示,不中断执行。`)
@@ -1,7 +1,7 @@
1
1
  import { join } from 'node:path';
2
2
  import { existsSync, readFileSync } from 'node:fs';
3
3
  import { homedir } from 'node:os';
4
- import { buildCommandRouteHint, buildStateSyncHint, buildWorkflowRouteHint } from './workflow-state.mjs';
4
+ import { buildCommandRouteHint, buildStateSyncHint, buildWorkflowRouteHint, readStateSnapshot } from './workflow-state.mjs';
5
5
  import { buildCapabilityHint } from './capability-registry.mjs';
6
6
  import {
7
7
  buildProjectStorageBlock,
@@ -69,16 +69,14 @@ export function buildCompactionContext({ payload, pkgRoot, settings, bootstrapFi
69
69
  summaryParts.push('以下信息在上下文压缩前保存,确保压缩后不丢失关键状态。');
70
70
 
71
71
  const cwd = payload.cwd || process.cwd();
72
- const statePath = join(cwd, '.helloagents', 'STATE.md');
73
- const stateSyncHint = buildStateSyncHint(cwd);
74
- if (existsSync(statePath)) {
75
- try {
76
- const stateContent = readFileSync(statePath, 'utf-8');
77
- summaryParts.push('');
78
- summaryParts.push('## 恢复快照(从 STATE.md 读取,只用于找回上次停在哪)');
79
- summaryParts.push('恢复时先看当前用户消息,确认仍是同一任务再按 STATE.md 接续。');
80
- summaryParts.push(stateContent);
81
- } catch {}
72
+ const workflowOptions = { payload };
73
+ const stateSnapshot = readStateSnapshot(cwd, workflowOptions);
74
+ const stateSyncHint = buildStateSyncHint(cwd, workflowOptions);
75
+ if (stateSnapshot.exists && stateSnapshot.content) {
76
+ summaryParts.push('');
77
+ summaryParts.push(`## 状态文件(从 ${stateSnapshot.statePath.replace(/\\/g, '/')} 读取,只用于找回上次停在哪)`);
78
+ summaryParts.push('恢复时先看当前用户消息;如果仍是同一任务,再参考状态文件。');
79
+ summaryParts.push(stateSnapshot.content);
82
80
  }
83
81
 
84
82
  let bootstrap = '';
@@ -103,7 +101,7 @@ export function buildCompactionContext({ payload, pkgRoot, settings, bootstrapFi
103
101
  summaryParts.push(readRootBlock);
104
102
  }
105
103
 
106
- const projectStorageBlock = buildProjectStorageBlock(cwd);
104
+ const projectStorageBlock = buildProjectStorageBlock(cwd, workflowOptions);
107
105
  if (projectStorageBlock) {
108
106
  summaryParts.push('');
109
107
  summaryParts.push(projectStorageBlock);
@@ -111,7 +109,7 @@ export function buildCompactionContext({ payload, pkgRoot, settings, bootstrapFi
111
109
 
112
110
  if (stateSyncHint) {
113
111
  summaryParts.push('');
114
- summaryParts.push('## STATE.md 提醒');
112
+ summaryParts.push('## 状态文件提醒');
115
113
  summaryParts.push(stateSyncHint);
116
114
  }
117
115
 
@@ -123,13 +121,15 @@ export function buildCompactionContext({ payload, pkgRoot, settings, bootstrapFi
123
121
  return summaryParts.join('\n');
124
122
  }
125
123
 
126
- export function buildInjectContext({ source, bootstrap, settings, pkgRoot, host, cwd }) {
124
+ export function buildInjectContext({ source, bootstrap, settings, pkgRoot, host, cwd, payload = {} }) {
125
+ const workflowOptions = { payload };
127
126
  const packageRootBlock = buildPackageRootBlock(pkgRoot);
128
127
  const readRootBlock = buildReadRootBlock(resolveReadRoot({ cwd, pkgRoot, host, settings }));
129
- const workflowHint = buildWorkflowRouteHint(cwd);
130
- const capabilityHint = buildCapabilityHint({ cwd });
131
- const projectStorageBlock = buildProjectStorageBlock(cwd);
132
- const stateSyncHint = buildStateSyncHint(cwd);
128
+ const workflowHint = buildWorkflowRouteHint(cwd, workflowOptions);
129
+ const capabilityHint = buildCapabilityHint({ cwd, options: workflowOptions });
130
+ const projectStorageBlock = buildProjectStorageBlock(cwd, workflowOptions);
131
+ const stateSnapshot = readStateSnapshot(cwd, workflowOptions);
132
+ const stateSyncHint = buildStateSyncHint(cwd, workflowOptions);
133
133
  const settingsBlock = Object.keys(settings).length
134
134
  ? `\n\n## 当前用户设置\n\`\`\`json\n${JSON.stringify(settings, null, 2)}\n\`\`\``
135
135
  : '';
@@ -140,34 +140,36 @@ export function buildInjectContext({ source, bootstrap, settings, pkgRoot, host,
140
140
  if (projectStorageBlock) context += `\n\n${projectStorageBlock}`;
141
141
  if (workflowHint) context += `\n\n## 当前工作流提示\n${workflowHint}`;
142
142
  if (capabilityHint) context += `\n\n## 当前按需能力\n${capabilityHint}`;
143
- if (stateSyncHint) context += `\n\n## STATE.md 提醒\n${stateSyncHint}`;
143
+ if (stateSyncHint) context += `\n\n## 状态文件提醒\n${stateSyncHint}`;
144
144
  context += settingsBlock;
145
145
  if (source === 'resume' || source === 'compact') {
146
- context += '\n\n> ⚠️ 会话已恢复/压缩,请先读取 `.helloagents/STATE.md` 恢复工作状态;先看当前用户消息确认仍是同一任务,再按 STATE.md 接续。';
146
+ context += `\n\n> ⚠️ 会话已恢复/压缩,请先读取 \`state_path\` 指向的 \`${stateSnapshot.statePath.replace(/\\/g, '/')}\`;先看当前用户消息,如果仍是同一任务,再参考状态文件。`;
147
147
  }
148
148
  return context;
149
149
  }
150
150
 
151
- export function buildRouteInstruction({ skillName, extraRules = '', cwd, pkgRoot, host, settings }) {
151
+ export function buildRouteInstruction({ skillName, extraRules = '', cwd, pkgRoot, host, settings, payload = {} }) {
152
+ const workflowOptions = { payload };
152
153
  const readRoot = resolveReadRoot({ cwd, pkgRoot, host, settings });
153
154
  const canonicalSkillName = resolveCanonicalCommandSkill(skillName);
154
155
  const skillPath = join(readRoot.root, 'skills', 'commands', canonicalSkillName, 'SKILL.md');
155
156
  const aliasNote = buildAliasRouteNote(skillName);
156
- const commandHint = buildCommandRouteHint(canonicalSkillName, cwd);
157
- const capabilityHint = buildCapabilityHint({ cwd, skillName: canonicalSkillName });
158
- const projectStorageHint = buildProjectStorageHint(cwd);
157
+ const commandHint = buildCommandRouteHint(canonicalSkillName, cwd, workflowOptions);
158
+ const capabilityHint = buildCapabilityHint({ cwd, skillName: canonicalSkillName, options: workflowOptions });
159
+ const projectStorageHint = buildProjectStorageHint(cwd, workflowOptions);
159
160
  return `用户使用了 ~${skillName} 命令。当前命令技能文件已解析为:${skillPath}。请直接读取这个 SKILL.md;不要再探测其他 helloagents 路径。${aliasNote ? ` ${aliasNote}` : ''}${projectStorageHint ? ` ${projectStorageHint}` : ''}${commandHint ? ` ${commandHint}` : ''}${capabilityHint ? ` ${capabilityHint}` : ''}${extraRules}`;
160
161
  }
161
162
 
162
- export function buildSemanticRouteInstruction(cwd) {
163
- const workflowHint = buildWorkflowRouteHint(cwd);
164
- const capabilityHint = buildCapabilityHint({ cwd });
165
- const projectStorageHint = buildProjectStorageHint(cwd);
163
+ export function buildSemanticRouteInstruction(cwd, payload = {}) {
164
+ const workflowOptions = { payload };
165
+ const workflowHint = buildWorkflowRouteHint(cwd, workflowOptions);
166
+ const capabilityHint = buildCapabilityHint({ cwd, options: workflowOptions });
167
+ const projectStorageHint = buildProjectStorageHint(cwd, workflowOptions);
166
168
  return [
167
169
  '当前消息未使用 ~command。',
168
170
  '请根据用户请求的真实意图选路,不依赖关键词表。',
169
- 'Delivery Tier: T0=探索/比较;T1=低风险小改动或显式验证;T2=多文件功能/新项目/需要结构化产物;T3=高风险或不可逆链路。',
170
- '路由映射:~idea=只读探索,不创建文件;~build=明确实现;~verify=审查/验证;~plan=结构化规划;~prd=重型规格;~auto=自动编排并自动衔接后续阶段。',
171
+ 'Delivery Tier: T0=探索/比较;T1=低风险小改动或显式验证;T2=多文件功能/新项目/需要结构化产物;T3=高风险或不可逆操作。',
172
+ '路由映射:~idea=只读探索,不创建文件;~build=明确实现;~verify=审查/验证;~plan=结构化规划;~prd=重型规格;~auto=自动选择并继续执行后续阶段。',
171
173
  '若判定为 T3,默认先走 ~plan / ~prd;纯审查/验证请求才优先 ~verify。',
172
174
  `涉及 UI 任务时,设计决策优先级:当前活跃 plan / PRD → ${describeProjectStoreFile(cwd, 'DESIGN.md')} → 通用 UI 规则。`,
173
175
  projectStorageHint,
@@ -5,11 +5,3 @@ export function shouldIgnoreCodexNotifyClient(client) {
5
5
  export function shouldIgnoreFormattedSubagent(lastMsg, outputFormatEnabled) {
6
6
  return outputFormatEnabled && !lastMsg.includes('【HelloAGENTS】');
7
7
  }
8
-
9
- export function claimsTaskComplete(lastMsg) {
10
- if (!lastMsg) return false;
11
- if (/^✅【HelloAGENTS】- .*(当前任务已完成|任务已完成|已修复|完成交付|done|fixed|completed|finished)/im.test(lastMsg)) {
12
- return true;
13
- }
14
- return /(当前任务已完成|任务已完成|已全部完成|已修复|修复完成|\b(done|fixed|completed|finished)\b)/i.test(lastMsg);
15
- }
@@ -0,0 +1,128 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { spawnSync } from 'node:child_process'
3
+
4
+ function truncateText(value = '') {
5
+ const text = String(value || '').trim()
6
+ return text.length > 1000 ? `${text.slice(0, 1000)}\n...(truncated)` : text
7
+ }
8
+
9
+ function buildGateErrorReason(source, detail = '') {
10
+ return [
11
+ `[HelloAGENTS Runtime] ${source} 执行失败,已暂停完成通知。`,
12
+ detail ? `原因:${detail}` : '',
13
+ '请修复脚本或重新运行验证后再报告完成。',
14
+ ].filter(Boolean).join('\n')
15
+ }
16
+
17
+ function emitGateError({
18
+ payload,
19
+ host,
20
+ source,
21
+ reason,
22
+ appendReplayEvent,
23
+ output,
24
+ }) {
25
+ appendReplayEvent(payload.cwd || process.cwd(), {
26
+ host,
27
+ event: 'runtime_gate_error',
28
+ source,
29
+ reason,
30
+ })
31
+ output({
32
+ decision: 'block',
33
+ reason,
34
+ suppressOutput: true,
35
+ })
36
+ return true
37
+ }
38
+
39
+ export function runGateScript({
40
+ payload,
41
+ host,
42
+ scriptPath,
43
+ args = [],
44
+ source,
45
+ blockEvent,
46
+ timeout,
47
+ appendReplayEvent,
48
+ output,
49
+ }) {
50
+ if (!existsSync(scriptPath)) {
51
+ return emitGateError({
52
+ payload,
53
+ host,
54
+ source,
55
+ reason: buildGateErrorReason(source, `脚本不存在:${scriptPath}`),
56
+ appendReplayEvent,
57
+ output,
58
+ })
59
+ }
60
+
61
+ const result = spawnSync(process.execPath, [scriptPath, ...args], {
62
+ input: JSON.stringify(payload),
63
+ encoding: 'utf-8',
64
+ timeout,
65
+ })
66
+
67
+ if (result.error) {
68
+ return emitGateError({
69
+ payload,
70
+ host,
71
+ source,
72
+ reason: buildGateErrorReason(source, result.error.message),
73
+ appendReplayEvent,
74
+ output,
75
+ })
76
+ }
77
+
78
+ if (result.status !== 0) {
79
+ const detail = truncateText(`${result.stderr || ''}\n${result.stdout || ''}`) || `退出码 ${result.status}`
80
+ return emitGateError({
81
+ payload,
82
+ host,
83
+ source,
84
+ reason: buildGateErrorReason(source, detail),
85
+ appendReplayEvent,
86
+ output,
87
+ })
88
+ }
89
+
90
+ const stdout = String(result.stdout || '').trim()
91
+ if (!stdout) {
92
+ return emitGateError({
93
+ payload,
94
+ host,
95
+ source,
96
+ reason: buildGateErrorReason(source, '脚本未返回有效结果'),
97
+ appendReplayEvent,
98
+ output,
99
+ })
100
+ }
101
+
102
+ let gateOutput
103
+ try {
104
+ gateOutput = JSON.parse(stdout)
105
+ } catch {
106
+ return emitGateError({
107
+ payload,
108
+ host,
109
+ source,
110
+ reason: buildGateErrorReason(source, `脚本返回了无法解析的 JSON:${truncateText(stdout)}`),
111
+ appendReplayEvent,
112
+ output,
113
+ })
114
+ }
115
+
116
+ if (gateOutput.decision === 'block') {
117
+ appendReplayEvent(payload.cwd || process.cwd(), {
118
+ host,
119
+ event: blockEvent,
120
+ source,
121
+ reason: gateOutput.reason || '',
122
+ })
123
+ output(gateOutput)
124
+ return true
125
+ }
126
+
127
+ return false
128
+ }
@@ -17,6 +17,7 @@ function buildHelpExtraRules(skillName) {
17
17
 
18
18
  function routeExplicitCommand({
19
19
  prompt,
20
+ payload,
20
21
  cwd,
21
22
  host,
22
23
  pkgRoot,
@@ -51,6 +52,7 @@ function routeExplicitCommand({
51
52
  pkgRoot,
52
53
  host,
53
54
  settings,
55
+ payload,
54
56
  }))
55
57
  return true
56
58
  }
@@ -80,6 +82,7 @@ export function handleRouteCommand({
80
82
 
81
83
  if (routeExplicitCommand({
82
84
  prompt,
85
+ payload,
83
86
  cwd,
84
87
  host,
85
88
  pkgRoot,
@@ -100,9 +103,9 @@ export function handleRouteCommand({
100
103
  host,
101
104
  event: 'semantic_route_prompted',
102
105
  source: 'route',
103
- recommendation: getWorkflowRecommendation(cwd),
106
+ recommendation: getWorkflowRecommendation(cwd, { payload }),
104
107
  })
105
- suppress(buildSemanticRouteInstruction(cwd))
108
+ suppress(buildSemanticRouteInstruction(cwd, payload))
106
109
  return
107
110
  }
108
111