helloagents 3.0.18 → 3.0.19

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.
@@ -0,0 +1,58 @@
1
+ const PAYLOAD_KEY_ALIASES = {
2
+ 'thread-id': 'threadId',
3
+ thread_id: 'threadId',
4
+ 'turn-id': 'turnId',
5
+ turn_id: 'turnId',
6
+ 'session-id': 'sessionId',
7
+ session_id: 'sessionId',
8
+ 'last-assistant-message': 'lastAssistantMessage',
9
+ last_assistant_message: 'lastAssistantMessage',
10
+ 'input-messages': 'inputMessages',
11
+ input_messages: 'inputMessages',
12
+ hook_event_name: 'hookEventName',
13
+ permission_mode: 'permissionMode',
14
+ stop_hook_active: 'stopHookActive',
15
+ 'goal-id': 'goalId',
16
+ goal_id: 'goalId',
17
+ }
18
+
19
+ function assignAlias(target, source, sourceKey, targetKey) {
20
+ if (!Object.prototype.hasOwnProperty.call(source, sourceKey)) return
21
+ if (target[targetKey] !== undefined) return
22
+ target[targetKey] = source[sourceKey]
23
+ }
24
+
25
+ function readMessageText(entry) {
26
+ if (typeof entry === 'string') return entry
27
+ if (!entry || typeof entry !== 'object') return ''
28
+ if (typeof entry.text === 'string') return entry.text
29
+ if (typeof entry.content === 'string') return entry.content
30
+ if (Array.isArray(entry.content)) {
31
+ return entry.content
32
+ .map((part) => (typeof part === 'string' ? part : part?.text || ''))
33
+ .filter(Boolean)
34
+ .join('\n')
35
+ }
36
+ return ''
37
+ }
38
+
39
+ export function normalizeNotifyPayload(payload = {}) {
40
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return {}
41
+ const normalized = { ...payload }
42
+
43
+ for (const [sourceKey, targetKey] of Object.entries(PAYLOAD_KEY_ALIASES)) {
44
+ assignAlias(normalized, payload, sourceKey, targetKey)
45
+ }
46
+
47
+ if (!normalized.prompt && Array.isArray(normalized.inputMessages)) {
48
+ normalized.prompt = normalized.inputMessages
49
+ .map(readMessageText)
50
+ .filter(Boolean)
51
+ .join('\n')
52
+ .trim()
53
+ }
54
+
55
+ return normalized
56
+ }
57
+
58
+ export { PAYLOAD_KEY_ALIASES }
@@ -11,9 +11,11 @@ import { resolveNotificationSource } from './notify-source.mjs';
11
11
  import { buildCompactionContext, buildInjectContext, buildRouteInstruction, buildSemanticRouteInstruction, resolveCanonicalCommandSkill } from './notify-context.mjs';
12
12
  import { resolveNotifyHost, shouldIgnoreCodexNotifyClient } from './notify-events.mjs';
13
13
  import { runGateScript } from './notify-gates.mjs';
14
+ import { normalizeNotifyPayload } from './notify-payload.mjs';
14
15
  import { handleRouteCommand, resolveBootstrapFile } from './notify-route.mjs';
15
16
  import { readSettings, readStdinJson, output, suppressedOutput, emptySuppress } from './notify-shared.mjs';
16
17
  import { clearRouteContext, getApplicableRouteContext, writeRouteContext } from './runtime-context.mjs';
18
+ import { readRuntimeEvidence, writeRuntimeEvidence } from './runtime-artifacts.mjs';
17
19
  import { appendReplayEvent, startReplaySession } from './replay-state.mjs';
18
20
  import { clearTurnState, readTurnState } from './turn-state.mjs';
19
21
  import { getWorkflowRecommendation } from './workflow-state.mjs';
@@ -27,12 +29,15 @@ const CONFIG_FILE = join(homedir(), '.helloagents', 'helloagents.json');
27
29
  const cmd = process.argv[2] || '';
28
30
  const HOST = resolveNotifyHost(process.argv);
29
31
  const IS_GEMINI = HOST === 'gemini';
32
+ const IS_CODEX = HOST === 'codex';
33
+ const IS_SILENT = process.argv.includes('--silent');
30
34
  const EVENT_NAME = {
31
35
  SessionStart: 'SessionStart',
32
36
  UserPromptSubmit: IS_GEMINI ? 'BeforeAgent' : 'UserPromptSubmit',
33
37
  PreCompact: IS_GEMINI ? 'BeforeAgent' : 'PreCompact',
34
38
  };
35
39
  const RALPH_LOOP_ROUTE_COMMANDS = new Set(['verify', 'loop']);
40
+ const CODEX_NATIVE_STOP_FILE = 'codex-native-stop.json';
36
41
 
37
42
  const playSound = (event) => _playSound(PKG_ROOT, event);
38
43
  const desktopNotify = (event, extra) => _desktopNotify(PKG_ROOT, event, extra);
@@ -52,6 +57,10 @@ function notifyByLevel(event, extra, settings = getSettings()) {
52
57
  }
53
58
  }
54
59
 
