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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +147 -45
- package/README_CN.md +148 -46
- package/bootstrap-lite.md +104 -46
- package/bootstrap.md +143 -112
- package/cli.mjs +80 -427
- package/gemini-extension.json +1 -1
- package/hooks/hooks-claude.json +10 -0
- package/hooks/hooks.json +10 -0
- package/package.json +2 -12
- package/scripts/advisor-state.mjs +222 -0
- package/scripts/capability-registry.mjs +59 -0
- package/scripts/cli-codex-backup.mjs +59 -0
- package/scripts/cli-codex-config.mjs +100 -0
- package/scripts/cli-codex.mjs +34 -156
- package/scripts/cli-config.mjs +1 -0
- package/scripts/cli-doctor-render.mjs +28 -0
- package/scripts/cli-doctor.mjs +367 -0
- package/scripts/cli-host-detect.mjs +94 -0
- package/scripts/cli-lifecycle-hosts.mjs +123 -0
- package/scripts/cli-lifecycle.mjs +213 -0
- package/scripts/cli-messages.mjs +76 -52
- package/scripts/closeout-state.mjs +213 -0
- package/scripts/delivery-gate.mjs +256 -0
- package/scripts/guard-rules.mjs +122 -0
- package/scripts/guard.mjs +190 -168
- package/scripts/notify-context.mjs +77 -17
- package/scripts/notify-events.mjs +5 -1
- package/scripts/notify-route.mjs +111 -0
- package/scripts/notify-shared.mjs +0 -2
- package/scripts/notify-source.mjs +113 -0
- package/scripts/notify-ui.mjs +40 -6
- package/scripts/notify.mjs +120 -59
- package/scripts/plan-contract.mjs +210 -0
- package/scripts/project-storage.mjs +235 -0
- package/scripts/ralph-loop.mjs +9 -58
- package/scripts/replay-state.mjs +210 -0
- package/scripts/review-state.mjs +220 -0
- package/scripts/runtime-context.mjs +74 -0
- package/scripts/verify-state.mjs +226 -0
- package/scripts/visual-state.mjs +244 -0
- package/scripts/workflow-core.mjs +165 -0
- package/scripts/workflow-plan-files.mjs +249 -0
- package/scripts/workflow-recommendation.mjs +335 -0
- package/scripts/workflow-state.mjs +113 -0
- package/skills/commands/auto/SKILL.md +37 -71
- package/skills/commands/build/SKILL.md +67 -0
- package/skills/commands/clean/SKILL.md +10 -8
- package/skills/commands/commit/SKILL.md +8 -4
- package/skills/commands/help/SKILL.md +19 -11
- package/skills/commands/idea/SKILL.md +55 -0
- package/skills/commands/init/SKILL.md +6 -3
- package/skills/commands/loop/SKILL.md +6 -5
- package/skills/commands/plan/SKILL.md +116 -0
- package/skills/commands/prd/SKILL.md +20 -15
- package/skills/commands/verify/SKILL.md +32 -9
- package/skills/commands/wiki/SKILL.md +59 -0
- package/skills/hello-review/SKILL.md +9 -0
- package/skills/hello-subagent/SKILL.md +4 -3
- package/skills/hello-ui/SKILL.md +36 -8
- package/skills/hello-verify/SKILL.md +10 -2
- package/skills/helloagents/SKILL.md +24 -13
- package/templates/DESIGN.md +25 -4
- package/templates/STATE.md +3 -0
- package/templates/plans/contract.json +48 -0
- package/templates/plans/plan.md +23 -0
- package/templates/plans/tasks.md +3 -3
- package/skills/commands/design/SKILL.md +0 -108
- package/skills/commands/review/SKILL.md +0 -16
- package/templates/plans/design.md +0 -14
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { join, normalize } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { resolveProjectPlanDir } from './project-storage.mjs'
|
|
5
|
+
|
|
6
|
+
export const PLAN_CONTRACT_FILE_NAME = 'contract.json'
|
|
7
|
+
const VALID_VERIFY_MODES = new Set(['test-first', 'review-first'])
|
|
8
|
+
const VALID_ADVISOR_SOURCES = new Set(['claude', 'codex', 'gemini'])
|
|
9
|
+
|
|
10
|
+
function normalizeStringArray(values) {
|
|
11
|
+
if (!Array.isArray(values)) return []
|
|
12
|
+
return [...new Set(values
|
|
13
|
+
.map((value) => (typeof value === 'string' ? value.trim() : ''))
|
|
14
|
+
.filter(Boolean))]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function normalizeVerifyMode(value) {
|
|
18
|
+
const normalized = typeof value === 'string' ? value.trim().toLowerCase() : ''
|
|
19
|
+
return VALID_VERIFY_MODES.has(normalized) ? normalized : ''
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeUiStyleAdvisorContract(input = {}) {
|
|
23
|
+
return {
|
|
24
|
+
required: Boolean(input.required),
|
|
25
|
+
reason: typeof input.reason === 'string' ? input.reason.trim() : '',
|
|
26
|
+
focus: normalizeStringArray(input.focus),
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizeUiVisualValidationContract(input = {}) {
|
|
31
|
+
return {
|
|
32
|
+
required: Boolean(input.required),
|
|
33
|
+
reason: typeof input.reason === 'string' ? input.reason.trim() : '',
|
|
34
|
+
screens: normalizeStringArray(input.screens),
|
|
35
|
+
states: normalizeStringArray(input.states),
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizeUiContract(input = {}) {
|
|
40
|
+
const styleAdvisor = normalizeUiStyleAdvisorContract(input.styleAdvisor)
|
|
41
|
+
const visualValidation = normalizeUiVisualValidationContract(input.visualValidation)
|
|
42
|
+
const sourcePriority = normalizeStringArray(input.sourcePriority)
|
|
43
|
+
const designContract = Boolean(input.designContract)
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
required: Boolean(input.required)
|
|
47
|
+
|| designContract
|
|
48
|
+
|| sourcePriority.length > 0
|
|
49
|
+
|| styleAdvisor.required
|
|
50
|
+
|| visualValidation.required,
|
|
51
|
+
designContract,
|
|
52
|
+
sourcePriority,
|
|
53
|
+
styleAdvisor,
|
|
54
|
+
visualValidation,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function normalizeAdvisorSources(values) {
|
|
59
|
+
return normalizeStringArray(values).filter((value) => VALID_ADVISOR_SOURCES.has(value))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeAdvisorContract(input = {}) {
|
|
63
|
+
return {
|
|
64
|
+
required: Boolean(input.required),
|
|
65
|
+
reason: typeof input.reason === 'string' ? input.reason.trim() : '',
|
|
66
|
+
focus: normalizeStringArray(input.focus),
|
|
67
|
+
preferredSources: normalizeAdvisorSources(input.preferredSources),
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function resolvePlanDir(cwd, input = {}) {
|
|
72
|
+
const rawPlanDir = typeof input.planDir === 'string' ? input.planDir.trim() : ''
|
|
73
|
+
if (!rawPlanDir) return ''
|
|
74
|
+
return resolveProjectPlanDir(cwd, rawPlanDir)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function getPlanContractPath(planDir) {
|
|
78
|
+
return join(planDir, PLAN_CONTRACT_FILE_NAME)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function readPlanContract(planDir) {
|
|
82
|
+
try {
|
|
83
|
+
return JSON.parse(readFileSync(getPlanContractPath(planDir), 'utf-8'))
|
|
84
|
+
} catch {
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function normalizePlanContract(input = {}) {
|
|
90
|
+
return {
|
|
91
|
+
version: 1,
|
|
92
|
+
source: typeof input.source === 'string' && input.source.trim() ? input.source.trim() : 'manual',
|
|
93
|
+
originCommand: typeof input.originCommand === 'string' ? input.originCommand.trim() : '',
|
|
94
|
+
verifyMode: normalizeVerifyMode(input.verifyMode),
|
|
95
|
+
reviewerFocus: normalizeStringArray(input.reviewerFocus),
|
|
96
|
+
testerFocus: normalizeStringArray(input.testerFocus),
|
|
97
|
+
ui: normalizeUiContract(input.ui),
|
|
98
|
+
advisor: normalizeAdvisorContract(input.advisor),
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function getAdvisorRequirement(contract = null) {
|
|
103
|
+
const normalized = normalizePlanContract(contract || {})
|
|
104
|
+
const advisor = normalized.advisor || normalizeAdvisorContract()
|
|
105
|
+
const styleAdvisor = normalized.ui?.styleAdvisor || normalizeUiStyleAdvisorContract()
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
required: Boolean(advisor.required || styleAdvisor.required),
|
|
109
|
+
genericRequired: advisor.required,
|
|
110
|
+
styleRequired: styleAdvisor.required,
|
|
111
|
+
reason: [advisor.reason, styleAdvisor.reason].filter(Boolean).join(';'),
|
|
112
|
+
focus: normalizeStringArray([...advisor.focus, ...styleAdvisor.focus]),
|
|
113
|
+
preferredSources: advisor.preferredSources,
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function getVisualValidationRequirement(contract = null) {
|
|
118
|
+
const normalized = normalizePlanContract(contract || {})
|
|
119
|
+
return normalized.ui?.visualValidation || normalizeUiVisualValidationContract()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function getPlanContractIssues(contract = null) {
|
|
123
|
+
if (!contract) {
|
|
124
|
+
return ['missing contract.json']
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const normalized = normalizePlanContract(contract)
|
|
128
|
+
const advisorRequirement = getAdvisorRequirement(normalized)
|
|
129
|
+
const visualValidation = getVisualValidationRequirement(normalized)
|
|
130
|
+
const issues = []
|
|
131
|
+
if (!normalizeVerifyMode(normalized.verifyMode)) {
|
|
132
|
+
issues.push('contract.json missing valid verifyMode')
|
|
133
|
+
}
|
|
134
|
+
if (normalizeStringArray(normalized.testerFocus).length === 0) {
|
|
135
|
+
issues.push('contract.json missing testerFocus')
|
|
136
|
+
}
|
|
137
|
+
if (normalizeVerifyMode(normalized.verifyMode) === 'review-first' && normalizeStringArray(normalized.reviewerFocus).length === 0) {
|
|
138
|
+
issues.push('contract.json missing reviewerFocus for review-first flow')
|
|
139
|
+
}
|
|
140
|
+
if (normalized.ui?.required && normalizeStringArray(normalized.ui.sourcePriority).length === 0) {
|
|
141
|
+
issues.push('contract.json missing ui.sourcePriority')
|
|
142
|
+
}
|
|
143
|
+
if (normalized.ui?.styleAdvisor?.required && !String(normalized.ui.styleAdvisor.reason || '').trim()) {
|
|
144
|
+
issues.push('contract.json missing ui.styleAdvisor.reason')
|
|
145
|
+
}
|
|
146
|
+
if (normalized.ui?.styleAdvisor?.required && normalizeStringArray(normalized.ui.styleAdvisor.focus).length === 0) {
|
|
147
|
+
issues.push('contract.json missing ui.styleAdvisor.focus')
|
|
148
|
+
}
|
|
149
|
+
if (visualValidation.required && !String(visualValidation.reason || '').trim()) {
|
|
150
|
+
issues.push('contract.json missing ui.visualValidation.reason')
|
|
151
|
+
}
|
|
152
|
+
if (visualValidation.required && visualValidation.screens.length === 0 && visualValidation.states.length === 0) {
|
|
153
|
+
issues.push('contract.json missing ui.visualValidation.screens or ui.visualValidation.states')
|
|
154
|
+
}
|
|
155
|
+
if (advisorRequirement.genericRequired && !String(normalized.advisor.reason || '').trim()) {
|
|
156
|
+
issues.push('contract.json missing advisor.reason')
|
|
157
|
+
}
|
|
158
|
+
if (advisorRequirement.genericRequired && normalizeStringArray(normalized.advisor.focus).length === 0) {
|
|
159
|
+
issues.push('contract.json missing advisor.focus')
|
|
160
|
+
}
|
|
161
|
+
if (advisorRequirement.genericRequired && normalizeAdvisorSources(normalized.advisor.preferredSources).length === 0) {
|
|
162
|
+
issues.push('contract.json missing advisor.preferredSources')
|
|
163
|
+
}
|
|
164
|
+
return issues
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function writePlanContract(planDir, input = {}) {
|
|
168
|
+
mkdirSync(planDir, { recursive: true })
|
|
169
|
+
const payload = {
|
|
170
|
+
updatedAt: new Date().toISOString(),
|
|
171
|
+
...normalizePlanContract(input),
|
|
172
|
+
}
|
|
173
|
+
writeFileSync(getPlanContractPath(planDir), `${JSON.stringify(payload, null, 2)}\n`, 'utf-8')
|
|
174
|
+
return payload
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function readStdinJson() {
|
|
178
|
+
try {
|
|
179
|
+
return JSON.parse(readFileSync(0, 'utf-8'))
|
|
180
|
+
} catch {
|
|
181
|
+
return {}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function main() {
|
|
186
|
+
const command = process.argv[2] || ''
|
|
187
|
+
if (command !== 'write') return
|
|
188
|
+
|
|
189
|
+
const input = readStdinJson()
|
|
190
|
+
const cwd = input.cwd || process.cwd()
|
|
191
|
+
const planDir = resolvePlanDir(cwd, input)
|
|
192
|
+
if (!planDir) {
|
|
193
|
+
process.stdout.write(JSON.stringify({
|
|
194
|
+
suppressOutput: true,
|
|
195
|
+
error: 'planDir is required',
|
|
196
|
+
}))
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const payload = writePlanContract(planDir, input)
|
|
201
|
+
process.stdout.write(JSON.stringify({
|
|
202
|
+
suppressOutput: true,
|
|
203
|
+
path: getPlanContractPath(planDir),
|
|
204
|
+
payload,
|
|
205
|
+
}))
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
209
|
+
main()
|
|
210
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process'
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
4
|
+
import { homedir } from 'node:os'
|
|
5
|
+
import { basename, dirname, isAbsolute, join, normalize, resolve } from 'node:path'
|
|
6
|
+
|
|
7
|
+
import { DEFAULTS } from './cli-config.mjs'
|
|
8
|
+
|
|
9
|
+
export const PROJECT_DIR_NAME = '.helloagents'
|
|
10
|
+
const PROJECTS_DIR_NAME = 'projects'
|
|
11
|
+
const PROJECT_STORE_MODES = new Set(['local', 'repo-shared'])
|
|
12
|
+
|
|
13
|
+
function safeJson(filePath) {
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(readFileSync(filePath, 'utf-8'))
|
|
16
|
+
} catch {
|
|
17
|
+
return null
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function runGitRevParse(cwd, args = []) {
|
|
22
|
+
try {
|
|
23
|
+
return execFileSync('git', ['rev-parse', ...args], {
|
|
24
|
+
cwd,
|
|
25
|
+
encoding: 'utf-8',
|
|
26
|
+
timeout: 5_000,
|
|
27
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
28
|
+
}).trim()
|
|
29
|
+
} catch {
|
|
30
|
+
return ''
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function resolveGitTopLevel(cwd) {
|
|
35
|
+
const absolute = runGitRevParse(cwd, ['--path-format=absolute', '--show-toplevel'])
|
|
36
|
+
if (absolute) return normalize(absolute)
|
|
37
|
+
|
|
38
|
+
const raw = runGitRevParse(cwd, ['--show-toplevel'])
|
|
39
|
+
return raw ? normalize(resolve(cwd, raw)) : ''
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function resolveGitCommonDir(cwd, repoRoot = '') {
|
|
43
|
+
const absolute = runGitRevParse(cwd, ['--path-format=absolute', '--git-common-dir'])
|
|
44
|
+
if (absolute) return normalize(absolute)
|
|
45
|
+
|
|
46
|
+
const raw = runGitRevParse(cwd, ['--git-common-dir'])
|
|
47
|
+
if (!raw) return ''
|
|
48
|
+
if (isAbsolute(raw)) return normalize(raw)
|
|
49
|
+
return normalize(resolve(repoRoot || cwd, raw))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function sanitizeRepoName(value = '') {
|
|
53
|
+
const normalized = String(value).trim().toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '')
|
|
54
|
+
return normalized || 'project'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function buildProjectKey(cwd) {
|
|
58
|
+
const repoRoot = resolveGitTopLevel(cwd)
|
|
59
|
+
const commonDir = resolveGitCommonDir(cwd, repoRoot)
|
|
60
|
+
const commonDirName = commonDir && basename(commonDir).toLowerCase() === '.git'
|
|
61
|
+
? basename(dirname(commonDir))
|
|
62
|
+
: basename(commonDir || '')
|
|
63
|
+
const repoName = sanitizeRepoName(commonDirName || basename(repoRoot || cwd))
|
|
64
|
+
const keySource = commonDir || repoRoot || normalize(resolve(cwd))
|
|
65
|
+
const hash = createHash('sha1').update(keySource.toLowerCase()).digest('hex').slice(0, 12)
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
repoName,
|
|
69
|
+
hash,
|
|
70
|
+
key: `${repoName}-${hash}`,
|
|
71
|
+
repoRoot,
|
|
72
|
+
commonDir,
|
|
73
|
+
keySource,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function normalizeStoreRelativePath(relativePath = '') {
|
|
78
|
+
return String(relativePath)
|
|
79
|
+
.replace(/[`'"]/g, '')
|
|
80
|
+
.trim()
|
|
81
|
+
.replace(/^\.helloagents[\\/]+/, '')
|
|
82
|
+
.replace(/\\/g, '/')
|
|
83
|
+
.replace(/^\/+/, '')
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function formatPromptPath(pathValue = '') {
|
|
87
|
+
return pathValue ? normalize(pathValue).replace(/\\/g, '/') : ''
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function normalizeProjectStoreMode(value) {
|
|
91
|
+
const normalized = typeof value === 'string' ? value.trim().toLowerCase() : ''
|
|
92
|
+
return PROJECT_STORE_MODES.has(normalized) ? normalized : DEFAULTS.project_store_mode
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function getProjectConfigPath() {
|
|
96
|
+
return join(homedir(), PROJECT_DIR_NAME, 'helloagents.json')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function getProjectStoreMode() {
|
|
100
|
+
const settings = safeJson(getProjectConfigPath()) || {}
|
|
101
|
+
return normalizeProjectStoreMode(settings.project_store_mode)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function getProjectActivationDir(cwd) {
|
|
105
|
+
return join(cwd, PROJECT_DIR_NAME)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function getProjectStatePath(cwd) {
|
|
109
|
+
return join(getProjectActivationDir(cwd), 'STATE.md')
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function isRepoSharedProjectStore(cwd) {
|
|
113
|
+
return getProjectStoreMode(cwd) === 'repo-shared'
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function getProjectStoreDir(cwd) {
|
|
117
|
+
if (!isRepoSharedProjectStore(cwd)) {
|
|
118
|
+
return getProjectActivationDir(cwd)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const projectKey = buildProjectKey(cwd)
|
|
122
|
+
return join(homedir(), PROJECT_DIR_NAME, PROJECTS_DIR_NAME, projectKey.key)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function getProjectStoreSummary(cwd) {
|
|
126
|
+
const activationDir = getProjectActivationDir(cwd)
|
|
127
|
+
const storeDir = getProjectStoreDir(cwd)
|
|
128
|
+
const statePath = getProjectStatePath(cwd)
|
|
129
|
+
const projectKey = buildProjectKey(cwd)
|
|
130
|
+
const projectStoreMode = getProjectStoreMode(cwd)
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
projectStoreMode,
|
|
134
|
+
activationDir,
|
|
135
|
+
storeDir,
|
|
136
|
+
statePath,
|
|
137
|
+
usesSharedStore: projectStoreMode === 'repo-shared',
|
|
138
|
+
projectKey: projectKey.key,
|
|
139
|
+
repoRoot: projectKey.repoRoot,
|
|
140
|
+
commonDir: projectKey.commonDir,
|
|
141
|
+
promptActivationDir: formatPromptPath(activationDir),
|
|
142
|
+
promptStoreDir: formatPromptPath(storeDir),
|
|
143
|
+
promptStatePath: formatPromptPath(statePath),
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function getProjectKnowledgeFilePath(cwd, fileName) {
|
|
148
|
+
return join(getProjectStoreDir(cwd), fileName)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function getProjectDesignContractPath(cwd) {
|
|
152
|
+
return getProjectKnowledgeFilePath(cwd, 'DESIGN.md')
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function getProjectVerifyYamlPath(cwd) {
|
|
156
|
+
return getProjectKnowledgeFilePath(cwd, 'verify.yaml')
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function getProjectPlansDir(cwd) {
|
|
160
|
+
return join(getProjectStoreDir(cwd), 'plans')
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function resolveProjectPlanDir(cwd, rawPlanDir = '') {
|
|
164
|
+
const value = String(rawPlanDir).replace(/[`'"]/g, '').trim().replace(/[\\/]+$/, '')
|
|
165
|
+
if (!value) return ''
|
|
166
|
+
|
|
167
|
+
if (isAbsolute(value)) {
|
|
168
|
+
return normalize(value)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (value.startsWith('.helloagents/')) {
|
|
172
|
+
return normalize(join(getProjectStoreDir(cwd), normalizeStoreRelativePath(value)))
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (value.startsWith('.helloagents\\')) {
|
|
176
|
+
return normalize(join(getProjectStoreDir(cwd), normalizeStoreRelativePath(value)))
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (value.startsWith('plans/')) {
|
|
180
|
+
return normalize(join(getProjectStoreDir(cwd), normalizeStoreRelativePath(value)))
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (value.startsWith('plans\\')) {
|
|
184
|
+
return normalize(join(getProjectStoreDir(cwd), normalizeStoreRelativePath(value)))
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const fromCwd = normalize(join(cwd, value))
|
|
188
|
+
if (existsSync(fromCwd)) {
|
|
189
|
+
return fromCwd
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return normalize(join(getProjectPlansDir(cwd), value))
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function describeProjectStoreFile(cwd, relativePath = '') {
|
|
196
|
+
const normalizedRelativePath = normalizeStoreRelativePath(relativePath)
|
|
197
|
+
const logicalPath = normalizedRelativePath ? `.helloagents/${normalizedRelativePath}` : '.helloagents/'
|
|
198
|
+
if (!isRepoSharedProjectStore(cwd)) {
|
|
199
|
+
return `\`${logicalPath}\``
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const actualPath = formatPromptPath(join(getProjectStoreDir(cwd), normalizedRelativePath))
|
|
203
|
+
return `逻辑路径 \`${logicalPath}\`(实际存储:\`${actualPath}\`)`
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function buildProjectStorageHint(cwd) {
|
|
207
|
+
const summary = getProjectStoreSummary(cwd)
|
|
208
|
+
if (!summary.usesSharedStore) return ''
|
|
209
|
+
return `项目存储:\`project_store_mode=repo-shared\`;本地激活/运行态目录仍是 \`${summary.promptActivationDir}\`,知识库/方案目录改为 \`${summary.promptStoreDir}\`。`
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function buildProjectStorageBlock(cwd) {
|
|
213
|
+
const summary = getProjectStoreSummary(cwd)
|
|
214
|
+
if (!summary.usesSharedStore && !existsSync(summary.activationDir)) {
|
|
215
|
+
return ''
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const details = {
|
|
219
|
+
project_store_mode: summary.projectStoreMode,
|
|
220
|
+
activation_dir: summary.promptActivationDir,
|
|
221
|
+
state_path: summary.promptStatePath,
|
|
222
|
+
knowledge_base_dir: summary.promptStoreDir,
|
|
223
|
+
uses_shared_store: summary.usesSharedStore,
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return [
|
|
227
|
+
'## 当前项目存储',
|
|
228
|
+
'```json',
|
|
229
|
+
JSON.stringify(details, null, 2),
|
|
230
|
+
'```',
|
|
231
|
+
summary.usesSharedStore
|
|
232
|
+
? '说明:`STATE.md` 与 `.ralph-*.json` 继续写本地激活目录;`context.md`、`guidelines.md`、`DESIGN.md`、`verify.yaml`、`modules/`、`plans/`、`archive/` 写知识库/方案目录。'
|
|
233
|
+
: '说明:当前使用项目本地 `.helloagents/` 作为激活目录、知识库目录和方案目录。',
|
|
234
|
+
].join('\n')
|
|
235
|
+
}
|
package/scripts/ralph-loop.mjs
CHANGED
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
* Runs on SubagentStop (Claude Code) and Stop (Codex CLI).
|
|
5
5
|
* Auto-detects lint/test commands and blocks if they fail.
|
|
6
6
|
*/
|
|
7
|
-
import { readFileSync,
|
|
7
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
import { execSync } from 'node:child_process';
|
|
10
10
|
import { homedir } from 'node:os';
|
|
11
|
+
import { clearVerifyEvidence, detectCommands, hasUnsafeVerifyCommand, writeVerifyEvidence } from './verify-state.mjs';
|
|
11
12
|
|
|
12
13
|
const CONFIG_FILE = join(homedir(), '.helloagents', 'helloagents.json');
|
|
13
14
|
const CMD_TIMEOUT = 60_000; // 60s
|
|
@@ -25,60 +26,6 @@ function readSettings() {
|
|
|
25
26
|
return {};
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
// ── Detect verification commands ──────────────────────────────────────
|
|
29
|
-
function loadVerifyYaml(cwd) {
|
|
30
|
-
const f = join(cwd, '.helloagents', 'verify.yaml');
|
|
31
|
-
if (!existsSync(f)) return null;
|
|
32
|
-
try {
|
|
33
|
-
const content = readFileSync(f, 'utf-8');
|
|
34
|
-
const cmds = [];
|
|
35
|
-
let inCmds = false;
|
|
36
|
-
for (const line of content.split('\n')) {
|
|
37
|
-
const s = line.trim();
|
|
38
|
-
if (s.startsWith('commands:')) { inCmds = true; continue; }
|
|
39
|
-
if (inCmds) {
|
|
40
|
-
if (s.startsWith('- ') && !s.startsWith('# ')) {
|
|
41
|
-
const cmd = s.slice(2).trim().replace(/^["']|["']$/g, '');
|
|
42
|
-
if (cmd && !cmd.startsWith('#')) cmds.push(cmd);
|
|
43
|
-
} else if (s && !s.startsWith('#')) break;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return cmds.length ? cmds : null;
|
|
47
|
-
} catch { return null; }
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function detectFromPackageJson(cwd) {
|
|
51
|
-
const f = join(cwd, 'package.json');
|
|
52
|
-
if (!existsSync(f)) return [];
|
|
53
|
-
try {
|
|
54
|
-
const scripts = JSON.parse(readFileSync(f, 'utf-8')).scripts || {};
|
|
55
|
-
return ['lint', 'typecheck', 'type-check', 'test', 'build']
|
|
56
|
-
.filter(k => k in scripts)
|
|
57
|
-
.map(k => `npm run ${k}`);
|
|
58
|
-
} catch { return []; }
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function detectFromPyproject(cwd) {
|
|
62
|
-
const f = join(cwd, 'pyproject.toml');
|
|
63
|
-
if (!existsSync(f)) return [];
|
|
64
|
-
try {
|
|
65
|
-
const content = readFileSync(f, 'utf-8');
|
|
66
|
-
const cmds = [];
|
|
67
|
-
if (content.includes('[tool.ruff')) cmds.push('ruff check .');
|
|
68
|
-
if (content.includes('[tool.mypy')) cmds.push('mypy .');
|
|
69
|
-
if (content.includes('[tool.pytest')) cmds.push('pytest --tb=short -q');
|
|
70
|
-
return cmds;
|
|
71
|
-
} catch { return []; }
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function detectCommands(cwd) {
|
|
75
|
-
const yaml = loadVerifyYaml(cwd);
|
|
76
|
-
if (yaml?.length) return yaml;
|
|
77
|
-
const pkg = detectFromPackageJson(cwd);
|
|
78
|
-
if (pkg.length) return pkg;
|
|
79
|
-
return detectFromPyproject(cwd);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
29
|
// ── Circuit Breaker (consecutive failure tracking) ───────────────────
|
|
83
30
|
const BREAKER_FILE_NAME = '.ralph-breaker.json';
|
|
84
31
|
|
|
@@ -123,12 +70,10 @@ function hasGitChanges(cwd) {
|
|
|
123
70
|
}
|
|
124
71
|
|
|
125
72
|
// ── Run verification ──────────────────────────────────────────────────
|
|
126
|
-
const SHELL_OPERATORS = /[;&|`$(){}\n\r]/;
|
|
127
|
-
|
|
128
73
|
function runVerify(commands, cwd) {
|
|
129
74
|
const failures = [];
|
|
130
75
|
for (const cmd of commands) {
|
|
131
|
-
if (
|
|
76
|
+
if (hasUnsafeVerifyCommand([cmd])) {
|
|
132
77
|
failures.push({ cmd, output: 'Blocked: shell operators not allowed in verify commands' });
|
|
133
78
|
continue;
|
|
134
79
|
}
|
|
@@ -151,6 +96,11 @@ function runVerify(commands, cwd) {
|
|
|
151
96
|
|
|
152
97
|
function handleSuccess(cwd, isSubagent) {
|
|
153
98
|
resetBreaker(cwd);
|
|
99
|
+
writeVerifyEvidence(cwd, {
|
|
100
|
+
commands: detectCommands(cwd),
|
|
101
|
+
fastOnly: isSubagent,
|
|
102
|
+
source: isSubagent ? 'subagent' : 'stop',
|
|
103
|
+
});
|
|
154
104
|
|
|
155
105
|
if (isSubagent) {
|
|
156
106
|
process.stdout.write(JSON.stringify({
|
|
@@ -178,6 +128,7 @@ function handleSuccess(cwd, isSubagent) {
|
|
|
178
128
|
}
|
|
179
129
|
|
|
180
130
|
function handleFailure(failures, cwd) {
|
|
131
|
+
clearVerifyEvidence(cwd);
|
|
181
132
|
const breaker = readBreaker(cwd);
|
|
182
133
|
breaker.consecutive_failures += 1;
|
|
183
134
|
breaker.last_failure = new Date().toISOString();
|