helloagents 3.0.12 → 3.0.15-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/.claude-plugin/marketplace.json +6 -4
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/README.md +169 -30
  5. package/README_CN.md +169 -30
  6. package/bootstrap-lite.md +27 -20
  7. package/bootstrap.md +30 -23
  8. package/cli.mjs +119 -11
  9. package/gemini-extension.json +1 -1
  10. package/install.ps1 +125 -0
  11. package/install.sh +118 -0
  12. package/package.json +23 -4
  13. package/scripts/advisor-state.mjs +36 -63
  14. package/scripts/capability-registry.mjs +3 -3
  15. package/scripts/cli-branch.mjs +84 -0
  16. package/scripts/cli-codex-config.mjs +11 -20
  17. package/scripts/cli-codex.mjs +32 -38
  18. package/scripts/cli-doctor-render.mjs +4 -0
  19. package/scripts/cli-doctor.mjs +40 -30
  20. package/scripts/cli-host-detect.mjs +0 -1
  21. package/scripts/cli-hosts.mjs +16 -8
  22. package/scripts/cli-lifecycle-hosts.mjs +92 -27
  23. package/scripts/cli-lifecycle.mjs +9 -7
  24. package/scripts/cli-messages.mjs +34 -16
  25. package/scripts/cli-runtime-carrier.mjs +36 -0
  26. package/scripts/cli-runtime-root.mjs +72 -0
  27. package/scripts/cli-toml.mjs +0 -79
  28. package/scripts/cli-utils.mjs +30 -4
  29. package/scripts/closeout-state.mjs +35 -62
  30. package/scripts/delivery-gate-messages.mjs +70 -0
  31. package/scripts/delivery-gate.mjs +9 -75
  32. package/scripts/guard-rules.mjs +42 -42
  33. package/scripts/guard.mjs +44 -24
  34. package/scripts/notify-context.mjs +19 -28
  35. package/scripts/notify-gates.mjs +2 -0
  36. package/scripts/notify-route.mjs +9 -7
  37. package/scripts/notify-ui.mjs +46 -33
  38. package/scripts/notify.mjs +60 -32
  39. package/scripts/project-storage.mjs +35 -66
  40. package/scripts/ralph-loop.mjs +36 -31
  41. package/scripts/replay-state.mjs +31 -128
  42. package/scripts/review-state.mjs +34 -61
  43. package/scripts/runtime-artifacts.mjs +95 -0
  44. package/scripts/runtime-context.mjs +35 -29
  45. package/scripts/runtime-scope.mjs +313 -0
  46. package/scripts/session-capsule.mjs +202 -0
  47. package/scripts/turn-state-cli.mjs +17 -0
  48. package/scripts/turn-state.mjs +185 -66
  49. package/scripts/turn-stop-gate.mjs +24 -6
  50. package/scripts/verify-state.mjs +34 -85
  51. package/scripts/visual-state.mjs +38 -65
  52. package/scripts/workflow-core.mjs +2 -2
  53. package/scripts/workflow-plan-files.mjs +1 -1
  54. package/scripts/workflow-recommendation.mjs +17 -13
  55. package/scripts/workflow-state.mjs +5 -5
  56. package/skills/commands/build/SKILL.md +1 -1
  57. package/skills/commands/commit/SKILL.md +1 -1
  58. package/skills/commands/help/SKILL.md +3 -3
  59. package/skills/commands/loop/SKILL.md +1 -1
  60. package/skills/commands/plan/SKILL.md +8 -6
  61. package/skills/commands/prd/SKILL.md +5 -3
  62. package/skills/commands/verify/SKILL.md +5 -5
  63. package/skills/hello-debug/SKILL.md +20 -3
  64. package/skills/hello-review/SKILL.md +2 -2
  65. package/skills/hello-subagent/SKILL.md +2 -2
  66. package/skills/hello-test/SKILL.md +6 -2
  67. package/skills/hello-ui/SKILL.md +4 -4
  68. package/skills/hello-verify/SKILL.md +10 -7
  69. package/skills/helloagents/SKILL.md +12 -7
  70. package/templates/context.md +6 -0
  71. package/templates/plans/plan.md +3 -0
  72. package/templates/plans/tasks.md +8 -3
