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,222 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
import { appendReplayEvent } from './replay-state.mjs'
|
|
6
|
+
import { captureWorkspaceFingerprint } from './verify-state.mjs'
|
|
7
|
+
|
|
8
|
+
export const ADVISOR_EVIDENCE_FILE_NAME = '.ralph-advisor.json'
|
|
9
|
+
const ADVISOR_EVIDENCE_MAX_AGE_MS = 30 * 60 * 1000
|
|
10
|
+
const VALID_ADVISOR_OUTCOMES = new Set(['clean', 'findings'])
|
|
11
|
+
const VALID_SOURCES = new Set(['claude', 'codex', 'gemini'])
|
|
12
|
+
|
|
13
|
+
function normalizeStringArray(values) {
|
|
14
|
+
if (!Array.isArray(values)) return []
|
|
15
|
+
return [...new Set(values.map((value) => (typeof value === 'string' ? value.trim() : '')).filter(Boolean))]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function normalizeSources(values) {
|
|
19
|
+
return normalizeStringArray(values).filter((value) => VALID_SOURCES.has(value))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeOutcome(value) {
|
|
23
|
+
const normalized = typeof value === 'string' ? value.trim().toLowerCase() : ''
|
|
24
|
+
return VALID_ADVISOR_OUTCOMES.has(normalized) ? normalized : ''
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getAdvisorEvidencePath(cwd) {
|
|
28
|
+
return join(cwd, '.helloagents', ADVISOR_EVIDENCE_FILE_NAME)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function readAdvisorEvidence(cwd) {
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(readFileSync(getAdvisorEvidencePath(cwd), 'utf-8'))
|
|
34
|
+
} catch {
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function clearAdvisorEvidence(cwd) {
|
|
40
|
+
rmSync(getAdvisorEvidencePath(cwd), { force: true })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function normalizeAdvisorEvidence(input = {}) {
|
|
44
|
+
return {
|
|
45
|
+
source: typeof input.source === 'string' && input.source.trim() ? input.source.trim() : 'manual',
|
|
46
|
+
originCommand: typeof input.originCommand === 'string' ? input.originCommand.trim() : '',
|
|
47
|
+
reason: typeof input.reason === 'string' ? input.reason.trim() : '',
|
|
48
|
+
focus: normalizeStringArray(input.focus),
|
|
49
|
+
preferredSources: normalizeSources(input.preferredSources),
|
|
50
|
+
consultedSources: normalizeSources(input.consultedSources),
|
|
51
|
+
outcome: normalizeOutcome(input.outcome),
|
|
52
|
+
summary: typeof input.summary === 'string' ? input.summary.trim() : '',
|
|
53
|
+
findings: normalizeStringArray(input.findings),
|
|
54
|
+
recommendations: normalizeStringArray(input.recommendations),
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function writeAdvisorEvidence(cwd, input = {}) {
|
|
59
|
+
mkdirSync(join(cwd, '.helloagents'), { recursive: true })
|
|
60
|
+
const normalized = normalizeAdvisorEvidence(input)
|
|
61
|
+
const payload = {
|
|
62
|
+
updatedAt: new Date().toISOString(),
|
|
63
|
+
...normalized,
|
|
64
|
+
fingerprint: captureWorkspaceFingerprint(cwd),
|
|
65
|
+
}
|
|
66
|
+
writeFileSync(getAdvisorEvidencePath(cwd), `${JSON.stringify(payload, null, 2)}\n`, 'utf-8')
|
|
67
|
+
appendReplayEvent(cwd, {
|
|
68
|
+
event: 'advisor_evidence_written',
|
|
69
|
+
source: normalized.source,
|
|
70
|
+
skillName: normalized.originCommand,
|
|
71
|
+
details: {
|
|
72
|
+
reason: normalized.reason,
|
|
73
|
+
focus: normalized.focus,
|
|
74
|
+
preferredSources: normalized.preferredSources,
|
|
75
|
+
consultedSources: normalized.consultedSources,
|
|
76
|
+
outcome: normalized.outcome,
|
|
77
|
+
},
|
|
78
|
+
artifacts: ['.helloagents/.ralph-advisor.json'],
|
|
79
|
+
})
|
|
80
|
+
return payload
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function readRequiredAdvisorEvidence(cwd, required) {
|
|
84
|
+
if (!required) {
|
|
85
|
+
return {
|
|
86
|
+
required: false,
|
|
87
|
+
status: 'not-applicable',
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const evidence = readAdvisorEvidence(cwd)
|
|
92
|
+
if (evidence) return { evidence }
|
|
93
|
+
return {
|
|
94
|
+
error: {
|
|
95
|
+
required: true,
|
|
96
|
+
status: 'missing',
|
|
97
|
+
details: ['missing advisor evidence required by the active contract'],
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function validateAdvisorTimestamp(evidence, now) {
|
|
103
|
+
const updatedAt = Date.parse(evidence.updatedAt || '')
|
|
104
|
+
if (!Number.isFinite(updatedAt)) {
|
|
105
|
+
return {
|
|
106
|
+
required: true,
|
|
107
|
+
status: 'invalid',
|
|
108
|
+
evidence,
|
|
109
|
+
details: ['advisor evidence timestamp is invalid'],
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (now - updatedAt > ADVISOR_EVIDENCE_MAX_AGE_MS) {
|
|
113
|
+
return {
|
|
114
|
+
required: true,
|
|
115
|
+
status: 'stale-time',
|
|
116
|
+
evidence,
|
|
117
|
+
details: ['advisor evidence is older than 30 minutes'],
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return null
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function validateAdvisorFingerprint(cwd, evidence) {
|
|
124
|
+
const currentFingerprint = captureWorkspaceFingerprint(cwd)
|
|
125
|
+
if (
|
|
126
|
+
currentFingerprint.available
|
|
127
|
+
&& evidence.fingerprint?.available
|
|
128
|
+
&& currentFingerprint.combined !== evidence.fingerprint.combined
|
|
129
|
+
) {
|
|
130
|
+
return {
|
|
131
|
+
required: true,
|
|
132
|
+
status: 'stale-diff',
|
|
133
|
+
evidence,
|
|
134
|
+
details: ['workspace diff changed after the last advisor evidence'],
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return null
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function validateAdvisorContent(evidence, focus = []) {
|
|
141
|
+
if (!normalizeOutcome(evidence.outcome) || !String(evidence.summary || '').trim() || !String(evidence.reason || '').trim()) {
|
|
142
|
+
return {
|
|
143
|
+
required: true,
|
|
144
|
+
status: 'invalid',
|
|
145
|
+
evidence,
|
|
146
|
+
details: ['advisor evidence must record explicit outcome, reason, and summary'],
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (normalizeSources(evidence.consultedSources).length === 0) {
|
|
150
|
+
return {
|
|
151
|
+
required: true,
|
|
152
|
+
status: 'invalid',
|
|
153
|
+
evidence,
|
|
154
|
+
details: ['advisor evidence must record at least one consulted source'],
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (normalizeStringArray(focus).length > 0 && normalizeStringArray(evidence.focus).length === 0) {
|
|
158
|
+
return {
|
|
159
|
+
required: true,
|
|
160
|
+
status: 'invalid',
|
|
161
|
+
evidence,
|
|
162
|
+
details: ['advisor evidence must retain the requested advisor focus'],
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (normalizeOutcome(evidence.outcome) !== 'clean') {
|
|
166
|
+
return {
|
|
167
|
+
required: true,
|
|
168
|
+
status: 'blocked',
|
|
169
|
+
evidence,
|
|
170
|
+
details: ['latest advisor evidence still records blocking findings'],
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return null
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function getAdvisorEvidenceStatus(cwd, { required = false, focus = [], now = Date.now() } = {}) {
|
|
177
|
+
const requiredEvidence = readRequiredAdvisorEvidence(cwd, required)
|
|
178
|
+
if ('status' in requiredEvidence) return requiredEvidence
|
|
179
|
+
if (requiredEvidence.error) return requiredEvidence.error
|
|
180
|
+
|
|
181
|
+
const { evidence } = requiredEvidence
|
|
182
|
+
const timestampError = validateAdvisorTimestamp(evidence, now)
|
|
183
|
+
if (timestampError) return timestampError
|
|
184
|
+
|
|
185
|
+
const fingerprintError = validateAdvisorFingerprint(cwd, evidence)
|
|
186
|
+
if (fingerprintError) return fingerprintError
|
|
187
|
+
|
|
188
|
+
const contentError = validateAdvisorContent(evidence, focus)
|
|
189
|
+
if (contentError) return contentError
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
required: true,
|
|
193
|
+
status: 'valid',
|
|
194
|
+
evidence,
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function readStdinJson() {
|
|
199
|
+
try {
|
|
200
|
+
return JSON.parse(readFileSync(0, 'utf-8'))
|
|
201
|
+
} catch {
|
|
202
|
+
return {}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function main() {
|
|
207
|
+
const command = process.argv[2] || ''
|
|
208
|
+
if (command !== 'write') return
|
|
209
|
+
|
|
210
|
+
const input = readStdinJson()
|
|
211
|
+
const cwd = input.cwd || process.cwd()
|
|
212
|
+
const payload = writeAdvisorEvidence(cwd, input)
|
|
213
|
+
process.stdout.write(JSON.stringify({
|
|
214
|
+
suppressOutput: true,
|
|
215
|
+
path: getAdvisorEvidencePath(cwd),
|
|
216
|
+
payload,
|
|
217
|
+
}))
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
221
|
+
main()
|
|
222
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
|
|
3
|
+
import { getAdvisorRequirement, getVisualValidationRequirement } from './plan-contract.mjs'
|
|
4
|
+
import { describeProjectStoreFile, getProjectDesignContractPath } from './project-storage.mjs'
|
|
5
|
+
import { getWorkflowRecommendation, getWorkflowSnapshot } from './workflow-state.mjs'
|
|
6
|
+
|
|
7
|
+
function getPrimaryPlan(snapshot) {
|
|
8
|
+
return snapshot.activePlans[0] || snapshot.plans[0] || null
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function selectCapabilities({ cwd, skillName = '' }) {
|
|
12
|
+
const snapshot = getWorkflowSnapshot(cwd)
|
|
13
|
+
const recommendation = getWorkflowRecommendation(cwd)
|
|
14
|
+
const plan = getPrimaryPlan(snapshot)
|
|
15
|
+
const advisorRequirement = getAdvisorRequirement(plan?.contract)
|
|
16
|
+
const visualRequirement = getVisualValidationRequirement(plan?.contract)
|
|
17
|
+
const capabilities = []
|
|
18
|
+
|
|
19
|
+
if (skillName === 'plan' || skillName === 'prd' || recommendation?.nextCommand === 'plan') {
|
|
20
|
+
capabilities.push({
|
|
21
|
+
id: 'plan-contract',
|
|
22
|
+
description: '结构化 contract:仅在规划/PRD 场景使用 `scripts/plan-contract.mjs write` 写 `contract.json`,不要只把验证路径留在 prose。',
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
if (advisorRequirement.required) {
|
|
26
|
+
capabilities.push({
|
|
27
|
+
id: 'advisor-artifact',
|
|
28
|
+
description: advisorRequirement.styleRequired
|
|
29
|
+
? '风格 advisor:当前 UI contract 要求进入收尾前复查设计方向,并复用 `.helloagents/.ralph-advisor.json` 记录 reason、focus、consultedSources 与结论。'
|
|
30
|
+
: '独立 advisor:当前 contract 要求进入收尾前写 `.helloagents/.ralph-advisor.json`,记录 advisor reason、focus、consultedSources 与结论。',
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
if (plan?.contract?.verifyMode === 'review-first') {
|
|
34
|
+
capabilities.push({
|
|
35
|
+
id: 'review-evaluator',
|
|
36
|
+
description: '审查优先:当前验证主路径是 review-first,先做 hello-review,再做 hello-verify。',
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
if (plan?.contract?.ui?.required || existsSync(getProjectDesignContractPath(cwd))) {
|
|
40
|
+
capabilities.push({
|
|
41
|
+
id: 'design-contract',
|
|
42
|
+
description: `UI 契约:仅在 UI 场景按需读取当前 plan.md / prd/03-ui-design.md、${describeProjectStoreFile(cwd, 'DESIGN.md')} 与 hello-ui,不全局常驻。`,
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
if (visualRequirement.required) {
|
|
46
|
+
capabilities.push({
|
|
47
|
+
id: 'visual-evaluator',
|
|
48
|
+
description: '视觉验收:当前 UI contract 要求进入收尾前写 `.helloagents/.ralph-visual.json`,记录 tooling、screensChecked、statesChecked、status 与 summary。',
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return capabilities
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function buildCapabilityHint({ cwd, skillName = '' }) {
|
|
56
|
+
const capabilities = selectCapabilities({ cwd, skillName })
|
|
57
|
+
if (capabilities.length === 0) return ''
|
|
58
|
+
return `按需能力:${capabilities.map((entry) => `${entry.id}=${entry.description}`).join(' ')}`
|
|
59
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, readdirSync } from 'node:fs'
|
|
2
|
+
import { dirname, join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { removeIfExists, safeRead } from './cli-utils.mjs'
|
|
5
|
+
|
|
6
|
+
const CODEX_BACKUP_TIMESTAMP_RE = /^\d{8}-\d{6}$/
|
|
7
|
+
|
|
8
|
+
function formatBackupTimestamp(date = new Date()) {
|
|
9
|
+
const pad = (value, size = 2) => String(value).padStart(size, '0')
|
|
10
|
+
return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createTimestampedBackupPath(filePath, backupBaseName) {
|
|
14
|
+
return join(dirname(filePath), `${backupBaseName}_${formatBackupTimestamp()}.bak`)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function listTimestampedBackups(directory, backupBaseName) {
|
|
18
|
+
if (!existsSync(directory)) return []
|
|
19
|
+
return readdirSync(directory)
|
|
20
|
+
.filter((name) => name.startsWith(`${backupBaseName}_`) && name.endsWith('.bak'))
|
|
21
|
+
.filter((name) => CODEX_BACKUP_TIMESTAMP_RE.test(name.slice(backupBaseName.length + 1, -4)))
|
|
22
|
+
.sort()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getLatestTimestampedBackupPath(filePath, backupBaseName) {
|
|
26
|
+
const backups = listTimestampedBackups(dirname(filePath), backupBaseName)
|
|
27
|
+
const latest = backups.at(-1)
|
|
28
|
+
return latest ? join(dirname(filePath), latest) : ''
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readLatestTimestampedBackup(filePath, backupBaseName) {
|
|
32
|
+
const backupPath = getLatestTimestampedBackupPath(filePath, backupBaseName)
|
|
33
|
+
return backupPath ? safeRead(backupPath) || '' : ''
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function removeLatestTimestampedBackup(filePath, backupBaseName) {
|
|
37
|
+
const backupPath = getLatestTimestampedBackupPath(filePath, backupBaseName)
|
|
38
|
+
if (backupPath) removeIfExists(backupPath)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function ensureTimestampedBackup(filePath, backupBaseName) {
|
|
42
|
+
if (!existsSync(filePath)) return ''
|
|
43
|
+
const existingBackup = getLatestTimestampedBackupPath(filePath, backupBaseName)
|
|
44
|
+
if (existingBackup) return existingBackup
|
|
45
|
+
const backupPath = createTimestampedBackupPath(filePath, backupBaseName)
|
|
46
|
+
copyFileSync(filePath, backupPath)
|
|
47
|
+
return backupPath
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function readCodexBackup(filePath, backupBaseName) {
|
|
51
|
+
const latest = readLatestTimestampedBackup(filePath, backupBaseName)
|
|
52
|
+
if (latest) return latest
|
|
53
|
+
return safeRead(`${filePath}.bak`) || ''
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function removeCodexBackup(filePath, backupBaseName) {
|
|
57
|
+
removeLatestTimestampedBackup(filePath, backupBaseName)
|
|
58
|
+
removeIfExists(`${filePath}.bak`)
|
|
59
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createTimestampedBackupPath,
|
|
3
|
+
readCodexBackup,
|
|
4
|
+
removeCodexBackup,
|
|
5
|
+
} from './cli-codex-backup.mjs'
|
|
6
|
+
import { safeWrite } from './cli-utils.mjs'
|
|
7
|
+
import {
|
|
8
|
+
ensureTopLevelTomlBlock,
|
|
9
|
+
readTopLevelTomlBlock,
|
|
10
|
+
removeTopLevelTomlBlock,
|
|
11
|
+
stripTomlSection,
|
|
12
|
+
upsertTopLevelTomlBlock,
|
|
13
|
+
} from './cli-toml.mjs'
|
|
14
|
+
|
|
15
|
+
export const CODEX_PLUGIN_CONFIG_HEADER = '[plugins."helloagents@local-plugins"]'
|
|
16
|
+
export const CODEX_MANAGED_TOML_COMMENT = '# helloagents-managed'
|
|
17
|
+
const CODEX_DEVELOPER_INSTRUCTIONS_BACKUP_BASENAME = 'developer_instructions'
|
|
18
|
+
|
|
19
|
+
export const CODEX_DEVELOPER_INSTRUCTIONS = `CRITICAL: These are HelloAGENTS global defaults for Codex. Use them as the baseline for main-agent behavior. Spawned sub-agents should focus on the delegated task unless they are explicitly required to follow main-agent-only workflow.
|
|
20
|
+
If the current workspace contains a project-level AGENTS.md or other repo-specific instructions, treat those as the more specific and authoritative instructions. Use these global defaults only where they do not conflict. Standby/global behavior is determined by the active workspace instructions, not by this global default block.
|
|
21
|
+
If work was already in progress and earlier context was compressed, first restore the active project state from the most relevant project state files or other project-local context artifacts, then continue from the actual interruption point without restarting the workflow or repeating completed steps.`
|
|
22
|
+
|
|
23
|
+
export function isManagedCodexStandbyInstructionPath(normalized = '') {
|
|
24
|
+
return /\/\.codex\/helloagents\/bootstrap-lite\.md/i.test(normalized)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function isManagedCodexGlobalInstructionPath(normalized = '') {
|
|
28
|
+
return /\/plugins\/helloagents\/AGENTS\.md/i.test(normalized)
|
|
29
|
+
|| /\/plugins\/helloagents\/bootstrap\.md/i.test(normalized)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function upsertCodexPluginConfig(text) {
|
|
33
|
+
const stripped = stripTomlSection(text, CODEX_PLUGIN_CONFIG_HEADER).text.trimEnd()
|
|
34
|
+
const block = `${CODEX_PLUGIN_CONFIG_HEADER}\nenabled = true`
|
|
35
|
+
return stripped ? `${stripped}\n\n${block}\n` : `${block}\n`
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function removeCodexPluginConfig(text) {
|
|
39
|
+
return stripTomlSection(text, CODEX_PLUGIN_CONFIG_HEADER).text
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function isManagedCodexModelInstruction(line = '') {
|
|
43
|
+
const normalized = String(line || '').replace(/\\/g, '/')
|
|
44
|
+
return line.includes('model_instructions_file')
|
|
45
|
+
&& (
|
|
46
|
+
line.includes(CODEX_MANAGED_TOML_COMMENT)
|
|
47
|
+
|| isManagedCodexStandbyInstructionPath(normalized)
|
|
48
|
+
|| isManagedCodexGlobalInstructionPath(normalized)
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function isManagedCodexNotify(line = '') {
|
|
53
|
+
return line.includes('codex-notify') || (line.includes('helloagents') && line.includes('notify'))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function isManagedCodexBackupInstruction(line = '') {
|
|
57
|
+
return line.includes(CODEX_MANAGED_TOML_COMMENT)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function formatManagedCodexDeveloperInstructions() {
|
|
61
|
+
return `"""\n${CODEX_DEVELOPER_INSTRUCTIONS}\n"""`
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function backupUserCodexDeveloperInstructions(configPath, existingBlock) {
|
|
65
|
+
if (!existingBlock || existingBlock.includes('HelloAGENTS')) return
|
|
66
|
+
safeWrite(createTimestampedBackupPath(configPath, CODEX_DEVELOPER_INSTRUCTIONS_BACKUP_BASENAME), `${existingBlock}\n`)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function sanitizeCodexDeveloperInstructionsBackup(block = '') {
|
|
70
|
+
const normalized = String(block || '').trim()
|
|
71
|
+
if (!normalized.startsWith('developer_instructions =')) return ''
|
|
72
|
+
if (normalized.includes(CODEX_DEVELOPER_INSTRUCTIONS)) return ''
|
|
73
|
+
return normalized
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readCodexDeveloperInstructionsBackup(configPath) {
|
|
77
|
+
return sanitizeCodexDeveloperInstructionsBackup(
|
|
78
|
+
readCodexBackup(configPath, CODEX_DEVELOPER_INSTRUCTIONS_BACKUP_BASENAME),
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function removeCodexDeveloperInstructionsBackup(configPath) {
|
|
83
|
+
removeCodexBackup(configPath, CODEX_DEVELOPER_INSTRUCTIONS_BACKUP_BASENAME)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function installCodexDeveloperInstructions(configPath, toml) {
|
|
87
|
+
const existing = readTopLevelTomlBlock(toml, 'developer_instructions')
|
|
88
|
+
backupUserCodexDeveloperInstructions(configPath, existing)
|
|
89
|
+
return upsertTopLevelTomlBlock(toml, 'developer_instructions', formatManagedCodexDeveloperInstructions())
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function uninstallCodexDeveloperInstructions(configPath, toml) {
|
|
93
|
+
const existing = readTopLevelTomlBlock(toml, 'developer_instructions')
|
|
94
|
+
if (!existing.includes('HelloAGENTS')) return toml
|
|
95
|
+
let next = removeTopLevelTomlBlock(toml, 'developer_instructions')
|
|
96
|
+
const backupDeveloperInstructions = readCodexDeveloperInstructionsBackup(configPath)
|
|
97
|
+
next = ensureTopLevelTomlBlock(next, 'developer_instructions', backupDeveloperInstructions)
|
|
98
|
+
removeCodexDeveloperInstructionsBackup(configPath)
|
|
99
|
+
return next
|
|
100
|
+
}
|