helloagents 3.0.33 → 3.0.35
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.
- package/.claude-plugin/plugin.json +2 -2
- package/.codex-plugin/plugin.json +3 -4
- package/README.md +70 -71
- package/README_CN.md +70 -71
- package/bootstrap-lite.md +9 -11
- package/bootstrap.md +21 -23
- package/gemini-extension.json +1 -1
- package/install.ps1 +21 -3
- package/install.sh +19 -2
- package/package.json +2 -2
- package/scripts/capability-registry.mjs +5 -3
- package/scripts/cli-doctor-codex.mjs +150 -1
- package/scripts/cli-doctor-render.mjs +2 -1
- package/scripts/cli-lifecycle-hosts.mjs +76 -34
- package/scripts/cli-lifecycle.mjs +50 -15
- package/scripts/cli-messages.mjs +5 -5
- package/scripts/delivery-gate-messages.mjs +5 -4
- package/scripts/delivery-gate.mjs +11 -22
- package/scripts/guard.mjs +1 -1
- package/scripts/notify-closeout.mjs +61 -22
- package/scripts/notify-context.mjs +5 -5
- package/scripts/notify-route.mjs +1 -1
- package/scripts/notify.mjs +2 -2
- package/scripts/plan-contract.mjs +10 -14
- package/scripts/project-session-cleanup.mjs +45 -31
- package/scripts/qa-review-state.mjs +313 -0
- package/scripts/ralph-loop.mjs +32 -13
- package/scripts/runtime-scope.mjs +1 -3
- package/scripts/session-capsule.mjs +51 -13
- package/scripts/state-document.mjs +77 -0
- package/scripts/workflow-core.mjs +13 -19
- package/scripts/workflow-plan-files.mjs +1 -1
- package/scripts/workflow-recommendation.mjs +55 -67
- package/scripts/workflow-state.mjs +8 -8
- package/skills/commands/auto/SKILL.md +12 -12
- package/skills/commands/build/SKILL.md +9 -10
- package/skills/commands/commit/SKILL.md +1 -1
- package/skills/commands/help/SKILL.md +11 -13
- package/skills/commands/init/SKILL.md +18 -9
- package/skills/commands/loop/SKILL.md +70 -96
- package/skills/commands/plan/SKILL.md +7 -8
- package/skills/commands/prd/SKILL.md +3 -3
- package/skills/commands/qa/SKILL.md +49 -0
- package/skills/hello-ui/SKILL.md +3 -3
- package/skills/helloagents/SKILL.md +11 -14
- package/skills/qa-review/SKILL.md +92 -0
- package/templates/plans/contract.json +4 -7
- package/templates/plans/plan.md +1 -1
- package/templates/plans/tasks.md +1 -1
- package/templates/verify.yaml +1 -1
- package/scripts/review-state.mjs +0 -193
- package/scripts/verify-state.mjs +0 -175
- package/skills/commands/global/SKILL.md +0 -71
- package/skills/commands/verify/SKILL.md +0 -46
- package/skills/commands/wiki/SKILL.md +0 -57
- package/skills/hello-review/SKILL.md +0 -42
- package/skills/hello-verify/SKILL.md +0 -144
|
@@ -24,10 +24,8 @@ function issueHeading(issue) {
|
|
|
24
24
|
return '任务缺少可交付元数据'
|
|
25
25
|
case 'missing-contract':
|
|
26
26
|
return '方案包缺少可信的结构化契约'
|
|
27
|
-
case 'missing-
|
|
28
|
-
return '
|
|
29
|
-
case 'missing-review-evidence':
|
|
30
|
-
return '当前工作流缺少最新审查证据'
|
|
27
|
+
case 'missing-qa-review-evidence':
|
|
28
|
+
return '当前工作流缺少最新 qa-review 证据'
|
|
31
29
|
case 'missing-advisor-evidence':
|
|
32
30
|
return '当前工作流缺少最新 advisor 证据'
|
|
33
31
|
case 'missing-visual-evidence':
|
|
@@ -62,6 +60,9 @@ export function buildDeliveryBlockReason(issues, recommendation, gateHint) {
|
|
|
62
60
|
if (issues.some((issue) => issue.type === 'missing-visual-evidence')) {
|
|
63
61
|
lines.push('视觉验收动作:先写入当前会话 `artifacts/visual.json`,记录 `tooling`、`screensChecked`、`statesChecked`、`status` 和 `summary`,再报告完成。')
|
|
64
62
|
}
|
|
63
|
+
if (issues.some((issue) => issue.type === 'missing-qa-review-evidence')) {
|
|
64
|
+
lines.push('质量闭环动作:先完成 `~qa` 或写入当前会话 `artifacts/qa-review.json`,记录结论、问题定位、验证命令与最新结果,再报告完成。')
|
|
65
|
+
}
|
|
65
66
|
if (gateHint) {
|
|
66
67
|
lines.push(gateHint)
|
|
67
68
|
}
|
|
@@ -9,10 +9,9 @@ import { fileURLToPath } from 'node:url'
|
|
|
9
9
|
import { getAdvisorEvidenceStatus } from './advisor-state.mjs'
|
|
10
10
|
import { getCloseoutEvidenceStatus } from './closeout-state.mjs'
|
|
11
11
|
import { getAdvisorRequirement, getVisualValidationRequirement } from './plan-contract.mjs'
|
|
12
|
+
import { getQaReviewEvidenceStatus } from './qa-review-state.mjs'
|
|
12
13
|
import { getVisualEvidenceStatus } from './visual-state.mjs'
|
|
13
14
|
import { buildDeliveryGateHint, getDeliveryAction, getWorkflowRecommendation, getWorkflowSnapshot } from './workflow-state.mjs'
|
|
14
|
-
import { getReviewEvidenceStatus } from './review-state.mjs'
|
|
15
|
-
import { getVerifyEvidenceStatus } from './verify-state.mjs'
|
|
16
15
|
import { buildDeliveryBlockReason, buildUnderSpecifiedDetails } from './delivery-gate-messages.mjs'
|
|
17
16
|
|
|
18
17
|
function selectGatePlans(snapshot) {
|
|
@@ -84,20 +83,12 @@ function collectPlanIssues(planEntries) {
|
|
|
84
83
|
return issues
|
|
85
84
|
}
|
|
86
85
|
|
|
87
|
-
function collectEvidenceIssues(issues,
|
|
88
|
-
if (
|
|
86
|
+
function collectEvidenceIssues(issues, qaStatus, advisorStatus, visualStatus, closeoutStatus) {
|
|
87
|
+
if (qaStatus?.required && qaStatus.status !== 'valid') {
|
|
89
88
|
issues.push({
|
|
90
|
-
type: 'missing-
|
|
89
|
+
type: 'missing-qa-review-evidence',
|
|
91
90
|
planName: 'delivery',
|
|
92
|
-
details:
|
|
93
|
-
})
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (reviewStatus?.required && reviewStatus.status !== 'valid') {
|
|
97
|
-
issues.push({
|
|
98
|
-
type: 'missing-review-evidence',
|
|
99
|
-
planName: 'delivery',
|
|
100
|
-
details: reviewStatus.details,
|
|
91
|
+
details: qaStatus.details,
|
|
101
92
|
})
|
|
102
93
|
}
|
|
103
94
|
if (advisorStatus?.required && advisorStatus.status !== 'valid') {
|
|
@@ -124,9 +115,9 @@ function collectEvidenceIssues(issues, verificationStatus, reviewStatus, advisor
|
|
|
124
115
|
}
|
|
125
116
|
}
|
|
126
117
|
|
|
127
|
-
function collectGateIssues(planEntries,
|
|
118
|
+
function collectGateIssues(planEntries, qaStatus, advisorStatus, visualStatus, closeoutStatus) {
|
|
128
119
|
const issues = collectPlanIssues(planEntries)
|
|
129
|
-
collectEvidenceIssues(issues,
|
|
120
|
+
collectEvidenceIssues(issues, qaStatus, advisorStatus, visualStatus, closeoutStatus)
|
|
130
121
|
return issues
|
|
131
122
|
}
|
|
132
123
|
|
|
@@ -143,11 +134,10 @@ export function evaluateDeliveryGate(data = {}) {
|
|
|
143
134
|
const workflowOptions = { payload: data }
|
|
144
135
|
const snapshot = getWorkflowSnapshot(cwd, workflowOptions)
|
|
145
136
|
const recommendation = getWorkflowRecommendation(cwd, workflowOptions)
|
|
146
|
-
const verificationStatus = getVerifyEvidenceStatus(cwd, workflowOptions)
|
|
147
137
|
const deliveryAction = getDeliveryAction(cwd, workflowOptions)
|
|
148
138
|
const gatePlans = selectGatePlans(snapshot)
|
|
149
|
-
const
|
|
150
|
-
required: deliveryAction?.phase === '
|
|
139
|
+
const qaStatus = getQaReviewEvidenceStatus(cwd, {
|
|
140
|
+
required: deliveryAction?.phase === 'qa' || deliveryAction?.phase === 'consolidate',
|
|
151
141
|
...workflowOptions,
|
|
152
142
|
})
|
|
153
143
|
if (gatePlans.length === 0) {
|
|
@@ -169,8 +159,7 @@ export function evaluateDeliveryGate(data = {}) {
|
|
|
169
159
|
})
|
|
170
160
|
const closeoutRequired = (
|
|
171
161
|
gatePlans.every((entry) => entry.missingFiles.length === 0 && entry.templateIssues.length === 0 && entry.taskSummary.total > 0 && entry.taskSummary.open === 0 && entry.taskSummary.underSpecifiedCount === 0)
|
|
172
|
-
&& (!
|
|
173
|
-
&& (!reviewStatus.required || reviewStatus.status === 'valid')
|
|
162
|
+
&& (!qaStatus.required || qaStatus.status === 'valid')
|
|
174
163
|
&& (!advisorStatus.required || advisorStatus.status === 'valid')
|
|
175
164
|
&& (!visualStatus.required || visualStatus.status === 'valid')
|
|
176
165
|
)
|
|
@@ -179,7 +168,7 @@ export function evaluateDeliveryGate(data = {}) {
|
|
|
179
168
|
...workflowOptions,
|
|
180
169
|
})
|
|
181
170
|
|
|
182
|
-
const issues = collectGateIssues(gatePlans,
|
|
171
|
+
const issues = collectGateIssues(gatePlans, qaStatus, advisorStatus, visualStatus, closeoutStatus)
|
|
183
172
|
if (issues.length === 0) {
|
|
184
173
|
return { suppressOutput: true }
|
|
185
174
|
}
|
package/scripts/guard.mjs
CHANGED
|
@@ -76,7 +76,7 @@ function buildHighRiskGate(matches, cwd, payload = {}) {
|
|
|
76
76
|
if (!recommendation) return null
|
|
77
77
|
if (matches.some((match) => match.gate === 'post-verify')) {
|
|
78
78
|
return {
|
|
79
|
-
reason: `[HelloAGENTS Guard] 已阻止 T3 命令:当前工作流尚未进入
|
|
79
|
+
reason: `[HelloAGENTS Guard] 已阻止 T3 命令:当前工作流尚未进入 QA / CONSOLIDATE。\n当前工作流:${recommendation.summary}\n处理路径:${recommendation.nextPath}\n${recommendation.guidance}`,
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
if (matches.some((match) => match.gate === 'plan-first') && recommendation.nextCommand === 'plan') {
|
|
@@ -1,15 +1,56 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto'
|
|
2
2
|
import { closeSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
3
|
-
import { dirname } from 'node:path'
|
|
3
|
+
import { dirname, join } from 'node:path'
|
|
4
|
+
import { homedir } from 'node:os'
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export const CODEX_CLOSEOUT_EVIDENCE_FILE = 'codex-native-stop.json'
|
|
8
|
-
export const CODEX_QUICK_NOTIFY_EVIDENCE_FILE = 'codex-quick-notify.json'
|
|
9
|
-
const CODEX_CLOSEOUT_LOCK_FILE = 'codex-native-stop.lock'
|
|
6
|
+
export const CODEX_NOTIFY_STATE_FILE = 'notify-state.json'
|
|
7
|
+
export const CODEX_NOTIFY_LOCK_FILE = 'notify.lock'
|
|
10
8
|
const WEAK_KEY_TTL_MS = 10_000
|
|
11
9
|
const LOCK_STALE_MS = 120_000
|
|
12
10
|
|
|
11
|
+
function getHomeDir(env = process.env) {
|
|
12
|
+
return env.HOME || env.USERPROFILE || homedir()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getCodexNotifyDir(env = process.env) {
|
|
16
|
+
return join(getHomeDir(env), '.codex', '.helloagents')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getCodexNotifyStatePath(env = process.env) {
|
|
20
|
+
return join(getCodexNotifyDir(env), CODEX_NOTIFY_STATE_FILE)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getCodexNotifyLockPath(env = process.env) {
|
|
24
|
+
return join(getCodexNotifyDir(env), CODEX_NOTIFY_LOCK_FILE)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readNotifyState(env = process.env) {
|
|
28
|
+
try {
|
|
29
|
+
const value = JSON.parse(readFileSync(getCodexNotifyStatePath(env), 'utf-8'))
|
|
30
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
31
|
+
return { version: 1, nativeStop: null, quickNotify: null }
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
version: 1,
|
|
35
|
+
nativeStop: value.nativeStop && typeof value.nativeStop === 'object' ? value.nativeStop : null,
|
|
36
|
+
quickNotify: value.quickNotify && typeof value.quickNotify === 'object' ? value.quickNotify : null,
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
return { version: 1, nativeStop: null, quickNotify: null }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function writeNotifyState(state, env = process.env) {
|
|
44
|
+
const filePath = getCodexNotifyStatePath(env)
|
|
45
|
+
mkdirSync(dirname(filePath), { recursive: true })
|
|
46
|
+
writeFileSync(filePath, `${JSON.stringify({
|
|
47
|
+
version: 1,
|
|
48
|
+
nativeStop: state?.nativeStop || null,
|
|
49
|
+
quickNotify: state?.quickNotify || null,
|
|
50
|
+
}, null, 2)}\n`, 'utf-8')
|
|
51
|
+
return filePath
|
|
52
|
+
}
|
|
53
|
+
|
|
13
54
|
function getTurnId(payload = {}) {
|
|
14
55
|
return String(payload.turnId || payload.turn_id || payload['turn-id'] || '').trim()
|
|
15
56
|
}
|
|
@@ -118,13 +159,10 @@ export function matchesCodexCloseoutEvidence(evidence, snapshot, now = Date.now(
|
|
|
118
159
|
return intersects(snapshot.weakKeys, weakKeys)
|
|
119
160
|
}
|
|
120
161
|
|
|
121
|
-
/**
|
|
122
|
-
* Try to claim the current Codex closeout so Stop and native notify handle one turn only once.
|
|
123
|
-
*/
|
|
124
162
|
export function beginCodexCloseoutClaim(cwd, { payload = {}, turnState = null, source = '' } = {}) {
|
|
125
163
|
const snapshot = buildCodexCloseoutSnapshot({ payload, turnState })
|
|
126
|
-
const lockPath =
|
|
127
|
-
const evidencePath =
|
|
164
|
+
const lockPath = getCodexNotifyLockPath()
|
|
165
|
+
const evidencePath = getCodexNotifyStatePath()
|
|
128
166
|
const now = Date.now()
|
|
129
167
|
const lockPayload = {
|
|
130
168
|
source,
|
|
@@ -164,8 +202,8 @@ export function beginCodexCloseoutClaim(cwd, { payload = {}, turnState = null, s
|
|
|
164
202
|
}
|
|
165
203
|
}
|
|
166
204
|
|
|
167
|
-
const
|
|
168
|
-
if (matchesCodexCloseoutEvidence(
|
|
205
|
+
const notifyState = readNotifyState()
|
|
206
|
+
if (matchesCodexCloseoutEvidence(notifyState.nativeStop, snapshot, now)) {
|
|
169
207
|
releaseLockFile(lockPath)
|
|
170
208
|
return {
|
|
171
209
|
claimed: false,
|
|
@@ -186,15 +224,13 @@ export function beginCodexCloseoutClaim(cwd, { payload = {}, turnState = null, s
|
|
|
186
224
|
}
|
|
187
225
|
}
|
|
188
226
|
|
|
189
|
-
/**
|
|
190
|
-
* Persist the handled closeout fingerprint and release the in-flight lock.
|
|
191
|
-
*/
|
|
192
227
|
export function finalizeCodexCloseoutClaim(claim, meta = {}) {
|
|
193
228
|
if (!claim?.claimed) return
|
|
194
229
|
|
|
195
230
|
try {
|
|
196
231
|
if (meta.handled !== false) {
|
|
197
|
-
|
|
232
|
+
const notifyState = readNotifyState()
|
|
233
|
+
notifyState.nativeStop = {
|
|
198
234
|
version: 2,
|
|
199
235
|
updatedAt: new Date().toISOString(),
|
|
200
236
|
source: meta.source || claim.source || '',
|
|
@@ -205,7 +241,8 @@ export function finalizeCodexCloseoutClaim(claim, meta = {}) {
|
|
|
205
241
|
messageHash: claim.snapshot.messageHash,
|
|
206
242
|
strongKeys: claim.snapshot.strongKeys,
|
|
207
243
|
weakKeys: claim.snapshot.weakKeys,
|
|
208
|
-
}
|
|
244
|
+
}
|
|
245
|
+
writeNotifyState(notifyState)
|
|
209
246
|
}
|
|
210
247
|
} finally {
|
|
211
248
|
releaseLockFile(claim.lockPath)
|
|
@@ -214,7 +251,8 @@ export function finalizeCodexCloseoutClaim(claim, meta = {}) {
|
|
|
214
251
|
|
|
215
252
|
export function writeCodexQuickNotifyEvidence(cwd, { payload = {}, turnState = null, event = '' } = {}) {
|
|
216
253
|
const snapshot = buildCodexCloseoutSnapshot({ payload, turnState })
|
|
217
|
-
|
|
254
|
+
const notifyState = readNotifyState()
|
|
255
|
+
notifyState.quickNotify = {
|
|
218
256
|
version: 1,
|
|
219
257
|
updatedAt: new Date().toISOString(),
|
|
220
258
|
event,
|
|
@@ -223,11 +261,12 @@ export function writeCodexQuickNotifyEvidence(cwd, { payload = {}, turnState = n
|
|
|
223
261
|
messageHash: snapshot.messageHash,
|
|
224
262
|
strongKeys: snapshot.strongKeys,
|
|
225
263
|
weakKeys: snapshot.weakKeys,
|
|
226
|
-
}
|
|
264
|
+
}
|
|
265
|
+
return writeNotifyState(notifyState)
|
|
227
266
|
}
|
|
228
267
|
|
|
229
268
|
export function hasCodexQuickNotifyEvidence(cwd, { payload = {}, turnState = null } = {}) {
|
|
230
269
|
const snapshot = buildCodexCloseoutSnapshot({ payload, turnState })
|
|
231
|
-
const
|
|
232
|
-
return matchesCodexCloseoutEvidence(
|
|
270
|
+
const notifyState = readNotifyState()
|
|
271
|
+
return matchesCodexCloseoutEvidence(notifyState.quickNotify, snapshot)
|
|
233
272
|
}
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
const COMMAND_ALIASES = {
|
|
12
12
|
do: 'build',
|
|
13
13
|
design: 'plan',
|
|
14
|
-
review: '
|
|
14
|
+
review: 'qa',
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
function buildRuntimeRootBlock(pkgRoot) {
|
|
@@ -49,7 +49,7 @@ function buildAliasRouteNote(skillName) {
|
|
|
49
49
|
return '兼容别名映射:本次按 ~plan 规则执行;方案文件使用 `plan.md`,项目级 UI 契约仍使用 `DESIGN.md`。';
|
|
50
50
|
}
|
|
51
51
|
if (skillName === 'review') {
|
|
52
|
-
return '兼容别名映射:本次按 ~
|
|
52
|
+
return '兼容别名映射:本次按 ~qa 规则执行;统一走 qa-review 质量闭环。';
|
|
53
53
|
}
|
|
54
54
|
return '';
|
|
55
55
|
}
|
|
@@ -137,7 +137,7 @@ export function buildInjectContext({ source, bootstrap, settings, pkgRoot, host,
|
|
|
137
137
|
if (capabilityHint) context += `\n\n## 当前按需能力\n${capabilityHint}`;
|
|
138
138
|
if (stateSyncHint) context += `\n\n## 状态文件提醒\n${stateSyncHint}`;
|
|
139
139
|
context += settingsBlock;
|
|
140
|
-
if (source === 'resume' || source === 'compact') {
|
|
140
|
+
if ((source === 'resume' || source === 'compact') && stateSnapshot.exists) {
|
|
141
141
|
context += `\n\n> ⚠️ 会话已恢复/压缩,请先读取 \`state_path\` 指向的 \`${stateSnapshot.statePath.replace(/\\/g, '/')}\`;先看当前用户消息,如果仍是同一任务,再参考状态文件。`;
|
|
142
142
|
}
|
|
143
143
|
return context;
|
|
@@ -165,8 +165,8 @@ export function buildSemanticRouteInstruction(cwd, payload = {}) {
|
|
|
165
165
|
'请根据用户请求的真实意图选路,不依赖关键词表。',
|
|
166
166
|
buildDelegatedTaskHint(),
|
|
167
167
|
'Delivery Tier: T0=探索/比较;T1=低风险小改动或显式验证;T2=多文件功能/新项目/需要结构化产物;T3=高风险或不可逆操作。',
|
|
168
|
-
'路由映射:~idea=只读探索,不创建文件;~build=明确实现;~
|
|
169
|
-
'若判定为 T3,默认先走 ~plan / ~prd
|
|
168
|
+
'路由映射:~idea=只读探索,不创建文件;~build=明确实现;~qa=统一质量审查/验证/修复/收尾;~plan=结构化规划;~prd=重型规格;~auto=自动选择并继续执行后续阶段。',
|
|
169
|
+
'若判定为 T3,默认先走 ~plan / ~prd;纯质量审查、验真或收尾请求才优先 ~qa。',
|
|
170
170
|
`涉及 UI 任务时,设计决策优先级:当前活跃 plan / PRD → ${describeProjectStoreFile(cwd, 'DESIGN.md')} → 已读取的 hello-ui 规则;同时所有 UI 任务都必须满足 UI 质量基线。`,
|
|
171
171
|
projectStorageHint,
|
|
172
172
|
workflowHint ? `项目状态:${workflowHint}` : '',
|
package/scripts/notify-route.mjs
CHANGED
|
@@ -22,7 +22,7 @@ function shouldBypassRoute(prompt) {
|
|
|
22
22
|
|
|
23
23
|
function buildHelpExtraRules(skillName) {
|
|
24
24
|
if (skillName !== 'help') return ''
|
|
25
|
-
return ' 这是 HelloAGENTS 的帮助命令,不是宿主 CLI 的内置帮助。仅显示 HelloAGENTS 的帮助和当前设置;优先使用当前会话上下文中已注入的“当前用户设置”、配置文件原始 JSON 或此前读取结果摘要,上下文不存在或缺少要展示的配置项时才读取一次 ~/.helloagents/helloagents.json
|
|
25
|
+
return ' 这是 HelloAGENTS 的帮助命令,不是宿主 CLI 的内置帮助。仅显示 HelloAGENTS 的帮助和当前设置;优先使用当前会话上下文中已注入的“当前用户设置”、配置文件原始 JSON 或此前读取结果摘要,上下文不存在或缺少要展示的配置项时才读取一次 ~/.helloagents/helloagents.json;自动激活技能说明仅在宿主全局模式或已初始化项目时生效。不要调用宿主 CLI 的帮助工具(如 cli_help 或 /help),不要使用子代理,不要读取项目文件;若受工作区限制无法读取配置,必须明确说明并按已知默认值或已注入设置展示。'
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
function routeExplicitCommand({
|
package/scripts/notify.mjs
CHANGED
|
@@ -41,7 +41,7 @@ const EVENT_NAME = {
|
|
|
41
41
|
UserPromptSubmit: IS_GEMINI ? 'BeforeAgent' : 'UserPromptSubmit',
|
|
42
42
|
PreCompact: IS_GEMINI ? 'BeforeAgent' : 'PreCompact',
|
|
43
43
|
};
|
|
44
|
-
const RALPH_LOOP_ROUTE_COMMANDS = new Set(['
|
|
44
|
+
const RALPH_LOOP_ROUTE_COMMANDS = new Set(['qa', 'loop']);
|
|
45
45
|
const CODEX_HOOKS_FILE = join(homedir(), '.codex', 'hooks.json');
|
|
46
46
|
const GATE_MODULE_LOADERS = {
|
|
47
47
|
'turn-stop-gate': () => import('./turn-stop-gate.mjs'),
|
|
@@ -459,7 +459,7 @@ function cmdInject() {
|
|
|
459
459
|
const cwd = payload.cwd || process.cwd();
|
|
460
460
|
const settings = getSettings();
|
|
461
461
|
const bootstrapFile = resolveBootstrapFile(cwd, settings, HOST);
|
|
462
|
-
const shouldEnsureProjectLocal =
|
|
462
|
+
const shouldEnsureProjectLocal = isProjectRuntimeActive(cwd);
|
|
463
463
|
|
|
464
464
|
startReplaySession(cwd, {
|
|
465
465
|
host: HOST,
|
|
@@ -4,7 +4,7 @@ import { fileURLToPath } from 'node:url'
|
|
|
4
4
|
import { resolveProjectPlanDir } from './project-storage.mjs'
|
|
5
5
|
|
|
6
6
|
export const PLAN_CONTRACT_FILE_NAME = 'contract.json'
|
|
7
|
-
const
|
|
7
|
+
const VALID_QA_MODES = new Set(['standard', 'deep'])
|
|
8
8
|
const VALID_ADVISOR_SOURCES = new Set(['claude', 'codex', 'gemini'])
|
|
9
9
|
|
|
10
10
|
function normalizeStringArray(values) {
|
|
@@ -14,9 +14,9 @@ function normalizeStringArray(values) {
|
|
|
14
14
|
.filter(Boolean))]
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
function
|
|
17
|
+
function normalizeQaMode(value) {
|
|
18
18
|
const normalized = typeof value === 'string' ? value.trim().toLowerCase() : ''
|
|
19
|
-
return
|
|
19
|
+
return VALID_QA_MODES.has(normalized) ? normalized : ''
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
function normalizeUiStyleAdvisorContract(input = {}) {
|
|
@@ -88,12 +88,11 @@ export function readPlanContract(planDir) {
|
|
|
88
88
|
|
|
89
89
|
export function normalizePlanContract(input = {}) {
|
|
90
90
|
return {
|
|
91
|
-
version:
|
|
91
|
+
version: 2,
|
|
92
92
|
source: typeof input.source === 'string' && input.source.trim() ? input.source.trim() : 'manual',
|
|
93
93
|
originCommand: typeof input.originCommand === 'string' ? input.originCommand.trim() : '',
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
testerFocus: normalizeStringArray(input.testerFocus),
|
|
94
|
+
qaMode: normalizeQaMode(input.qaMode),
|
|
95
|
+
qaFocus: normalizeStringArray(input.qaFocus),
|
|
97
96
|
ui: normalizeUiContract(input.ui),
|
|
98
97
|
advisor: normalizeAdvisorContract(input.advisor),
|
|
99
98
|
}
|
|
@@ -128,14 +127,11 @@ export function getPlanContractIssues(contract = null) {
|
|
|
128
127
|
const advisorRequirement = getAdvisorRequirement(normalized)
|
|
129
128
|
const visualValidation = getVisualValidationRequirement(normalized)
|
|
130
129
|
const issues = []
|
|
131
|
-
if (!
|
|
132
|
-
issues.push('contract.json missing valid
|
|
130
|
+
if (!normalizeQaMode(normalized.qaMode)) {
|
|
131
|
+
issues.push('contract.json missing valid qaMode')
|
|
133
132
|
}
|
|
134
|
-
if (normalizeStringArray(normalized.
|
|
135
|
-
issues.push('contract.json missing
|
|
136
|
-
}
|
|
137
|
-
if (normalizeVerifyMode(normalized.verifyMode) === 'review-first' && normalizeStringArray(normalized.reviewerFocus).length === 0) {
|
|
138
|
-
issues.push('contract.json missing reviewerFocus for review-first flow')
|
|
133
|
+
if (normalizeStringArray(normalized.qaFocus).length === 0) {
|
|
134
|
+
issues.push('contract.json missing qaFocus')
|
|
139
135
|
}
|
|
140
136
|
if (normalized.ui?.required && normalizeStringArray(normalized.ui.sourcePriority).length === 0) {
|
|
141
137
|
issues.push('contract.json missing ui.sourcePriority')
|
|
@@ -1,19 +1,18 @@
|
|
|
1
|
-
import { existsSync, readdirSync, rmSync } from 'node:fs'
|
|
1
|
+
import { existsSync, readdirSync, rmSync, statSync } from 'node:fs'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
ACTIVE_SESSION_FILE_NAME,
|
|
6
|
-
CAPSULE_FILE_NAME,
|
|
7
|
-
EVENTS_FILE_NAME,
|
|
8
|
-
PROJECT_ARTIFACTS_DIR_NAME,
|
|
9
6
|
PROJECT_SESSIONS_DIR_NAME,
|
|
10
7
|
getProjectActivationDir,
|
|
11
8
|
getProjectRoot,
|
|
12
9
|
readJsonFile,
|
|
13
10
|
writeJsonFileAtomic,
|
|
14
11
|
} from './runtime-scope.mjs'
|
|
12
|
+
import { LONG_RUNNING_TTL_MS } from './runtime-ttl.mjs'
|
|
15
13
|
|
|
16
14
|
export const PROJECT_SESSION_CLEANUP_COOLDOWN_MS = 10 * 60 * 1000
|
|
15
|
+
export const PROJECT_SESSION_MAX_AGE_MS = LONG_RUNNING_TTL_MS
|
|
17
16
|
|
|
18
17
|
function removePath(filePath, result, bucket) {
|
|
19
18
|
try {
|
|
@@ -33,29 +32,6 @@ function isDirectoryEmptyRecursive(dirPath) {
|
|
|
33
32
|
})
|
|
34
33
|
}
|
|
35
34
|
|
|
36
|
-
function listFilesRecursive(dirPath) {
|
|
37
|
-
const entries = readdirSync(dirPath, { withFileTypes: true })
|
|
38
|
-
return entries.flatMap((entry) => {
|
|
39
|
-
const entryPath = join(dirPath, entry.name)
|
|
40
|
-
if (entry.isDirectory()) {
|
|
41
|
-
return listFilesRecursive(entryPath).map((child) => `${entry.name}/${child}`)
|
|
42
|
-
}
|
|
43
|
-
return entry.isFile() ? [entry.name] : []
|
|
44
|
-
})
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function isRouteOnlySessionDir(sessionDir) {
|
|
48
|
-
if (existsSync(join(sessionDir, 'STATE.md'))) return false
|
|
49
|
-
const files = listFilesRecursive(sessionDir).map((file) => file.replace(/\\/g, '/'))
|
|
50
|
-
if (files.length === 0) return false
|
|
51
|
-
if (!files.includes(`${PROJECT_ARTIFACTS_DIR_NAME}/codex-native-stop.json`)) return false
|
|
52
|
-
return files.every((file) => [
|
|
53
|
-
CAPSULE_FILE_NAME,
|
|
54
|
-
EVENTS_FILE_NAME,
|
|
55
|
-
`${PROJECT_ARTIFACTS_DIR_NAME}/codex-native-stop.json`,
|
|
56
|
-
].includes(file))
|
|
57
|
-
}
|
|
58
|
-
|
|
59
35
|
function shouldKeepSession(active, workspace, session) {
|
|
60
36
|
const activeWorkspace = active.workspace || active.branch || ''
|
|
61
37
|
return activeWorkspace === workspace && active.session === session
|
|
@@ -75,7 +51,35 @@ function writeCleanupCheckpoint(activePath, active, now) {
|
|
|
75
51
|
})
|
|
76
52
|
}
|
|
77
53
|
|
|
78
|
-
|
|
54
|
+
function hasStateSnapshot(sessionDir) {
|
|
55
|
+
return existsSync(join(sessionDir, 'STATE.md'))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readSessionStateMtimeMs(sessionDir) {
|
|
59
|
+
try {
|
|
60
|
+
return statSync(join(sessionDir, 'STATE.md')).mtimeMs
|
|
61
|
+
} catch {
|
|
62
|
+
return 0
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isStaleStateSession(sessionDir, now, maxAgeMs) {
|
|
67
|
+
const mtimeMs = readSessionStateMtimeMs(sessionDir)
|
|
68
|
+
return !Number.isFinite(mtimeMs) || mtimeMs <= 0 || (now - mtimeMs > maxAgeMs)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isTransientSessionTemp(entryName = '') {
|
|
72
|
+
return /^\.[0-9]+-[0-9a-f-]+\.tmp$/i.test(entryName)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function cleanupTransientSessionTemps(sessionsDir, result) {
|
|
76
|
+
for (const entry of readdirSync(sessionsDir, { withFileTypes: true })) {
|
|
77
|
+
if (!entry.isFile() || !isTransientSessionTemp(entry.name)) continue
|
|
78
|
+
removePath(join(sessionsDir, entry.name), result, 'removedTempFiles')
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function cleanupProjectSessions(cwd, { now = Date.now(), minIntervalMs = 0, maxAgeMs = PROJECT_SESSION_MAX_AGE_MS } = {}) {
|
|
79
83
|
const projectRoot = getProjectRoot(cwd)
|
|
80
84
|
const activationDir = getProjectActivationDir(projectRoot)
|
|
81
85
|
const sessionsDir = join(activationDir, PROJECT_SESSIONS_DIR_NAME)
|
|
@@ -84,7 +88,9 @@ export function cleanupProjectSessions(cwd, { now = Date.now(), minIntervalMs =
|
|
|
84
88
|
const result = {
|
|
85
89
|
sessionsDir,
|
|
86
90
|
removedEmptyDirs: [],
|
|
87
|
-
|
|
91
|
+
removedInactiveDirs: [],
|
|
92
|
+
removedNoStateDirs: [],
|
|
93
|
+
removedTempFiles: [],
|
|
88
94
|
errors: [],
|
|
89
95
|
skipped: false,
|
|
90
96
|
}
|
|
@@ -98,6 +104,12 @@ export function cleanupProjectSessions(cwd, { now = Date.now(), minIntervalMs =
|
|
|
98
104
|
}
|
|
99
105
|
}
|
|
100
106
|
|
|
107
|
+
try {
|
|
108
|
+
cleanupTransientSessionTemps(sessionsDir, result)
|
|
109
|
+
} catch (error) {
|
|
110
|
+
result.errors.push(`${sessionsDir}: ${error.message}`)
|
|
111
|
+
}
|
|
112
|
+
|
|
101
113
|
for (const workspaceEntry of readdirSync(sessionsDir, { withFileTypes: true })) {
|
|
102
114
|
if (!workspaceEntry.isDirectory()) continue
|
|
103
115
|
const workspaceDir = join(sessionsDir, workspaceEntry.name)
|
|
@@ -110,8 +122,10 @@ export function cleanupProjectSessions(cwd, { now = Date.now(), minIntervalMs =
|
|
|
110
122
|
try {
|
|
111
123
|
if (isDirectoryEmptyRecursive(sessionDir)) {
|
|
112
124
|
removePath(sessionDir, result, 'removedEmptyDirs')
|
|
113
|
-
} else if (
|
|
114
|
-
removePath(sessionDir, result, '
|
|
125
|
+
} else if (!hasStateSnapshot(sessionDir)) {
|
|
126
|
+
removePath(sessionDir, result, 'removedNoStateDirs')
|
|
127
|
+
} else if (isStaleStateSession(sessionDir, now, maxAgeMs)) {
|
|
128
|
+
removePath(sessionDir, result, 'removedInactiveDirs')
|
|
115
129
|
}
|
|
116
130
|
} catch (error) {
|
|
117
131
|
result.errors.push(`${sessionDir}: ${error.message}`)
|