@@ -1,76 +1,11 @@
1
- import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
2
- import { dirname, join, normalize, resolve } from 'node:path'
3
- import { homedir } from 'node:os'
1
+ import { dirname } from 'node:path'
4
2
 
5
- const RUNTIME_DIR = join(homedir(), '.helloagents', 'runtime')
6
- const REPLAY_CONTEXT_PATH = join(RUNTIME_DIR, 'replay-context.json')
7
- const REPLAY_SESSION_TTL_MS = 12 * 60 * 60 * 1000
8
- const MAX_REPLAY_SESSIONS = 3
9
-
10
- function normalizePath(filePath = '') {
11
- return filePath ? normalize(resolve(filePath)) : ''
12
- }
13
-
14
- function ensureRuntimeDir() {
15
- mkdirSync(dirname(REPLAY_CONTEXT_PATH), { recursive: true })
16
- }
17
-
18
- function readReplayContext() {
19
- try {
20
- return JSON.parse(readFileSync(REPLAY_CONTEXT_PATH, 'utf-8'))
21
- } catch {
22
- return {}
23
- }
24
- }
25
-
26
- function writeReplayContext(context) {
27
- ensureRuntimeDir()
28
- writeFileSync(REPLAY_CONTEXT_PATH, `${JSON.stringify(context, null, 2)}\n`, 'utf-8')
29
- }
30
-
31
- function getReplayKey(cwd, host = '') {
32
- return `${normalizePath(cwd)}::${host || 'unknown'}`
33
- }
34
-
35
- function findLatestReplaySession(context, cwd) {
36
- const normalizedCwd = normalizePath(cwd)
37
- const entries = Object.values(context)
38
- .filter((entry) => entry?.cwd === normalizedCwd && entry.filePath && existsSync(entry.filePath))
39
- .sort((left, right) => (right.updatedAt || 0) - (left.updatedAt || 0))
40
- return entries[0] || null
41
- }
42
-
43
- function getProjectRoot(cwd) {
44
- const projectRoot = join(cwd, '.helloagents')
45
- return existsSync(projectRoot) ? projectRoot : ''
46
- }
47
-
48
- export function getReplayDir(cwd) {
49
- const projectRoot = getProjectRoot(cwd)
50
- return projectRoot ? join(projectRoot, 'replay') : ''
51
- }
52
-
53
- function ensureReplayDir(cwd) {
54
- const replayDir = getReplayDir(cwd)
55
- if (!replayDir) return ''
56
- mkdirSync(replayDir, { recursive: true })
57
- return replayDir
58
- }
59
-
60
- function listReplaySessionFiles(replayDir) {
61
- if (!replayDir || !existsSync(replayDir)) return []
62
- return readdirSync(replayDir)
63
- .filter((name) => name.endsWith('.jsonl'))
64
- .sort()
65
- }
66
-
67
- function trimReplaySessions(replayDir) {
68
- const files = listReplaySessionFiles(replayDir)
69
- const staleFiles = files.slice(0, Math.max(0, files.length - MAX_REPLAY_SESSIONS))
70
- for (const fileName of staleFiles) {
71
- rmSync(join(replayDir, fileName), { force: true })
72
- }
73
- }
3
+ import {
4
+ appendSessionEvent,
5
+ getSessionEventsPath,
6
+ resetSessionEvents,
7
+ } from './session-capsule.mjs'
8
+ import { getProjectSessionScope } from './runtime-scope.mjs'
74
9
 