60
+ function readPayloadFromStdin() {
61
+ return normalizeNotifyPayload(readStdinJson());
62
+ }
63
+
55
64
  function buildNotifyExtra(payload = {}, options = {}) {
56
65
  const source = resolveNotificationSource({
57
66
  host: HOST,
@@ -130,6 +139,27 @@ function attachTurnSession(payload = {}, cwd = payload.cwd || process.cwd()) {
130
139
  return { ...payload, sessionId };
131
140
  }
132
141
 
142
+ function getCodexTurnId(payload = {}) {
143
+ return String(payload.turnId || payload.turn_id || payload['turn-id'] || '').trim();
144
+ }
145
+
146
+ function markCodexNativeStopProcessed(cwd, payload = {}) {
147
+ if (!IS_CODEX) return;
148
+ const turnId = getCodexTurnId(payload);
149
+ if (!turnId) return;
150
+ writeRuntimeEvidence(cwd, CODEX_NATIVE_STOP_FILE, {
151
+ turnId,
152
+ updatedAt: new Date().toISOString(),
153
+ }, { payload });
154
+ }
155
+
156
+ function hasCodexNativeStopProcessed(cwd, payload = {}) {
157
+ const turnId = getCodexTurnId(payload);
158
+ if (!turnId) return false;
159
+ const evidence = readRuntimeEvidence(cwd, CODEX_NATIVE_STOP_FILE, { payload });
160
+ return evidence?.turnId === turnId;
161
+ }
162
+
133
163
  function readMainTurnState(cwd, payload = {}) {
134
164
  const turnState = readTurnState(cwd, { payload });
135
165
  return turnState?.role === 'main' ? turnState : null;
@@ -145,7 +175,7 @@ function shouldProcessCloseout(turnState) {
145
175
  }
146
176
 
147
177
  function cmdPreCompact() {
148
- const payload = readStdinJson();
178
+ const payload = readPayloadFromStdin();
149
179
  const cwd = payload.cwd || process.cwd();
150
180
  const settings = getSettings();
151
181
  const bootstrapFile = resolveBootstrapFile(cwd, settings, HOST);
@@ -170,37 +200,34 @@ function cmdPreCompact() {
170
200
  }
171
201
 
172
202
  function cmdRoute() {
173
- const payload = readStdinJson();
203
+ const payload = readPayloadFromStdin();
174
204
  clearTurnState(payload.cwd || process.cwd(), { payload });
175
205
  handleRouteCommand({
176
206
  payload,
177
207
  host: HOST,
178
208
  pkgRoot: PKG_ROOT,
179
209
  settings: getSettings(),
180
- buildRouteInstruction,
181
- buildSemanticRouteInstruction,
210
+ buildRouteInstruction: IS_SILENT ? () => null : buildRouteInstruction,
211
+ buildSemanticRouteInstruction: IS_SILENT ? () => null : buildSemanticRouteInstruction,
182
212
  resolveCanonicalCommandSkill,
183
213
  writeRouteContext,
184
214
  clearRouteContext,
185
215
  appendReplayEvent,
186
216
  getWorkflowRecommendation,
187
- suppress: (context) => suppressedOutput(EVENT_NAME.UserPromptSubmit, context),
217
+ suppress: (context) => IS_SILENT
218
+ ? emptySuppress()
219
+ : suppressedOutput(EVENT_NAME.UserPromptSubmit, context),
188
220
  emptySuppress,
189
221
  });
190
222
  }
191
223
 
192
224
  function cmdInject() {
193
- const payload = readStdinJson();
225
+ const payload = readPayloadFromStdin();
194
226
  const source = payload.source || 'startup';
195
227
  const cwd = payload.cwd || process.cwd();
196
228
  const settings = getSettings();
197
229
  const bootstrapFile = resolveBootstrapFile(cwd, settings, HOST);
198
230
 
199
- let bootstrap = '';
200
- try {
201
- bootstrap = readFileSync(join(PKG_ROOT, bootstrapFile), 'utf-8');
202
- } catch {}
203
-
204
231
  startReplaySession(cwd, {
205
232
  host: HOST,
206
233
  source,
@@ -219,6 +246,17 @@ function cmdInject() {
219
246
  activatedProject: isProjectRuntimeActive(cwd),
220
247
  },
221
248
  });
249
+ clearRouteContext({ cwd, payload });
250
+ clearTurnState(cwd, { payload });
251
+ if (IS_SILENT) {
252
+ emptySuppress();
253
+ return;
254
+ }
255
+
256
+ let bootstrap = '';
257
+ try {
258
+ bootstrap = readFileSync(join(PKG_ROOT, bootstrapFile), 'utf-8');
259
+ } catch {}
222
260
  const context = buildInjectContext({
223
261
  source,
224
262
  bootstrap,
@@ -228,29 +266,34 @@ function cmdInject() {
228
266
  cwd,
229
267
  payload,
230
268
  });
231
- clearRouteContext({ cwd, payload });
232
- clearTurnState(cwd, { payload });
233
269
  suppressedOutput(EVENT_NAME.SessionStart, context || undefined);
234
270
  }
235
271
 
236
272
  function cmdStop() {
237
- const payload = readStdinJson();
273
+ const payload = readPayloadFromStdin();
238
274
  const cwd = payload.cwd || process.cwd();
239
275
  const turnPayload = attachTurnSession(payload, cwd);
276
+ if (IS_CODEX && hasCodexNativeStopProcessed(cwd, turnPayload)) {
277
+ emptySuppress();
278
+ return;
279
+ }
240
280
  const turnState = readMainTurnState(cwd, turnPayload);
241
281
  if (runTurnStopGate(turnPayload)) {
242
282
  if (turnState && turnState.kind !== 'complete') consumeMainTurnState(cwd, turnState, turnPayload);
283
+ markCodexNativeStopProcessed(cwd, turnPayload);
243
284
  return;
244
285
  }
245
286
  const shouldProcess = shouldProcessCloseout(turnState);
246
287
  if (shouldProcess && runRalphLoop(turnPayload, { turnState })) {
247
288
  consumeMainTurnState(cwd, turnState, turnPayload);
248
289
  notifyByLevel('warning', buildNotifyExtra(payload));
290
+ markCodexNativeStopProcessed(cwd, turnPayload);
249
291
  return;
250
292
  }
251
293
  if (shouldProcess && runDeliveryGate(turnPayload)) {
252
294
  consumeMainTurnState(cwd, turnState, turnPayload);
253
295
  notifyByLevel('warning', buildNotifyExtra(payload));
296
+ markCodexNativeStopProcessed(cwd, turnPayload);
254
297
  return;
255
298
  }
256
299
 
@@ -260,6 +303,7 @@ function cmdStop() {
260
303
  }
261
304
  consumeMainTurnState(cwd, turnState, turnPayload);
262
305
  clearRouteContext({ cwd, payload: turnPayload });
306
+ markCodexNativeStopProcessed(cwd, turnPayload);
263
307
  emptySuppress();
264
308
  }
265
309
 
@@ -274,6 +318,7 @@ function cmdDesktop() {
274
318
  function cmdCodexNotify() {
275
319
  let data = {};
276
320
  try { data = JSON.parse(process.argv[3] || '{}'); } catch {}
321
+ data = normalizeNotifyPayload(data);
277
322
  const cwd = data.cwd || process.cwd();
278
323
  const turnPayload = attachTurnSession(data, cwd);
279
324
 
@@ -286,6 +331,7 @@ function cmdCodexNotify() {
286
331
  return;
287
332
  }
288
333
  if (type !== 'agent-turn-complete') return;
334
+ if (hasCodexNativeStopProcessed(cwd, turnPayload)) return;
289
335
 
290
336
  const turnState = readMainTurnState(cwd, turnPayload);
291
337
  if (runTurnStopGate(turnPayload)) {
@@ -7,8 +7,9 @@ import {
7
7
  readSessionArtifact,
8
8
  writeSessionArtifact,
9
9
  } from './session-capsule.mjs'
10
+ import { EVIDENCE_MAX_AGE_MS, LONG_RUNNING_TTL_HOURS } from './runtime-ttl.mjs'
10
11
 
11
- export const EVIDENCE_MAX_AGE_MS = 30 * 60 * 1000
12
+ export { EVIDENCE_MAX_AGE_MS }
12
13
 
13
14
  function readGitDiffStat(cwd, args) {
14
15
  try {
@@ -23,16 +24,31 @@ function readGitDiffStat(cwd, args) {
23
24
  }
24
25
  }
25
26
 
27
+ function readGitHead(cwd) {
28
+ try {
29
+ return execSync('git rev-parse HEAD', {
30
+ cwd,
31
+ encoding: 'utf-8',
32
+ timeout: 10_000,
33
+ stdio: ['pipe', 'pipe', 'pipe'],
34
+ }).trim()
35
+ } catch {
36
+ return null
37
+ }
38
+ }
39
+
26
40
  export function captureWorkspaceFingerprint(cwd) {
41
+ const head = readGitHead(cwd)
27
42
  const unstaged = readGitDiffStat(cwd, 'HEAD')
28
43
  const staged = readGitDiffStat(cwd, '--cached')
29
- const available = unstaged !== null || staged !== null
44
+ const available = head !== null || unstaged !== null || staged !== null
30
45
 
31
46
  return {
32
47
  available,
48
+ head: head || '',
33
49
  unstaged: unstaged || '',
34
50
  staged: staged || '',
35
- combined: `${unstaged || ''}\n---\n${staged || ''}`.trim(),
51
+ combined: [`HEAD:${head || ''}`, unstaged || '', staged || ''].join('\n---\n').trim(),
36
52
  }
37
53
  }
38
54
 
@@ -71,7 +87,7 @@ export function validateEvidenceTimestamp(evidence, now, label) {
71
87
  required: true,
72
88
  status: 'stale-time',
73
89
  evidence,
74
- details: [`${label}超过 30 分钟`],
90
+ details: [`${label}超过 ${LONG_RUNNING_TTL_HOURS} 小时`],
75
91
  }
76
92
  }
77
93
  return null
@@ -1,4 +1,5 @@
1
1
  import { normalize, resolve } from 'node:path'
2
+ import { createHash } from 'node:crypto'
2
3
 
3
4
  import {
4
5
  clearCapsuleSection,
@@ -6,13 +7,66 @@ import {
6
7
  readCapsuleSection,
7
8
  writeCapsuleSection,
8
9
  } from './session-capsule.mjs'
10
+ import { ROUTE_CONTEXT_TTL_MS } from './runtime-ttl.mjs'
9
11
 
10
- const ROUTE_CONTEXT_TTL_MS = 30 * 60 * 1000
12
+ export const UNBOUND_ROUTE_CONTEXT_TTL_MS = 10 * 60 * 1000
11
13
 
12
14
  function normalizePath(filePath = '') {
13
15
  return filePath ? normalize(resolve(filePath)) : ''
14
16
  }
15
17
 
18
+ function normalizeIdentityValue(value) {
19
+ return String(value || '').trim()
20
+ }
21
+
22
+ function extractPayloadIdentity(payload = {}) {
23
+ return {
24
+ sessionId: normalizeIdentityValue(payload.sessionId || payload.session_id || payload['session-id']),
25
+ threadId: normalizeIdentityValue(payload.threadId || payload.thread_id || payload['thread-id']),
26
+ turnId: normalizeIdentityValue(payload.turnId || payload.turn_id || payload['turn-id']),
27
+ goalId: normalizeIdentityValue(payload.goalId || payload.goal_id || payload['goal-id']),
28
+ }
29
+ }
30
+
31
+ function hashPrompt(prompt = '') {
32
+ const text = String(prompt || '').trim()
33
+ if (!text) return ''
34
+ return createHash('sha1').update(text).digest('hex').slice(0, 16)
35
+ }
36
+
37
+ function routeSource(payload = {}) {
38
+ return normalizeIdentityValue(payload.hookEventName || payload.source || payload.type || '')
39
+ }
40
+
41
+ function hasTurnBinding(identity = {}) {
42
+ return Boolean(identity.turnId)
43
+ }
44
+
45
+ function hasAnyIdentity(identity = {}) {
46
+ return Boolean(identity.sessionId || identity.threadId || identity.turnId || identity.goalId)
47
+ }
48
+
49
+ function routeContextMaxAge(context = {}) {
50
+ return hasTurnBinding(context.identity) ? ROUTE_CONTEXT_TTL_MS : UNBOUND_ROUTE_CONTEXT_TTL_MS
51
+ }
52
+
53
+ function identityFieldsMatch(contextIdentity = {}, payloadIdentity = {}) {
54
+ for (const key of ['sessionId', 'threadId', 'turnId', 'goalId']) {
55
+ if (contextIdentity[key] && !payloadIdentity[key]) return false
56
+ if (contextIdentity[key] && payloadIdentity[key] && contextIdentity[key] !== payloadIdentity[key]) return false
57
+ }
58
+ return true
59
+ }
60
+
61
+ function payloadMatchesRouteContext(context = {}, payload = {}) {
62
+ if (!Object.prototype.hasOwnProperty.call(context, 'identity')) return false
63
+
64
+ const contextIdentity = context.identity || {}
65
+ if (!hasAnyIdentity(contextIdentity)) return true
66
+
67
+ return identityFieldsMatch(contextIdentity, extractPayloadIdentity(payload))
68
+ }
69
+
16
70
  function resolvePayload(options = {}) {
17
71
  return options.payload && typeof options.payload === 'object' ? options.payload : options
18
72
  }
@@ -30,6 +84,9 @@ export function writeRouteContext({ cwd, skillName, sourceSkillName = skillName,
30
84
  skillName,
31
85
  sourceSkillName,
32
86
  zeroSideEffect: skillName === 'idea',
87
+ identity: extractPayloadIdentity(payload),
88
+ source: routeSource(payload),
89
+ promptHash: hashPrompt(payload.prompt),
33
90
  scope: scope.scope,
34
91
  key: scope.key,
35
92
  updatedAt: Date.now(),
@@ -44,7 +101,7 @@ export function readRouteContext(options = {}) {
44
101
  if (!context?.cwd || !context?.skillName || !context?.updatedAt) {
45
102
  return null
46
103
  }
47
- if (Date.now() - context.updatedAt > ROUTE_CONTEXT_TTL_MS) {
104
+ if (Date.now() - context.updatedAt > routeContextMaxAge(context)) {
48
105
  clearRouteContext({ cwd, payload, env: options.env, ppid: options.ppid })
49
106
  return null
50
107
  }
@@ -58,6 +115,7 @@ export function readRouteContext(options = {}) {
58
115
  export function getApplicableRouteContext({ cwd = '', filePath = '', payload = {}, env, ppid } = {}) {
59
116
  const context = readRouteContext({ cwd, payload, env, ppid })
60
117
  if (!context) return null
118
+ if (!payloadMatchesRouteContext(context, payload)) return null
61
119
 
62
120
  const normalizedCwd = normalizePath(cwd)
63
121
  if (normalizedCwd && normalizedCwd === context.cwd) {
@@ -5,6 +5,7 @@ import { dirname, join, normalize, resolve } from 'node:path'
5
5
  import { homedir } from 'node:os'
6
6
 
7
7
  import { resolveSessionToken } from './session-token.mjs'
8
+ import { USER_RUNTIME_MAX_AGE_MS } from './runtime-ttl.mjs'
8
9
 
9
10
  export const PROJECT_DIR_NAME = '.helloagents'
10
11
  export const PROJECT_SESSIONS_DIR_NAME = 'sessions'
@@ -13,7 +14,7 @@ export const CAPSULE_FILE_NAME = 'capsule.json'
13
14
  export const EVENTS_FILE_NAME = 'events.jsonl'
14
15
  export const DEFAULT_STATE_SESSION_TOKEN = 'default'
15
16
  export const USER_RUNTIME_DIR_NAME = 'runtime'
16
- export const USER_RUNTIME_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000
17
+ export { USER_RUNTIME_MAX_AGE_MS }
17
18
 
18
19
  function normalizePath(filePath = '') {
19
20
  return filePath ? normalize(resolve(filePath)) : ''
@@ -0,0 +1,7 @@
1
+ export const LONG_RUNNING_TTL_HOURS = 720
2
+ export const LONG_RUNNING_TTL_MS = LONG_RUNNING_TTL_HOURS * 60 * 60 * 1000
3
+
4
+ export const ROUTE_CONTEXT_TTL_MS = LONG_RUNNING_TTL_MS
5
+ export const TURN_STATE_TTL_MS = LONG_RUNNING_TTL_MS
6
+ export const EVIDENCE_MAX_AGE_MS = LONG_RUNNING_TTL_MS
7
+ export const USER_RUNTIME_MAX_AGE_MS = LONG_RUNNING_TTL_MS
@@ -7,6 +7,7 @@ const PAYLOAD_SESSION_KEYS = [
7
7
  'conversation',
8
8
  'threadId',
9
9
  'thread_id',
10
+ 'thread-id',
10
11
  'thread',
11
12
  'windowId',
12
13
  'window_id',
@@ -10,8 +10,8 @@ import {
10
10
  readCapsuleSection,
11
11
  writeCapsuleSection,
12
12
  } from './session-capsule.mjs'
13
+ import { TURN_STATE_TTL_MS } from './runtime-ttl.mjs'
13
14
 
14
- const TURN_STATE_TTL_MS = 30 * 60 * 1000
15
15
  const VALID_KINDS = new Set(['complete', 'waiting', 'blocked', 'progress'])
16
16
  const VALID_ROLES = new Set(['main', 'subagent'])
17
17
  const VALID_REASON_CATEGORIES = new Set([
@@ -23,7 +23,7 @@ const PLAN_TEMPLATE_MARKERS = {
23
23
  'tasks.md': [
24
24
  /# \{项目\/功能名称\} — 任务分解/,
25
25
  /\[按执行顺序排列,每个任务独立可验证\]/,
26
- /- \[ \] 任务1:描述/,
26
+ /- \[ \] 任务1(AFK\/HITL):端到端行为描述/,
27
27
  ],
28
28
  }
29
29
 
@@ -15,7 +15,7 @@ Trigger: ~auto <任务描述>
15
15
  - `T3` 高风险或不可逆操作默认不直接进入 `~build`;优先先走 `~plan` 或 `~prd`,纯审查/纯验证请求才可先进入 `~verify`
16
16
  - 主路径一旦确定,立即读取对应 command skill,并在阶段完成后继续执行后续阶段,避免同一任务重复探索或重复等待
17
17
  - 选路不替代授权;涉及外部副作用或高风险不可逆操作时,仍遵守 bootstrap 的阻塞判定与确认规则
18
- - 用户显式使用 `~auto`,表示已授权在当前任务边界内沿选定主路径持续执行;`~plan` / `~prd` 作为中间阶段时,不再额外询问“是否开始执行”,除非仍有真实阻塞
18
+ - 用户显式使用 `~auto`,表示已授权在当前任务边界内沿选定主路径持续执行;若本轮运行在 Codex `/goal` 下,`/goal` 只提供长程续跑与预算,`~auto` 仍按方案包、`state_path` 与验证契约推进;`~plan` / `~prd` 作为中间阶段时,不再额外询问“是否开始执行”,除非仍有真实阻塞;不得把 `🔄 下一步` 当作阶段交接或继续执行占位
19
19
  - 优先消费当前上下文中已注入的 ROUTE / TIER、当前工作流约束与项目状态;不要在 `~auto` 内另建一套关键词路由表
20
20
 
21
21
  ## 流程
@@ -27,6 +27,7 @@ Trigger: ~auto <任务描述>
27
27
  - 活跃方案包不完整或缺少任务清单 → 先 `~plan`
28
28
  - 活跃方案包仍在执行 → 先 `~build`,完成当前实现后再 `~verify`
29
29
  - 活跃方案包已闭合 → 先 `~verify` 或收尾
30
+ - Codex active goal 已关联方案包 → 按 `tasks.md` 未完成 AFK 项、`contract.json` 与 `state_path` 确定主路径,不把 goal 目标原文替代方案包
30
31
  - 只有当用户明确要求换方向、重做方案,或现有方案已失效时,才偏离当前推荐重新规划
31
32
 
32
33
  ### 1. 选路
@@ -64,9 +65,10 @@ Trigger: ~auto <任务描述>
64
65
  - 若主路径是 `~prd` → PRD / 任务 / 契约写入后,若当前任务来自 `~auto` 且未命中阻塞判定,按当前结果继续进入 `~build`,必要时先补一轮轻量 `~plan`
65
66
  - 若主路径是 `~verify` → 完成审查 / 验证 / 收尾后结束
66
67
  - 若主路径是 `~idea`,且用户本意就是探索/比较,则在探索输出后结束;若探索后已有明确方向且当前任务仍要求写文件或改代码,则继续进入 `~plan` 或 `~build`
68
+ - 若 Codex active goal 的目标已满足 → 仍先完成 `~verify` 与 HelloAGENTS 收尾,再标记 goal complete;未满足时继续下一项可执行 AFK 任务
67
69
 
68
70
  ### 5. 何时允许停下
69
71
 
70
72
  - 仅在 bootstrap 的阻塞判定成立时停下:真实歧义、缺必需信息/文件/凭据、未授权外部副作用、高风险或不可逆操作
71
73
  - 不得把 `~plan` / `~prd` 中“是否进入执行”的默认询问原样带入 `~auto` 流程
72
- - 不得把“给出方案”“给出任务列表”“给出建议下一步”当作 `~auto` 的默认完成态;用户显式要求 `~auto` 时,默认目标是把当前任务推进到可交付完成
74
+ - 不得把“给出方案”“给出任务列表”“未执行修改”“等待下一步确认”“给出建议下一步”当作 `~auto` 的默认完成态;用户显式要求 `~auto` 时,默认目标是把当前任务推进到可交付完成
@@ -20,7 +20,7 @@ Trigger: ~build [description]
20
20
 
21
21
  ### 1. 恢复与定位
22
22
 
23
- - 优先按当前已加载 bootstrap 的“.helloagents/ 文件读取优先级”恢复当前任务;若当前消息明确要继续上次任务,或会话刚经历恢复 / 压缩,先读取 `state_path`,再用当前用户消息、活跃方案包 / PRD 与代码事实确认当前任务
23
+ - 优先按当前已加载 bootstrap 的“.helloagents/ 文件读取优先级”恢复当前任务;若当前消息明确要继续上次任务、会话刚经历恢复 / 压缩,或本轮运行在 Codex active goal 下,先读取 `state_path`,再用当前用户消息、活跃方案包 / PRD 与代码事实确认当前任务
24
24
  - 若存在最近的活跃方案包,读取对应的:
25
25
  - `requirements.md`
26
26
  - `plan.md`
@@ -28,6 +28,7 @@ Trigger: ~build [description]
28
28
  - `contract.json`
29
29
  - 实现时优先把 `tasks.md` 中每个任务的“完成标准”当作本轮实现约束,不要只按任务标题猜测范围
30
30
  - `contract.json` 存在时,优先按其中的 `verifyMode`、`reviewerFocus`、`testerFocus` 理解后续验证边界
31
+ - 若本轮运行在 Codex active goal 下,按 `tasks.md` 未完成项、`contract.json` 与 `state_path` 恢复实现位置;不要自动创建新 goal,也不要把 goal 目标原文替代方案包
31
32
  - 若当前上下文中已注入“当前工作流约束”或“当前建议下一命令”,先服从它;只有推荐仍为 `~build`,或用户明确提出新增实现范围时,才继续 `~build`
32
33
  - 其余项目知识库与相关代码文件,按 bootstrap 的项目上下文规则按需读取
33
34
  - 若任务涉及 UI,按以下优先级读取并遵循:当前活跃 `plan.md` / PRD 中的 UI 决策 > 逻辑 `.helloagents/DESIGN.md`(实际路径按当前项目存储模式解析) > `hello-ui` 通用规则
@@ -63,5 +64,6 @@ Trigger: ~build [description]
63
64
 
64
65
  - 有方案包时,只同步本次实现直接影响的任务状态;未完成项保持打开
65
66
  - 当前实现已闭合、且需要进入交付或收尾时,转入 `~verify`
67
+ - 若 Codex active goal 仍有未完成 AFK 任务,继续下一项可执行任务;若目标已满足,先转入 `~verify` 与 HelloAGENTS 收尾,再标记 goal complete
66
68
  - 状态文件、知识库、`CHANGELOG.md`、modules 文档与归档边界,按当前已加载 bootstrap 的 VERIFY / CONSOLIDATE 规则执行
67
69
  - 不在 `~build` 内把仍未闭合的方案包整体报告为已完成
@@ -37,8 +37,9 @@ iteration commit metric delta guard status description
37
37
 
38
38
  `~loop` 的八阶段循环是统一执行流程(ROUTE/TIER→SPEC→PLAN→BUILD→VERIFY→CONSOLIDATE)在迭代优化场景下的特化形式。每轮迭代的“修改”阶段遵循已标记的 hello-* 质量技能规范,“验证”阶段遵循 hello-verify 的验证规范。
39
39
  执行 `~loop` 时,涉及公共阶段边界、阻塞判定与收尾要求的部分,仍按当前已加载 bootstrap 执行;本 skill 负责补充 loop 场景的迭代顺序与回滚规则。
40
+ 若本轮运行在 Codex `/goal` 下,`/goal` 只作为外层长程续跑与预算控制;`~loop` 仍负责指标、守卫、实验提交、keep/revert、results log、`state_path` 与收尾验证,不把 `/goal` 当成循环逻辑本身。
40
41
 
41
- 除非达到迭代上限或命中阻塞判定,否则继续执行,不额外询问是否继续。
42
+ 除非达到迭代上限或命中阻塞判定,否则继续执行,不额外询问是否继续,也不把 `🔄 下一步` 当作单轮结果或继续执行占位。
42
43
  每轮迭代必须完整走完以下八个阶段:
43
44
 
44
45
  ### 第 1 阶段:回顾
@@ -91,6 +92,7 @@ iteration commit metric delta guard status description
91
92
  - 最有效的 3 个改进
92
93
  - results log 路径
93
94
  - 重写 `state_path`:将“主线目标”保留为本次优化目标,“正在做什么”更新为已完成,保留最终结论摘要,清空阻塞项,并给出可立即执行的下一步(如继续优化、停止、切换目标)
95
+ - 若 Codex `/goal` 处于 active 且目标已达成,完成 HelloAGENTS 验证和收尾后再标记 goal complete;不得因达到预算或单轮结束而标记 complete
94
96
 
95
97
  ## 安全规则
96
98
  - 使用 `git revert`(保留历史)而非 `git reset --hard`(丢失历史)
@@ -14,7 +14,7 @@ Trigger: ~plan [description]
14
14
  - 在用户确认方案之前,禁止编写任何实现代码、创建任何实现文件、或执行任何实现操作
15
15
  - 需求澄清阶段不读取实现类技能(hello-ui / hello-test / hello-verify 等),需求明确后再按需读取
16
16
  - 方案必须整理为可执行产物,不停留在泛化建议
17
- - 若当前任务来自 `~auto`,则“开始执行”视为已包含在 `~auto` 授权内;方案包写入后默认继续执行,只有命中阻塞判定时才停下
17
+ - 若当前任务来自 `~auto`,则“开始执行”视为已包含在 `~auto` 授权内;方案包写入后默认继续执行,只有命中阻塞判定时才停下。`~design` 是 `~plan` 的兼容别名,只有在 `~auto` 内触发其语义时才默认继续进入 `~build`
18
18
  - 涉及 UI 时,当前方案包中的 UI 决策优先于 `.helloagents/DESIGN.md`;`.helloagents/DESIGN.md`(按当前项目存储模式解析)优先于通用 UI 规则
19
19
 
20
20
  ## 流程
@@ -87,6 +87,7 @@ Trigger: ~plan [description]
87
87
  - 只有在 UI 验收确有收益时,才额外写 `ui.visualValidation.required`、`ui.visualValidation.reason`、`ui.visualValidation.screens` 与 `ui.visualValidation.states`;不要把视觉验收扩成所有 UI 任务的固定步骤
88
88
  - 只有在 `T3`、非 UI 的高风险审查或确需额外跨模型建议时,才写 `advisor.required`、`advisor.reason`、`advisor.focus` 与 `advisor.preferredSources`;不要把 advisor 变成默认常驻流程
89
89
  - 使用 `scripts/plan-contract.mjs write` 写 `contract.json`,不要让后续检查脚本再从 `plan.md` 的自然语言说明里猜验证主路径
90
+ - 在 `tasks.md` 中保留 “Codex /goal 执行入口”,内容必须引用当前方案包路径、AFK/HITL 边界、完成前验证与 HelloAGENTS 收尾;不要把普通 PRD 原文当作 `/goal` 目标
90
91
  - 涉及 UI 的项目:生成或更新 `.helloagents/DESIGN.md`(按当前项目存储模式解析);若原文件不存在,先按模板建立最小设计契约,再写入已确认的稳定设计决策
91
92
  - 重写 `state_path`,其中“主线目标”写本次规划要完成的目标,不保留其他任务的内容
92
93
 
@@ -101,6 +102,7 @@ Trigger: ~plan [description]
101
102
 
102
103
  如果用户已明确表示继续执行,则视为授权成立,可直接继续执行。
103
104
  如果当前任务来自 `~auto`,且方案包已足够支撑实现、也未命中阻塞判定,则默认直接进入 `~build`,不再追加一次“是否开始执行”的询问。
105
+ 如果当前任务是显式 `~plan` 或 `~design`,且尚未获得执行授权,收尾必须使用 `❓等待输入`,下一步写清待确认的执行动作。
104
106
 
105
107
  ## 方案包要求
106
108
 
@@ -109,6 +111,7 @@ Trigger: ~plan [description]
109
111
  - 每个任务标注 `AFK` 或 `HITL`:`AFK` 表示代理可独立完成,`HITL` 表示需要用户决策、外部凭据、人工视觉确认或手动验收
110
112
  - 明确文件路径、预期变更、完成标准、验证方式与依赖关系
111
113
  - 每个任务可独立验证;厚任务必须拆成更薄的可验收切片
114
+ - “Codex /goal 执行入口”只作为长程执行提示,不计入任务列表;入口必须让 Codex 按已拆好的 `tasks.md` 执行,而不是直接消费未拆分需求文档
112
115
 
113
116
  方案包中的 `contract.json` 必须满足:
114
117
  - `verifyMode` 只能是 `test-first` 或 `review-first`
@@ -103,6 +103,7 @@ c. AI 总结该维度的决策结果,进入下一个维度
103
103
  - 创建 `.helloagents/plans/YYYYMMDDHHMM_{feature}/prd/`(按当前项目存储模式解析)
104
104
  - 按 templates/plans/prd/ 的模板格式,仅写入用户未跳过的维度文件
105
105
  - 生成 tasks.md(每个任务默认是端到端垂直切片,标注 AFK / HITL、依赖、具体文件路径、预期变更、完成标准与验证方式;任务独立可验证)
106
+ - 在 `tasks.md` 中保留 “Codex /goal 执行入口”,让 Codex 按已拆分任务、验收边界和 `contract.json` 执行;不要把完整 PRD 原文直接当作 `/goal` 目标
106
107
  - 生成 decisions.md(贯穿全程的决策日志)
107
108
  - 生成 `contract.json`(至少包含 `verifyMode`、`reviewerFocus`、`testerFocus`;涉及 UI 时补 `ui.required`、`ui.designContract`、`ui.sourcePriority`;仅在确需先明确审美方向时再补 `ui.styleAdvisor.required`、`ui.styleAdvisor.reason`、`ui.styleAdvisor.focus`;仅在确需视觉验收时再补 `ui.visualValidation.required`、`ui.visualValidation.reason`、`ui.visualValidation.screens`、`ui.visualValidation.states`;仅在确需独立 advisor 时,再补 `advisor.required`、`advisor.reason`、`advisor.focus`、`advisor.preferredSources`)
108
109
  - 使用 `scripts/plan-contract.mjs write` 写 `contract.json`,不要只把验证路径留在自然语言说明里
@@ -11,6 +11,7 @@ Trigger: ~test [scope]
11
11
  1. 确定测试范围:
12
12
  - 无参数:为最近变更的文件编写测试
13
13
  - 指定文件/模块:为指定范围编写测试
14
+ - Codex active goal 下无参数:从 `tasks.md` 未完成项、`contract.json` 与 `state_path` 推导本轮测试范围
14
15
  2. 按 hello-* 技能查找路径读取 hello-test SKILL.md,按其 TDD 规范和边界用例要求编写测试
15
16
  3. 运行测试确认全部通过
16
- 4. 报告覆盖情况和遗漏
17
+ 4. 同步直接相关的任务状态,报告覆盖情况和遗漏;测试通过只作为 goal 交付证据,不直接标记 goal complete
@@ -13,6 +13,7 @@ Trigger: ~verify [scope]
13
13
  - 即使命令通过,也不能越过当前方案包边界:不完整方案包不能视为可信交付记录,未闭合方案包不能被整体报告为已完成
14
14
  - 当推荐路径已进入 `~verify` / 收尾时,优先把本命令用于审查、验真和交付收尾
15
15
  - 若当前存在活跃方案包,先读取 `requirements.md`、`plan.md`、`tasks.md`、`contract.json`,把它们当作本轮验证契约;不要只看命令结果
16
+ - 若本轮运行在 Codex active goal 下,按 active goal 关联方案包和 `state_path` 复核范围;`/goal` 只负责续跑,不改变验证契约
16
17
  - 若 `contract.json` 声明 `advisor.required=true` 或 `ui.styleAdvisor.required=true`,则本轮还必须补齐当前会话 `artifacts/advisor.json`;advisor / style advisor 都是可选能力,不是默认常驻步骤
17
18
  - 若 `contract.json` 声明 `ui.visualValidation.required=true`,则本轮还必须补齐当前会话 `artifacts/visual.json`;视觉验收优先用截图/浏览器工具,没有工具时才降级为结构化代码级自检
18
19
  1. 先决定验证分流:
@@ -30,6 +31,7 @@ Trigger: ~verify [scope]
30
31
  - 逐个运行所有检测到的命令
31
32
  - 收集每个命令的输出和退出码
32
33
  - 对照当前契约逐项核对:requirements 是否覆盖、tasks 中每项“完成标准”是否满足、`plan.md` 中风险与设计约束是否被验证、`contract.json` 中声明的 `verifyMode` / reviewer / tester 关注边界是否已被覆盖
34
+ - 若 Codex active goal 存在,还要确认 `tasks.md` 的 AFK/HITL 边界:仍有可执行 AFK 项时,不进入 complete;只在目标、任务、验证和收尾都闭合后标记 goal complete
33
35
  - 若 `advisor.required=true` 或 `ui.styleAdvisor.required=true`,在进入收尾前调用 `scripts/advisor-state.mjs write` 写当前会话 `artifacts/advisor.json`;记录触发原因、focus、consultedSources、结论与建议,禁止只在自然语言里留一段 advisor 意见
34
36
  - 若 `ui.visualValidation.required=true`,在进入收尾前调用 `scripts/visual-state.mjs write` 写当前会话 `artifacts/visual.json`;记录 `reason`、`tooling`、`screensChecked`、`statesChecked`、`status`、`summary`、`findings` 与 `recommendations`
35
37
  4. 汇总报告:
@@ -41,4 +43,4 @@ Trigger: ~verify [scope]
41
43
 
42
44
  ## 失败处理
43
45
  - 有失败 → 逐个修复,修复后重新运行对应审查或验证
44
- - 全部通过 → 进入当前已加载 bootstrap 的 CONSOLIDATE 收尾,再按交付边界报告完成
46
+ - 全部通过 → 进入当前已加载 bootstrap 的 CONSOLIDATE 收尾;若 Codex active goal 的目标也已满足,再标记 goal complete,并按交付边界报告完成
@@ -5,7 +5,7 @@ description: 按任务类型适用 — 建立质量驱动工作流,通过技
5
5
 
6
6
  # HelloAGENTS
7
7
 
8
- 主代理触发或读取任意 skill 时,只有在该条消息是本轮最终收尾消息时,才按当前已加载 bootstrap 规则包装 HelloAGENTS 外层输出格式;任何 skill 若在本轮明确要求输出停顿、确认或总结,对应消息也必须同时满足相同条件。
8
+ 主代理触发或读取任意 skill 时,只有在该条消息是本轮最终收尾消息时,才按当前已加载 bootstrap 规则包装 HelloAGENTS 外层输出格式;任何 skill 若在本轮明确要求输出停顿、确认或总结,对应消息也必须同时满足相同条件。最终收尾中的 `🔄 下一步` 写真实动作,不写当前状态;等待用户授权时按等待输入收尾,已获授权且可继续执行时不得收尾。
9
9
  子代理只豁免路由与收尾要求,直接执行任务;安全、质量、验证和失败处理规则仍持续生效,且不得包装 HelloAGENTS 外层输出格式。所有流式内容、进度或状态汇报、中间文本,以及任何仍将继续执行的文本,都不得触发外层格式。
10
10
  只有运行时必须识别本轮“完成 / 等待输入 / 阻塞”时,主代理才写 turn-state;普通问候、普通问答、T0 只读分析和一次性解释不调用。必须调用场景:显式 `~auto` / `~loop`、非只读任务完成验证并进入收尾、需要 delivery gate / Ralph Loop / closeout evidence、需要等待或阻塞且运行时必须识别状态、已进入项目连续流程或方案包闭环。首选 `helloagents-turn-state write --kind complete --role main`;等待或阻塞时写 `kind=waiting` / `kind=blocked`,并同时写 `reasonCategory` 与 `reason`。显式 `~auto` / `~loop` 下,还必须写入 `blocker.target`、`blocker.evidence`、`blocker.requiredAction`。不要查找、读取或拼接 `turn-state.mjs` 源码路径。子代理不得写 turn-state。
11
11
 
@@ -11,5 +11,8 @@
11
11
  - [ ] 任务2(AFK/HITL):端到端行为描述(依赖:任务1;涉及文件:...;预期变更:...;完成标准:...;验证方式:...)
12
12
  - [ ] 任务3(AFK/HITL):端到端行为描述(依赖:...;涉及文件:...;预期变更:...;完成标准:...;验证方式:...)
13
13
 
14
+ ## Codex /goal 执行入口
15
+ [可选。需要长程执行时复制到 Codex:/goal 按 `.helloagents/plans/{feature}/tasks.md` 执行本方案;遵守 `requirements.md`、`plan.md`、`contract.json`。按顺序完成所有 AFK 任务;HITL 仅在缺少外部决策、凭据或人工验收时暂停。不要把完整 PRD 原文直接当作 `/goal` 目标。完成前更新 tasks.md、运行契约验证并完成 HelloAGENTS 收尾。]
16
+
14
17
  ## 进度
15
18
  [执行过程中更新]