75
10
  function sanitizeReplayValue(value) {
76
11
  if (typeof value === 'string') {
@@ -102,43 +37,6 @@ function sanitizeReplayValue(value) {
102
37
  return value
103
38
  }
104
39
 
105
- function getReplaySession(cwd, { host = '', create = false, reset = false } = {}) {
106
- const replayDir = ensureReplayDir(cwd)
107
- if (!replayDir) return null
108
-
109
- const key = getReplayKey(cwd, host)
110
- const context = readReplayContext()
111
- const current = context[key] || (!host ? findLatestReplaySession(context, cwd) : null)
112
- const isExpired = !current?.updatedAt || (Date.now() - current.updatedAt > REPLAY_SESSION_TTL_MS)
113
- const isMissing = !current?.filePath || !existsSync(current.filePath)
114
-
115
- if (!reset && !isExpired && !isMissing) {
116
- context[key] = {
117
- ...current,
118
- updatedAt: Date.now(),
119
- }
120
- writeReplayContext(context)
121
- return context[key]
122
- }
123
-
124
- if (!create) return null
125
-
126
- const stamp = new Date().toISOString().replace(/[:.]/g, '-')
127
- const suffix = Math.random().toString(36).slice(2, 8)
128
- const sessionId = `${stamp}-${host || 'unknown'}-${suffix}`
129
- const filePath = join(replayDir, `${sessionId}.jsonl`)
130
- const next = {
131
- cwd: normalizePath(cwd),
132
- host: host || 'unknown',
133
- sessionId,
134
- filePath,
135
- updatedAt: Date.now(),
136
- }
137
- context[key] = next
138
- writeReplayContext(context)
139
- return next
140
- }
141
-
142
40
  function buildReplayRecommendation(recommendation) {
143
41
  if (!recommendation) return {}
144
42
  return {
@@ -151,24 +49,35 @@ function buildReplayRecommendation(recommendation) {
151
49
  }
152
50
  }
153
51
 
52
+ export function getReplayDir(cwd, options = {}) {
53
+ const eventPath = getSessionEventsPath(cwd, options)
54
+ return eventPath ? dirname(eventPath) : ''
55
+ }
56
+
154
57
  export function startReplaySession(cwd, {
155
58
  host = '',
156
59
  source = 'startup',
157
60
  bootstrapFile = '',
158
61
  installMode = '',
62
+ payload = {},
63
+ env,
64
+ ppid,
159
65
  } = {}) {
160
- const session = getReplaySession(cwd, { host, create: true, reset: true })
161
- if (!session) return ''
66
+ const scope = getProjectSessionScope(cwd, { payload, env, ppid })
67
+ if (!scope.active) return ''
162
68
 
69
+ const filePath = resetSessionEvents(cwd, { payload, env, ppid })
163
70
  appendReplayEvent(cwd, {
164
71
  host,
165
72
  event: 'session_started',
166
73
  source,
167
74
  bootstrapFile,
168
75
  installMode,
169
- sessionId: session.sessionId,
76
+ payload,
77
+ env,
78
+ ppid,
170
79
  })
171
- return session.filePath
80
+ return filePath
172
81
  }
173
82
 
174
83
  export function appendReplayEvent(cwd, {
@@ -182,29 +91,23 @@ export function appendReplayEvent(cwd, {
182
91
  artifacts = [],
183
92
  details = {},
184
93
  sessionId = '',
94
+ payload = {},
95
+ env,
96
+ ppid,
185
97
  } = {}) {
186
- if (!event) return ''
187
- const session = getReplaySession(cwd, { host, create: true })
188
- if (!session?.filePath) return ''
98
+ const scope = getProjectSessionScope(cwd, { payload, env, ppid })
99
+ if (!scope.active || !event) return ''
189
100
 
190
- const payload = sanitizeReplayValue({
191
- ts: new Date().toISOString(),
101
+ return appendSessionEvent(cwd, sanitizeReplayValue({
192
102
  event,
193
- host: host || session.host,
103
+ host: host || 'unknown',
194
104
  source,
195
- sessionId: sessionId || session.sessionId,
105
+ sessionId: sessionId || scope.session,
196
106
  skillName,
197
107
  sourceSkillName,
198
108
  recommendation: buildReplayRecommendation(recommendation),
199
109
  reason,
200
110
  artifacts,
201
111
  details,
202
- })
203
-
204
- writeFileSync(session.filePath, `${JSON.stringify(payload)}\n`, {
205
- encoding: 'utf-8',
206
- flag: 'a',
207
- })
208
- trimReplaySessions(getReplayDir(cwd))
209
- return session.filePath
112
+ }), { payload, env, ppid })
210
113
  }
@@ -1,11 +1,18 @@
1
- import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
1
+ import { readFileSync } from 'node:fs'
2
2
  import { fileURLToPath } from 'node:url'
3
- import { join } from 'node:path'
4
- import { captureWorkspaceFingerprint } from './verify-state.mjs'
5
3
  import { appendReplayEvent } from './replay-state.mjs'
6
-
7
- export const REVIEW_EVIDENCE_FILE_NAME = '.ralph-review.json'
8
- const REVIEW_EVIDENCE_MAX_AGE_MS = 30 * 60 * 1000
4
+ import {
5
+ captureWorkspaceFingerprint,
6
+ clearRuntimeEvidence,
7
+ getRuntimeEvidencePath,
8
+ getRuntimeEvidenceRelativePath,
9
+ readRuntimeEvidence,
10
+ validateEvidenceFingerprint,
11
+ validateEvidenceTimestamp,
12
+ writeRuntimeEvidence,
13
+ } from './runtime-artifacts.mjs'
14
+
15
+ export const REVIEW_EVIDENCE_FILE_NAME = 'review.json'
9
16
  const VALID_REVIEW_OUTCOMES = new Set(['clean', 'findings'])
10
17
 
11
18
  function normalizeStringArray(values) {
@@ -20,20 +27,16 @@ function normalizeReviewOutcome(value) {
20
27
  return VALID_REVIEW_OUTCOMES.has(normalized) ? normalized : ''
21
28
  }
22
29
 
23
- export function getReviewEvidencePath(cwd) {
24
- return join(cwd, '.helloagents', REVIEW_EVIDENCE_FILE_NAME)
30
+ export function getReviewEvidencePath(cwd, options = {}) {
31
+ return getRuntimeEvidencePath(cwd, REVIEW_EVIDENCE_FILE_NAME, options)
25
32
  }
26
33
 
27
- export function readReviewEvidence(cwd) {
28
- try {
29
- return JSON.parse(readFileSync(getReviewEvidencePath(cwd), 'utf-8'))
30
- } catch {
31
- return null
32
- }
34
+ export function readReviewEvidence(cwd, options = {}) {
35
+ return readRuntimeEvidence(cwd, REVIEW_EVIDENCE_FILE_NAME, options)
33
36
  }
34
37
 
35
- export function clearReviewEvidence(cwd) {
36
- rmSync(getReviewEvidencePath(cwd), { force: true })
38
+ export function clearReviewEvidence(cwd, options = {}) {
39
+ clearRuntimeEvidence(cwd, REVIEW_EVIDENCE_FILE_NAME, options)
37
40
  }
38
41
 
39
42
  export function normalizeReviewEvidence(input = {}) {
@@ -56,8 +59,7 @@ export function writeReviewEvidence(cwd, {
56
59
  conclusion = '',
57
60
  findings = [],
58
61
  fileReferences = [],
59
- } = {}) {
60
- mkdirSync(join(cwd, '.helloagents'), { recursive: true })
62
+ } = {}, options = {}) {
61
63
  const normalized = normalizeReviewEvidence({
62
64
  source,
63
65
  originCommand,
@@ -78,11 +80,12 @@ export function writeReviewEvidence(cwd, {
78
80
  fileReferences: normalized.fileReferences,
79
81
  fingerprint: captureWorkspaceFingerprint(cwd),
80
82
  }
81
- writeFileSync(getReviewEvidencePath(cwd), `${JSON.stringify(payload, null, 2)}\n`, 'utf-8')
83
+ writeRuntimeEvidence(cwd, REVIEW_EVIDENCE_FILE_NAME, payload, options)
82
84
  appendReplayEvent(cwd, {
83
85
  event: 'review_evidence_written',
84
86
  source: normalized.source,
85
87
  skillName: normalized.originCommand,
88
+ payload: options.payload || {},
86
89
  details: {
87
90
  reviewMode: normalized.reviewMode,
88
91
  outcome: normalized.outcome,
@@ -90,59 +93,29 @@ export function writeReviewEvidence(cwd, {
90
93
  findings: normalized.findings,
91
94
  fileReferences: normalized.fileReferences,
92
95
  },
93
- artifacts: ['.helloagents/.ralph-review.json'],
96
+ artifacts: [getRuntimeEvidenceRelativePath(cwd, REVIEW_EVIDENCE_FILE_NAME, options)],
94
97
  })
95
98
  return payload
96
99
  }
97
100
 
98
- function readRequiredReviewEvidence(cwd) {
99
- const evidence = readReviewEvidence(cwd)
101
+ function readRequiredReviewEvidence(cwd, options = {}) {
102
+ const evidence = readReviewEvidence(cwd, options)
100
103
  if (evidence) return { evidence }
101
104
  return {
102
105
  error: {
103
106
  required: true,
104
107
  status: 'missing',
105
- details: ['missing successful review evidence for review-first closeout'],
108
+ details: ['缺少 review-first 收尾所需的成功审查证据'],
106
109
  },
107
110
  }
108
111
  }
109
112
 
110
113
  function validateReviewTimestamp(evidence, now) {
111
- const updatedAt = Date.parse(evidence.updatedAt || '')
112
- if (!Number.isFinite(updatedAt)) {
113
- return {
114
- required: true,
115
- status: 'invalid',
116
- evidence,
117
- details: ['review evidence timestamp is invalid'],
118
- }
119
- }
120
- if (now - updatedAt > REVIEW_EVIDENCE_MAX_AGE_MS) {
121
- return {
122
- required: true,
123
- status: 'stale-time',
124
- evidence,
125
- details: ['review evidence is older than 30 minutes'],
126
- }
127
- }
128
- return null
114
+ return validateEvidenceTimestamp(evidence, now, '审查证据')
129
115
  }
130
116
 
131
117
  function validateReviewFingerprint(cwd, evidence) {
132
- const currentFingerprint = captureWorkspaceFingerprint(cwd)
133
- if (
134
- currentFingerprint.available
135
- && evidence.fingerprint?.available
136
- && currentFingerprint.combined !== evidence.fingerprint.combined
137
- ) {
138
- return {
139
- required: true,
140
- status: 'stale-diff',
141
- evidence,
142
- details: ['workspace diff changed after the last successful review evidence'],
143
- }
144
- }
145
- return null
118
+ return validateEvidenceFingerprint(cwd, evidence, '成功审查证据')
146
119
  }
147
120
 
148
121
  function validateReviewOutcome(evidence) {
@@ -151,7 +124,7 @@ function validateReviewOutcome(evidence) {
151
124
  required: true,
152
125
  status: 'invalid',
153
126
  evidence,
154
- details: ['review evidence must record explicit outcome and conclusion'],
127
+ details: ['审查证据必须记录明确的 outcome conclusion'],
155
128
  }
156
129
  }
157
130
  if (normalizeReviewOutcome(evidence.outcome) !== 'clean') {
@@ -159,13 +132,13 @@ function validateReviewOutcome(evidence) {
159
132
  required: true,
160
133
  status: 'blocked',
161
134
  evidence,
162
- details: ['latest review evidence still records blocking findings'],
135
+ details: ['最新审查证据仍记录阻塞问题'],
163
136
  }
164
137
  }
165
138
  return null
166
139
  }
167
140
 
168
- export function getReviewEvidenceStatus(cwd, { required = false, now = Date.now() } = {}) {
141
+ export function getReviewEvidenceStatus(cwd, { required = false, now = Date.now(), ...options } = {}) {
169
142
  if (!required) {
170
143
  return {
171
144
  required: false,
@@ -173,7 +146,7 @@ export function getReviewEvidenceStatus(cwd, { required = false, now = Date.now(
173
146
  }
174
147
  }
175
148
 
176
- const requiredEvidence = readRequiredReviewEvidence(cwd)
149
+ const requiredEvidence = readRequiredReviewEvidence(cwd, options)
177
150
  if (requiredEvidence.error) return requiredEvidence.error
178
151
 
179
152
  const { evidence } = requiredEvidence
@@ -207,10 +180,10 @@ function main() {
207
180
 
208
181
  const input = readStdinJson()
209
182
  const cwd = input.cwd || process.cwd()
210
- const payload = writeReviewEvidence(cwd, input)
183
+ const payload = writeReviewEvidence(cwd, input, { payload: input })
211
184
  process.stdout.write(JSON.stringify({
212
185
  suppressOutput: true,
213
- path: getReviewEvidencePath(cwd),
186
+ path: getReviewEvidencePath(cwd, { payload: input }),
214
187
  payload,
215
188
  }))
216
189
  }
@@ -0,0 +1,95 @@
1
+ import { execSync } from 'node:child_process'
2
+
3
+ import {
4
+ clearSessionArtifact,
5
+ getSessionArtifactPath,
6
+ getSessionArtifactRelativePath,
7
+ readSessionArtifact,
8
+ writeSessionArtifact,
9
+ } from './session-capsule.mjs'
10
+
11
+ export const EVIDENCE_MAX_AGE_MS = 30 * 60 * 1000
12
+
13
+ function readGitDiffStat(cwd, args) {
14
+ try {
15
+ return execSync(`git diff --stat ${args}`.trim(), {
16
+ cwd,
17
+ encoding: 'utf-8',
18
+ timeout: 10_000,
19
+ stdio: ['pipe', 'pipe', 'pipe'],
20
+ }).trim()
21
+ } catch {
22
+ return null
23
+ }
24
+ }
25
+
26
+ export function captureWorkspaceFingerprint(cwd) {
27
+ const unstaged = readGitDiffStat(cwd, 'HEAD')
28
+ const staged = readGitDiffStat(cwd, '--cached')
29
+ const available = unstaged !== null || staged !== null
30
+
31
+ return {
32
+ available,
33
+ unstaged: unstaged || '',
34
+ staged: staged || '',
35
+ combined: `${unstaged || ''}\n---\n${staged || ''}`.trim(),
36
+ }
37
+ }
38
+
39
+ export function getRuntimeEvidencePath(cwd, fileName, options = {}) {
40
+ return getSessionArtifactPath(cwd, fileName, options)
41
+ }
42
+
43
+ export function getRuntimeEvidenceRelativePath(cwd, fileName, options = {}) {
44
+ return getSessionArtifactRelativePath(cwd, fileName, options)
45
+ }
46
+
47
+ export function readRuntimeEvidence(cwd, fileName, options = {}) {
48
+ return readSessionArtifact(cwd, fileName, options)
49
+ }
50
+
51
+ export function clearRuntimeEvidence(cwd, fileName, options = {}) {
52
+ clearSessionArtifact(cwd, fileName, options)
53
+ }
54
+
55
+ export function writeRuntimeEvidence(cwd, fileName, payload, options = {}) {
56
+ return writeSessionArtifact(cwd, fileName, payload, options)
57
+ }
58
+
59
+ export function validateEvidenceTimestamp(evidence, now, label) {
60
+ const updatedAt = Date.parse(evidence.updatedAt || '')
61
+ if (!Number.isFinite(updatedAt)) {
62
+ return {
63
+ required: true,
64
+ status: 'invalid',
65
+ evidence,
66
+ details: [`${label}时间戳无效`],
67
+ }
68
+ }
69
+ if (now - updatedAt > EVIDENCE_MAX_AGE_MS) {
70
+ return {
71
+ required: true,
72
+ status: 'stale-time',
73
+ evidence,
74
+ details: [`${label}超过 30 分钟`],
75
+ }
76
+ }
77
+ return null
78
+ }
79
+
80
+ export function validateEvidenceFingerprint(cwd, evidence, label) {
81
+ const currentFingerprint = captureWorkspaceFingerprint(cwd)
82
+ if (
83
+ currentFingerprint.available
84
+ && evidence.fingerprint?.available
85
+ && currentFingerprint.combined !== evidence.fingerprint.combined
86
+ ) {
87
+ return {
88
+ required: true,
89
+ status: 'stale-diff',
90
+ evidence,
91
+ details: [`工作区变更已不同于最近一次${label}后的状态`],
92
+ }
93
+ }
94
+ return null
95
+ }
@@ -1,56 +1,62 @@
1
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
2
- import { dirname, join, normalize, resolve } from 'node:path'
3
- import { homedir } from 'node:os'
1
+ import { normalize, resolve } from 'node:path'
2
+
3
+ import {
4
+ clearCapsuleSection,
5
+ getRuntimeScope,
6
+ readCapsuleSection,
7
+ writeCapsuleSection,
8
+ } from './session-capsule.mjs'
4
9
 
5
- const RUNTIME_DIR = join(homedir(), '.helloagents', 'runtime')
6
- const ROUTE_CONTEXT_PATH = join(RUNTIME_DIR, 'route-context.json')
7
10
  const ROUTE_CONTEXT_TTL_MS = 30 * 60 * 1000
8
11
 
9
12
  function normalizePath(filePath = '') {
10
13
  return filePath ? normalize(resolve(filePath)) : ''
11
14
  }
12
15
 
13
- function ensureRuntimeDir() {
14
- mkdirSync(dirname(ROUTE_CONTEXT_PATH), { recursive: true })
16
+ function resolvePayload(options = {}) {
17
+ return options.payload && typeof options.payload === 'object' ? options.payload : options
15
18
  }
16
19
 
17
- export function clearRouteContext() {
18
- rmSync(ROUTE_CONTEXT_PATH, { force: true })
20
+ export function clearRouteContext(options = {}) {
21
+ const payload = resolvePayload(options)
22
+ const cwd = options.cwd || payload.cwd || process.cwd()
23
+ clearCapsuleSection(cwd, 'route', { payload, env: options.env, ppid: options.ppid })
19
24
  }
20
25
 
21
- export function writeRouteContext({ cwd, skillName, sourceSkillName = skillName }) {
22
- ensureRuntimeDir()
26
+ export function writeRouteContext({ cwd, skillName, sourceSkillName = skillName, payload = {}, env, ppid }) {
27
+ const scope = getRuntimeScope(cwd, { payload, env, ppid })
23
28
  const context = {
24
29
  cwd: normalizePath(cwd),
25
30
  skillName,
26
31
  sourceSkillName,
27
32
  zeroSideEffect: skillName === 'idea',
33
+ scope: scope.scope,
34
+ key: scope.key,
28
35
  updatedAt: Date.now(),
29
36
  }
30
- writeFileSync(ROUTE_CONTEXT_PATH, `${JSON.stringify(context, null, 2)}\n`, 'utf-8')
37
+ writeCapsuleSection(cwd, 'route', context, { payload, env, ppid })
31
38
  }
32
39
 
33
- export function readRouteContext() {
34
- if (!existsSync(ROUTE_CONTEXT_PATH)) return null
35
-
36
- try {
37
- const context = JSON.parse(readFileSync(ROUTE_CONTEXT_PATH, 'utf-8'))
38
- if (!context?.cwd || !context?.skillName || !context?.updatedAt) return null
39
- if (Date.now() - context.updatedAt > ROUTE_CONTEXT_TTL_MS) {
40
- clearRouteContext()
41
- return null
42
- }
43
- return {
44
- ...context,
45
- cwd: normalizePath(context.cwd),
46
- }
47
- } catch {
40
+ export function readRouteContext(options = {}) {
41
+ const payload = resolvePayload(options)
42
+ const cwd = options.cwd || payload.cwd || process.cwd()
43
+ const context = readCapsuleSection(cwd, 'route', { payload, env: options.env, ppid: options.ppid })
44
+ if (!context?.cwd || !context?.skillName || !context?.updatedAt) {
45
+ return null
46
+ }
47
+ if (Date.now() - context.updatedAt > ROUTE_CONTEXT_TTL_MS) {
48
+ clearRouteContext({ cwd, payload, env: options.env, ppid: options.ppid })
48
49
  return null
49
50
  }
51
+
52
+ return {
53
+ ...context,
54
+ cwd: normalizePath(context.cwd),
55
+ }
50
56
  }
51
57
 
52
- export function getApplicableRouteContext({ cwd = '', filePath = '' } = {}) {
53
- const context = readRouteContext()
58
+ export function getApplicableRouteContext({ cwd = '', filePath = '', payload = {}, env, ppid } = {}) {
59
+ const context = readRouteContext({ cwd, payload, env, ppid })
54
60
  if (!context) return null
55
61
 
56
62
  const normalizedCwd = normalizePath(cwd)