@cocorograph/hub-agent 0.7.89 → 0.7.91
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/package.json +1 -1
- package/src/claude-md-sync.mjs +51 -34
- package/src/claude-md.mjs +35 -28
- package/src/main.mjs +43 -15
- package/src/tmux.mjs +19 -3
package/package.json
CHANGED
package/src/claude-md-sync.mjs
CHANGED
|
@@ -26,12 +26,18 @@ import path from "node:path"
|
|
|
26
26
|
import crypto from "node:crypto"
|
|
27
27
|
|
|
28
28
|
const DEFAULT_HUB_API = "https://api.hub.cocorograph.com"
|
|
29
|
-
const TOKEN_PATH = path.join(os.homedir(), ".claude", ".hub_token.json")
|
|
30
29
|
const META_DIR = ".cockpit-sync"
|
|
31
|
-
const META_FILE = "claude-md.json"
|
|
32
|
-
const BASE_FILE = "CLAUDE.md.base"
|
|
33
30
|
const BAK_KEEP = 3 // 通常 .bak と衝突 .bak それぞれ直近 3 件まで保持
|
|
34
31
|
|
|
32
|
+
function configPaths(configFilename = "CLAUDE.md") {
|
|
33
|
+
const filename = configFilename === "AGENTS.md" ? "AGENTS.md" : "CLAUDE.md"
|
|
34
|
+
return {
|
|
35
|
+
filename,
|
|
36
|
+
metaFile: filename === "AGENTS.md" ? "agents-md.json" : "claude-md.json",
|
|
37
|
+
baseFile: `${filename}.base`,
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
35
41
|
async function readJsonOrNull(p) {
|
|
36
42
|
try {
|
|
37
43
|
const text = await fs.readFile(p, "utf-8")
|
|
@@ -41,9 +47,13 @@ async function readJsonOrNull(p) {
|
|
|
41
47
|
}
|
|
42
48
|
}
|
|
43
49
|
|
|
44
|
-
async function loadAccessToken() {
|
|
45
|
-
const
|
|
46
|
-
|
|
50
|
+
async function loadAccessToken(configFilename = "CLAUDE.md") {
|
|
51
|
+
const cliDir = configPaths(configFilename).filename === "AGENTS.md" ? ".codex" : ".claude"
|
|
52
|
+
const primary = await readJsonOrNull(path.join(os.homedir(), cliDir, ".hub_token.json"))
|
|
53
|
+
if (primary && typeof primary.access === "string" && primary.access) return primary.access
|
|
54
|
+
// 既存のhub-agent enrollmentは ~/.claude にtokenを持つため、Codex単独導入前の端末も救済する。
|
|
55
|
+
const legacy = await readJsonOrNull(path.join(os.homedir(), ".claude", ".hub_token.json"))
|
|
56
|
+
return (legacy && typeof legacy.access === "string" && legacy.access) || null
|
|
47
57
|
}
|
|
48
58
|
|
|
49
59
|
function sha256(s) {
|
|
@@ -133,29 +143,29 @@ async function putHubClaudeMd({ hubUrl, accessToken, dirName, body, fetchImpl, l
|
|
|
133
143
|
}
|
|
134
144
|
}
|
|
135
145
|
|
|
136
|
-
async function
|
|
146
|
+
async function readLocalConfigMd(targetDir, configFilename) {
|
|
137
147
|
try {
|
|
138
|
-
return await fs.readFile(path.join(targetDir,
|
|
148
|
+
return await fs.readFile(path.join(targetDir, configPaths(configFilename).filename), "utf-8")
|
|
139
149
|
} catch (err) {
|
|
140
150
|
if (err?.code === "ENOENT") return null
|
|
141
151
|
throw err
|
|
142
152
|
}
|
|
143
153
|
}
|
|
144
154
|
|
|
145
|
-
async function
|
|
146
|
-
await fs.writeFile(path.join(targetDir,
|
|
155
|
+
async function writeLocalConfigMd(targetDir, body, configFilename) {
|
|
156
|
+
await fs.writeFile(path.join(targetDir, configPaths(configFilename).filename), body, "utf-8")
|
|
147
157
|
}
|
|
148
158
|
|
|
149
|
-
async function readMeta(targetDir) {
|
|
150
|
-
const metaPath = path.join(targetDir, META_DIR,
|
|
159
|
+
async function readMeta(targetDir, configFilename) {
|
|
160
|
+
const metaPath = path.join(targetDir, META_DIR, configPaths(configFilename).metaFile)
|
|
151
161
|
return await readJsonOrNull(metaPath)
|
|
152
162
|
}
|
|
153
163
|
|
|
154
|
-
async function writeMeta(targetDir, meta) {
|
|
164
|
+
async function writeMeta(targetDir, meta, configFilename) {
|
|
155
165
|
const metaDir = path.join(targetDir, META_DIR)
|
|
156
166
|
await fs.mkdir(metaDir, { recursive: true })
|
|
157
167
|
await fs.writeFile(
|
|
158
|
-
path.join(metaDir,
|
|
168
|
+
path.join(metaDir, configPaths(configFilename).metaFile),
|
|
159
169
|
JSON.stringify(meta, null, 2),
|
|
160
170
|
"utf-8",
|
|
161
171
|
)
|
|
@@ -171,11 +181,11 @@ async function writeMeta(targetDir, meta) {
|
|
|
171
181
|
* meta(claude-md.json) は hash と updated_at しか持たないため、本文そのものが要る
|
|
172
182
|
* 3-way merge の用途ではこのファイルが必要。
|
|
173
183
|
*/
|
|
174
|
-
async function writeBaseBody(targetDir, body) {
|
|
184
|
+
async function writeBaseBody(targetDir, body, configFilename) {
|
|
175
185
|
if (body == null) return
|
|
176
186
|
const metaDir = path.join(targetDir, META_DIR)
|
|
177
187
|
await fs.mkdir(metaDir, { recursive: true })
|
|
178
|
-
await fs.writeFile(path.join(metaDir,
|
|
188
|
+
await fs.writeFile(path.join(metaDir, configPaths(configFilename).baseFile), body, "utf-8")
|
|
179
189
|
}
|
|
180
190
|
|
|
181
191
|
/**
|
|
@@ -184,11 +194,12 @@ async function writeBaseBody(targetDir, body) {
|
|
|
184
194
|
*
|
|
185
195
|
* @param {'normal'|'conflict'} kind バックアップ種別(命名 prefix)
|
|
186
196
|
*/
|
|
187
|
-
async function
|
|
197
|
+
async function backupLocalConfigMd(targetDir, body, kind, configFilename) {
|
|
188
198
|
if (body == null) return
|
|
189
199
|
const metaDir = path.join(targetDir, META_DIR)
|
|
190
200
|
await fs.mkdir(metaDir, { recursive: true })
|
|
191
|
-
const
|
|
201
|
+
const filename = configPaths(configFilename).filename
|
|
202
|
+
const prefix = kind === "conflict" ? `${filename}.conflict` : filename
|
|
192
203
|
const bakName = makeBakName(prefix)
|
|
193
204
|
await fs.writeFile(path.join(metaDir, bakName), body, "utf-8")
|
|
194
205
|
// 同 prefix の .bak を新しい順に BAK_KEEP 件まで残す。
|
|
@@ -224,12 +235,13 @@ export async function syncClaudeMdWithHub({
|
|
|
224
235
|
accessToken,
|
|
225
236
|
logger,
|
|
226
237
|
fetchImpl,
|
|
238
|
+
configFilename = "CLAUDE.md",
|
|
227
239
|
} = {}) {
|
|
228
240
|
if (!targetDir) throw new Error("syncClaudeMdWithHub requires targetDir")
|
|
229
241
|
if (!dirName) throw new Error("syncClaudeMdWithHub requires dirName")
|
|
230
242
|
|
|
231
243
|
const token =
|
|
232
|
-
accessToken === undefined ? await loadAccessToken() : accessToken || null
|
|
244
|
+
accessToken === undefined ? await loadAccessToken(configFilename) : accessToken || null
|
|
233
245
|
if (!token) {
|
|
234
246
|
logger?.info?.({ dirName }, "syncClaudeMdWithHub: no access token, skip")
|
|
235
247
|
return { action: "skipped", reason: "no-token" }
|
|
@@ -252,11 +264,11 @@ export async function syncClaudeMdWithHub({
|
|
|
252
264
|
const hubUpdatedAt = hub.claude_md_updated_at || null
|
|
253
265
|
|
|
254
266
|
// 2) ローカル CLAUDE.md を読み、現在 hash を計算
|
|
255
|
-
const localBody = await
|
|
267
|
+
const localBody = await readLocalConfigMd(targetDir, configFilename)
|
|
256
268
|
const localCurrentHash = localBody == null ? null : sha256(localBody)
|
|
257
269
|
|
|
258
270
|
// 3) メタファイルを読み、前回 sync の状態を取得
|
|
259
|
-
const meta = await readMeta(targetDir)
|
|
271
|
+
const meta = await readMeta(targetDir, configFilename)
|
|
260
272
|
|
|
261
273
|
// Hub が一度も保存されていない(空本文 + updated_at が null)プロジェクトは
|
|
262
274
|
// 「Hub UI で意図的に未設定にしている」運用(テナント標準 CLAUDE.md に任せる等)の
|
|
@@ -276,7 +288,12 @@ export async function syncClaudeMdWithHub({
|
|
|
276
288
|
if (!meta) {
|
|
277
289
|
// 初回 sync。
|
|
278
290
|
hubChanged = !!hubBody
|
|
279
|
-
if (
|
|
291
|
+
if (configPaths(configFilename).filename === "AGENTS.md") {
|
|
292
|
+
// Codex の AGENTS.md は既存リポジトリにも通常存在し、案件正本とは別の
|
|
293
|
+
// ローカル規約を含む。初回に terminal-wins で Project.claude_md を壊さないよう、
|
|
294
|
+
// Hub 正本を優先して pull(既存 AGENTS.md は .bak に退避)する。
|
|
295
|
+
localChanged = false
|
|
296
|
+
} else if (localBody == null) {
|
|
280
297
|
localChanged = false
|
|
281
298
|
} else {
|
|
282
299
|
localChanged = !!localBody // 非空なら端末側に何かある=衝突候補
|
|
@@ -300,15 +317,15 @@ export async function syncClaudeMdWithHub({
|
|
|
300
317
|
|
|
301
318
|
if (hubChanged && !localChanged) {
|
|
302
319
|
// pull: ローカルを .bak 退避 → Hub の本文で上書き
|
|
303
|
-
await
|
|
304
|
-
await
|
|
320
|
+
await backupLocalConfigMd(targetDir, localBody, "normal", configFilename)
|
|
321
|
+
await writeLocalConfigMd(targetDir, hubBody, configFilename)
|
|
305
322
|
await writeMeta(targetDir, {
|
|
306
323
|
lastSyncedHubUpdatedAt: hubUpdatedAt,
|
|
307
324
|
lastSyncedHash: hubHash || sha256(hubBody),
|
|
308
325
|
lastSyncedAt: nowIso(),
|
|
309
|
-
})
|
|
326
|
+
}, configFilename)
|
|
310
327
|
// 3-way merge 用 base: pull 直後はローカル == Hub なので Hub 本文を base に。
|
|
311
|
-
await writeBaseBody(targetDir, hubBody)
|
|
328
|
+
await writeBaseBody(targetDir, hubBody, configFilename)
|
|
312
329
|
logger?.info?.({ dirName }, "syncClaudeMdWithHub: pull")
|
|
313
330
|
return { action: "pull" }
|
|
314
331
|
}
|
|
@@ -334,17 +351,17 @@ export async function syncClaudeMdWithHub({
|
|
|
334
351
|
lastSyncedHubUpdatedAt: nowIso(),
|
|
335
352
|
lastSyncedHash: localCurrentHash,
|
|
336
353
|
lastSyncedAt: nowIso(),
|
|
337
|
-
})
|
|
354
|
+
}, configFilename)
|
|
338
355
|
// 3-way merge 用 base: push 直後は Hub の正本がローカルと一致するので、
|
|
339
356
|
// 次回の base は今 push した本文 (= localBody)。
|
|
340
|
-
await writeBaseBody(targetDir, localBody || "")
|
|
357
|
+
await writeBaseBody(targetDir, localBody || "", configFilename)
|
|
341
358
|
logger?.info?.({ dirName }, "syncClaudeMdWithHub: push")
|
|
342
359
|
return { action: "push" }
|
|
343
360
|
}
|
|
344
361
|
|
|
345
362
|
// hubChanged && localChanged → 衝突。端末側を勝者にして Hub に PUT。
|
|
346
363
|
// Hub の取り込み内容は .bak.conflict に退避(後で人手マージできるよう)。
|
|
347
|
-
await
|
|
364
|
+
await backupLocalConfigMd(targetDir, hubBody, "conflict", configFilename)
|
|
348
365
|
const ok = await putHubClaudeMd({
|
|
349
366
|
hubUrl: url,
|
|
350
367
|
accessToken: token,
|
|
@@ -361,9 +378,9 @@ export async function syncClaudeMdWithHub({
|
|
|
361
378
|
lastSyncedHubUpdatedAt: nowIso(),
|
|
362
379
|
lastSyncedHash: localCurrentHash,
|
|
363
380
|
lastSyncedAt: nowIso(),
|
|
364
|
-
})
|
|
381
|
+
}, configFilename)
|
|
365
382
|
// 3-way merge 用 base: conflict push 後の Hub 正本は端末側本文 (= localBody)。
|
|
366
|
-
await writeBaseBody(targetDir, localBody || "")
|
|
383
|
+
await writeBaseBody(targetDir, localBody || "", configFilename)
|
|
367
384
|
logger?.warn?.(
|
|
368
385
|
{ dirName },
|
|
369
386
|
"syncClaudeMdWithHub: conflict (terminal wins); hub body saved to .bak.conflict",
|
|
@@ -374,7 +391,7 @@ export async function syncClaudeMdWithHub({
|
|
|
374
391
|
// メタ操作・ハッシュ計算は ensureClaudeMd 側 (Hub pull 初回配信) でも使うため
|
|
375
392
|
// named export する。同じファイルパス・同じスキーマで管理することで、初回配信と
|
|
376
393
|
// 以降の sync で食い違いを起こさない。
|
|
377
|
-
export { sha256, readMeta, writeMeta, writeBaseBody, nowIso }
|
|
394
|
+
export { sha256, readMeta, writeMeta, writeBaseBody, nowIso, configPaths }
|
|
378
395
|
|
|
379
396
|
// 内部テスト用エクスポート
|
|
380
397
|
export const _internals = {
|
|
@@ -382,7 +399,7 @@ export const _internals = {
|
|
|
382
399
|
readMeta,
|
|
383
400
|
writeMeta,
|
|
384
401
|
writeBaseBody,
|
|
385
|
-
|
|
402
|
+
backupLocalConfigMd,
|
|
386
403
|
BAK_KEEP,
|
|
387
|
-
|
|
404
|
+
configPaths,
|
|
388
405
|
}
|
package/src/claude-md.mjs
CHANGED
|
@@ -27,7 +27,6 @@ import {
|
|
|
27
27
|
} from "./claude-md-sync.mjs"
|
|
28
28
|
|
|
29
29
|
const DEFAULT_HUB_API = "https://api.hub.cocorograph.com"
|
|
30
|
-
const TOKEN_PATH = path.join(os.homedir(), ".claude", ".hub_token.json")
|
|
31
30
|
|
|
32
31
|
async function readJsonOrNull(p) {
|
|
33
32
|
try {
|
|
@@ -38,9 +37,12 @@ async function readJsonOrNull(p) {
|
|
|
38
37
|
}
|
|
39
38
|
}
|
|
40
39
|
|
|
41
|
-
async function loadAccessToken() {
|
|
42
|
-
const
|
|
43
|
-
|
|
40
|
+
async function loadAccessToken(configFilename = "CLAUDE.md") {
|
|
41
|
+
const cliDir = configFilename === "AGENTS.md" ? ".codex" : ".claude"
|
|
42
|
+
const primary = await readJsonOrNull(path.join(os.homedir(), cliDir, ".hub_token.json"))
|
|
43
|
+
if (primary && typeof primary.access === "string" && primary.access) return primary.access
|
|
44
|
+
const legacy = await readJsonOrNull(path.join(os.homedir(), ".claude", ".hub_token.json"))
|
|
45
|
+
return (legacy && typeof legacy.access === "string" && legacy.access) || null
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
/**
|
|
@@ -134,7 +136,7 @@ function isGitHubSlug(repoUrl) {
|
|
|
134
136
|
return /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repoUrl)
|
|
135
137
|
}
|
|
136
138
|
|
|
137
|
-
function renderRepositorySection(repositories, dirName = "") {
|
|
139
|
+
function renderRepositorySection(repositories, dirName = "", cliKind = "claude") {
|
|
138
140
|
if (!Array.isArray(repositories) || repositories.length === 0) return ""
|
|
139
141
|
|
|
140
142
|
const lines = ["## Cockpit クローンレス開発フロー", ""]
|
|
@@ -211,7 +213,7 @@ function renderRepositorySection(repositories, dirName = "") {
|
|
|
211
213
|
"# 秘密鍵を ~/.ssh/hub_deploy/ に書き出し、ssh コマンドだけ受け取る",
|
|
212
214
|
)
|
|
213
215
|
lines.push(
|
|
214
|
-
`python3
|
|
216
|
+
`python3 ~/.${cliKind === "codex" ? "codex" : "claude"}/scripts/hub_helper.py deploy_creds ${dirName || "<dir_name>"} --env ${envExample}`,
|
|
215
217
|
)
|
|
216
218
|
lines.push("```")
|
|
217
219
|
lines.push("")
|
|
@@ -235,7 +237,7 @@ function renderRepositorySection(repositories, dirName = "") {
|
|
|
235
237
|
return lines.join("\n")
|
|
236
238
|
}
|
|
237
239
|
|
|
238
|
-
function renderWithDirector(dirName, director, repositorySection = "") {
|
|
240
|
+
function renderWithDirector(dirName, director, repositorySection = "", configFilename = "CLAUDE.md") {
|
|
239
241
|
const fm = director?.frontmatter || {}
|
|
240
242
|
const client = (fm.client || "").trim()
|
|
241
243
|
const domain = (fm.domain || "").trim()
|
|
@@ -245,9 +247,9 @@ function renderWithDirector(dirName, director, repositorySection = "") {
|
|
|
245
247
|
const googleDrive = (fm.google_drive || "").trim()
|
|
246
248
|
|
|
247
249
|
const lines = [
|
|
248
|
-
|
|
250
|
+
`# ${configFilename}`,
|
|
249
251
|
"",
|
|
250
|
-
|
|
252
|
+
`This file provides guidance to ${configFilename === "AGENTS.md" ? "Codex" : "Claude Code"} when working with code in this repository.`,
|
|
251
253
|
"",
|
|
252
254
|
"## プロジェクト概要",
|
|
253
255
|
"",
|
|
@@ -291,17 +293,17 @@ function renderWithDirector(dirName, director, repositorySection = "") {
|
|
|
291
293
|
"開発手順・SSH 接続情報・横断ナレッジへのポインタなど、より詳細な内容は",
|
|
292
294
|
)
|
|
293
295
|
lines.push(
|
|
294
|
-
"
|
|
296
|
+
`このリポジトリで ${configFilename === "AGENTS.md" ? "`codex` を起動して `/init-Codex-md`" : "`claude` を起動して `/init-claude-md`"} スキルを実行してください。`,
|
|
295
297
|
)
|
|
296
298
|
lines.push("")
|
|
297
299
|
return lines.join("\n")
|
|
298
300
|
}
|
|
299
301
|
|
|
300
|
-
function renderPlaceholder(dirName, repositorySection = "") {
|
|
302
|
+
function renderPlaceholder(dirName, repositorySection = "", configFilename = "CLAUDE.md") {
|
|
301
303
|
const sections = [
|
|
302
|
-
|
|
304
|
+
`# ${configFilename}`,
|
|
303
305
|
"",
|
|
304
|
-
|
|
306
|
+
`This file provides guidance to ${configFilename === "AGENTS.md" ? "Codex" : "Claude Code"} when working with code in this repository.`,
|
|
305
307
|
"",
|
|
306
308
|
"## 関連",
|
|
307
309
|
"",
|
|
@@ -315,7 +317,7 @@ function renderPlaceholder(dirName, repositorySection = "") {
|
|
|
315
317
|
"## 初期化",
|
|
316
318
|
"",
|
|
317
319
|
"これは hub-agent の自動生成プレースホルダです。Hub director も未作成です。",
|
|
318
|
-
"
|
|
320
|
+
`詳細を生成するには、このリポジトリで ${configFilename === "AGENTS.md" ? "`codex` を起動して `/init-Codex-md`" : "`claude` を起動して `/init-claude-md`"} スキルを実行してください。`,
|
|
319
321
|
"",
|
|
320
322
|
)
|
|
321
323
|
return sections.join("\n")
|
|
@@ -344,12 +346,16 @@ export async function ensureClaudeMd({
|
|
|
344
346
|
accessToken,
|
|
345
347
|
logger,
|
|
346
348
|
fetchImpl,
|
|
349
|
+
configFilename = "CLAUDE.md",
|
|
347
350
|
} = {}) {
|
|
348
351
|
if (!targetDir) throw new Error("ensureClaudeMd requires targetDir")
|
|
349
352
|
if (!dirName) throw new Error("ensureClaudeMd requires dirName")
|
|
350
|
-
const
|
|
353
|
+
const configMdPath = path.join(
|
|
354
|
+
targetDir,
|
|
355
|
+
configFilename === "AGENTS.md" ? "AGENTS.md" : "CLAUDE.md",
|
|
356
|
+
)
|
|
351
357
|
try {
|
|
352
|
-
await fs.access(
|
|
358
|
+
await fs.access(configMdPath)
|
|
353
359
|
return { written: false, source: "existing" }
|
|
354
360
|
} catch {
|
|
355
361
|
/* not found → continue */
|
|
@@ -358,7 +364,7 @@ export async function ensureClaudeMd({
|
|
|
358
364
|
// accessToken は明示 null (テスト用) で「token を読まない」を選べるようにする。
|
|
359
365
|
// undefined のときだけ ~/.claude/.hub_token.json にフォールバックする。
|
|
360
366
|
const token =
|
|
361
|
-
accessToken === undefined ? await loadAccessToken() : accessToken || null
|
|
367
|
+
accessToken === undefined ? await loadAccessToken(configFilename) : accessToken || null
|
|
362
368
|
const url = hubUrl || DEFAULT_HUB_API
|
|
363
369
|
|
|
364
370
|
// Stage 3: Hub に正本があればそれを最優先で書き込み、メタも初期化する
|
|
@@ -374,17 +380,17 @@ export async function ensureClaudeMd({
|
|
|
374
380
|
const hubBody = hub?.claude_md || ""
|
|
375
381
|
const hubInitialized = !!hub?.claude_md_updated_at
|
|
376
382
|
if (hubInitialized && hubBody) {
|
|
377
|
-
await fs.writeFile(
|
|
383
|
+
await fs.writeFile(configMdPath, hubBody, "utf-8")
|
|
378
384
|
await writeMeta(targetDir, {
|
|
379
385
|
lastSyncedHubUpdatedAt: hub.claude_md_updated_at,
|
|
380
386
|
lastSyncedHash: hub.claude_md_hash || sha256(hubBody),
|
|
381
387
|
lastSyncedAt: nowIso(),
|
|
382
|
-
})
|
|
388
|
+
}, configFilename)
|
|
383
389
|
// 初回 pull 直後は base = Hub 本文。以降の session-end 3-way merge の入力。
|
|
384
|
-
await writeBaseBody(targetDir, hubBody)
|
|
390
|
+
await writeBaseBody(targetDir, hubBody, configFilename)
|
|
385
391
|
logger?.info?.(
|
|
386
|
-
{ dirName,
|
|
387
|
-
"wrote
|
|
392
|
+
{ dirName, configMdPath },
|
|
393
|
+
"wrote project instruction file from Hub project claude_md (initial sync)",
|
|
388
394
|
)
|
|
389
395
|
return { written: true, source: "hub" }
|
|
390
396
|
}
|
|
@@ -399,16 +405,17 @@ export async function ensureClaudeMd({
|
|
|
399
405
|
])
|
|
400
406
|
: [null, []]
|
|
401
407
|
|
|
402
|
-
const
|
|
408
|
+
const cliKind = configFilename === "AGENTS.md" ? "codex" : "claude"
|
|
409
|
+
const repositorySection = renderRepositorySection(repositories, dirName, cliKind)
|
|
403
410
|
const body = director
|
|
404
|
-
? renderWithDirector(dirName, director, repositorySection)
|
|
405
|
-
: renderPlaceholder(dirName, repositorySection)
|
|
411
|
+
? renderWithDirector(dirName, director, repositorySection, configFilename)
|
|
412
|
+
: renderPlaceholder(dirName, repositorySection, configFilename)
|
|
406
413
|
const source = director ? "director" : "placeholder"
|
|
407
414
|
|
|
408
|
-
await fs.writeFile(
|
|
415
|
+
await fs.writeFile(configMdPath, body, "utf-8")
|
|
409
416
|
logger?.info?.(
|
|
410
|
-
{ dirName,
|
|
411
|
-
"wrote
|
|
417
|
+
{ dirName, configMdPath, source },
|
|
418
|
+
"wrote project instruction file for new workspace dir",
|
|
412
419
|
)
|
|
413
420
|
return { written: true, source }
|
|
414
421
|
}
|
package/src/main.mjs
CHANGED
|
@@ -1146,6 +1146,11 @@ export async function startDaemon({ version, ptyModule, claudeSdk } = {}) {
|
|
|
1146
1146
|
// ready-for-input 判定: claude プロセスの子プロセスを内省し「Stop フック/ツール実行が
|
|
1147
1147
|
// 終わって真に入力可能になった」エッジを検出する (proc-introspect.mjs)。capture-pane は
|
|
1148
1148
|
// Stop フック実行中を観測できないため、これが「stop の早期消灯/誤フラッシュ」の根治。
|
|
1149
|
+
// TUI の tmux session_name → Claude session_id。state loop 起動より**前**に作る。
|
|
1150
|
+
// 後で作ると state loop には undefined が渡り、バックグラウンド時だけ cwd 最新 JSONL
|
|
1151
|
+
// へフォールバックして別会話の context% を表示する。
|
|
1152
|
+
ctx.tuiReboundSessions = new Map()
|
|
1153
|
+
|
|
1149
1154
|
const readinessTracker = new ReadinessTracker()
|
|
1150
1155
|
// claude.tui.bind ハンドラが「生成中か (isArmed)」を参照して、生成中 claude を rebind の
|
|
1151
1156
|
// respawn-pane -k で kill する「謎停止」を防ぐ (生成中ガード)。
|
|
@@ -1360,11 +1365,6 @@ export async function startDaemon({ version, ptyModule, claudeSdk } = {}) {
|
|
|
1360
1365
|
)
|
|
1361
1366
|
}
|
|
1362
1367
|
|
|
1363
|
-
// T04784 会話継続バインディングの冪等性ガード: tmux セッション名 → 最後に --resume で
|
|
1364
|
-
// 載せ替えた Claude session_id。TUI ビューが remount のたびに claude.tui.bind を送っても、
|
|
1365
|
-
// 同じ id への再 respawn (= claude 再起動) を抑止する。
|
|
1366
|
-
ctx.tuiReboundSessions = new Map()
|
|
1367
|
-
|
|
1368
1368
|
// session 回転通知 (T04786 後続) の多重送信ガード: 回転キー (tmux 名 or cwd) →
|
|
1369
1369
|
// 最後に push した new_session_id。閲覧ハートビートは 5s 間隔で来るため、フロントが
|
|
1370
1370
|
// 切替を完了し new id でハートビートを送り直すまでの間に来る余分なハートビートで
|
|
@@ -2295,6 +2295,9 @@ export async function pushImmediateChatState({
|
|
|
2295
2295
|
stalled: false,
|
|
2296
2296
|
turn_active: false,
|
|
2297
2297
|
cli_kind: cliKind,
|
|
2298
|
+
// context_pct がどの会話の値かを明示する。frontend は ID が変わったときだけ
|
|
2299
|
+
// 0% を採用し、同一会話の一時的な 0/null は直近の確定値を保持する。
|
|
2300
|
+
context_session_id: sessionId ?? null,
|
|
2298
2301
|
chat_session_id: sessionId ?? null,
|
|
2299
2302
|
})
|
|
2300
2303
|
}
|
|
@@ -2610,10 +2613,13 @@ export function startStateLoop({ client, plugins, logger, intervalMs, claudeBrid
|
|
|
2610
2613
|
// (cwd, cliKind) キー化により「チャット信号の cliKind == 行の cliKind」が恒真に
|
|
2611
2614
|
// なったので、常に非 null の構造値を送る方が真に厳しい (= 情報量が多い) 上位互換。
|
|
2612
2615
|
//
|
|
2613
|
-
//
|
|
2614
|
-
//
|
|
2616
|
+
// context_session_id は「この context_pct がどの会話の値か」。チャットの session/thread を
|
|
2617
|
+
// 優先し、Claude TUI 専用行では bind 済みの session_id(fresh は fresh:<request_id>)を使う。
|
|
2618
|
+
// frontend はこれを境界として、新規会話の 0% と同一会話の一時的な 0/null を区別する。
|
|
2615
2619
|
const chatCliKind = sessionCliKind
|
|
2616
|
-
const
|
|
2620
|
+
const contextSessionId =
|
|
2621
|
+
chat?.sessionId ??
|
|
2622
|
+
(sessionCliKind === "claude" ? boundSessions?.get(s.session_name) ?? null : codexThreadId)
|
|
2617
2623
|
|
|
2618
2624
|
const prev = lastByName.get(s.session_name)
|
|
2619
2625
|
if (
|
|
@@ -2627,7 +2633,7 @@ export function startStateLoop({ client, plugins, logger, intervalMs, claudeBrid
|
|
|
2627
2633
|
prev.stalled !== stalled ||
|
|
2628
2634
|
prev.turn_active !== turnActive ||
|
|
2629
2635
|
prev.cli_kind !== chatCliKind ||
|
|
2630
|
-
prev.
|
|
2636
|
+
prev.context_session_id !== contextSessionId
|
|
2631
2637
|
) {
|
|
2632
2638
|
lastByName.set(s.session_name, {
|
|
2633
2639
|
status,
|
|
@@ -2639,7 +2645,9 @@ export function startStateLoop({ client, plugins, logger, intervalMs, claudeBrid
|
|
|
2639
2645
|
stalled,
|
|
2640
2646
|
turn_active: turnActive,
|
|
2641
2647
|
cli_kind: chatCliKind,
|
|
2642
|
-
|
|
2648
|
+
context_session_id: contextSessionId,
|
|
2649
|
+
// 旧クライアント互換。新規 consumer は context_session_id を使う。
|
|
2650
|
+
chat_session_id: contextSessionId,
|
|
2643
2651
|
})
|
|
2644
2652
|
// 計装 (2026-06-19, 既定 OFF=2026-06-20): 状態変化時に値を記録する。常時 info はログ肥大の
|
|
2645
2653
|
// 主因のため HUB_AGENT_STATE_TRACE=1 のときだけ出す。childBusy/outputActive を分けて出し、
|
|
@@ -2658,7 +2666,7 @@ export function startStateLoop({ client, plugins, logger, intervalMs, claudeBrid
|
|
|
2658
2666
|
context_pct: contextPct,
|
|
2659
2667
|
turn_active: turnActive,
|
|
2660
2668
|
cli_kind: chatCliKind,
|
|
2661
|
-
|
|
2669
|
+
context_session_id: contextSessionId,
|
|
2662
2670
|
},
|
|
2663
2671
|
"session.state push",
|
|
2664
2672
|
)
|
|
@@ -2680,9 +2688,11 @@ export function startStateLoop({ client, plugins, logger, intervalMs, claudeBrid
|
|
|
2680
2688
|
turn_active: turnActive,
|
|
2681
2689
|
// cli_kind (T05945): セッション名由来の構造値 ('claude' | 'codex')。frontend の
|
|
2682
2690
|
// 単一セレクタ (useCockpitSessionDisplay) が「この push はどの CLI 行のものか」を
|
|
2683
|
-
// 判定する唯一の軸。
|
|
2691
|
+
// 判定する唯一の軸。context_session_id は context% の会話 ID。
|
|
2684
2692
|
cli_kind: chatCliKind,
|
|
2685
|
-
|
|
2693
|
+
context_session_id: contextSessionId,
|
|
2694
|
+
// 旧クライアント互換。新規 consumer は context_session_id を使う。
|
|
2695
|
+
chat_session_id: contextSessionId,
|
|
2686
2696
|
})
|
|
2687
2697
|
}
|
|
2688
2698
|
// 消滅セッションの GC (perf監査/メモリリーク対策): tmux から消えたセッションの状態 Map を
|
|
@@ -4172,7 +4182,11 @@ async function dispatch(msg, ctx) {
|
|
|
4172
4182
|
// 非 fresh (resume/auto) の bind = ユーザーが既存会話を選んだ → 保留中の +新規 defer は破棄。
|
|
4173
4183
|
if (!fresh) ctx.pendingFreshRespawn.delete(sessionName)
|
|
4174
4184
|
let rebind = { ok: false, skipped: false }
|
|
4175
|
-
|
|
4185
|
+
// 新規会話の 0% は会話切替として扱うため、request_id を送れない旧 client
|
|
4186
|
+
// 経由でも bind ごとに固有IDを作る。空の `fresh:` を再利用すると、2回目以降の
|
|
4187
|
+
// 新規会話が同一会話扱いになり、直前値が残る。
|
|
4188
|
+
const freshBindId = request_id || randomUUID()
|
|
4189
|
+
const bindKey = fresh ? `fresh:${freshBindId}` : targetId
|
|
4176
4190
|
const recordDeferredFresh = () =>
|
|
4177
4191
|
ctx.pendingFreshRespawn.set(sessionName, {
|
|
4178
4192
|
cwd,
|
|
@@ -4281,6 +4295,19 @@ async function dispatch(msg, ctx) {
|
|
|
4281
4295
|
noCache: true,
|
|
4282
4296
|
logger,
|
|
4283
4297
|
})
|
|
4298
|
+
// bind が確定している復帰は、capture-pane の見た目や cwd 最新ファイルでなく
|
|
4299
|
+
// **指定 session_id の JSONL** から context% を即時解決する。fresh はまだ実 ID
|
|
4300
|
+
// が無いため 0% と fresh:<request_id> を対にして送る。
|
|
4301
|
+
const contextSessionId = fresh ? bindKey : targetId
|
|
4302
|
+
const boundContext =
|
|
4303
|
+
targetId && cwd
|
|
4304
|
+
? await jsonlContextForCwd(cwd, targetId).catch(() => null)
|
|
4305
|
+
: null
|
|
4306
|
+
const contextPct = fresh
|
|
4307
|
+
? 0
|
|
4308
|
+
: typeof boundContext?.percent === "number"
|
|
4309
|
+
? boundContext.percent
|
|
4310
|
+
: snap.context_pct
|
|
4284
4311
|
// 【問題1 根治 2026-06-22】配る status はプロセス実体の busy を権威に解決する
|
|
4285
4312
|
// (resolveBindSnapshotStatus)。capture(SPINNER_LINE_RE)の偽陽性に勝たせる。
|
|
4286
4313
|
// - busy=true (ターン進行中): capture gap で snap.status が waiting に化けても
|
|
@@ -4296,7 +4323,8 @@ async function dispatch(msg, ctx) {
|
|
|
4296
4323
|
type: "session.state",
|
|
4297
4324
|
session_name: sessionName,
|
|
4298
4325
|
status: snapStatus,
|
|
4299
|
-
context_pct:
|
|
4326
|
+
context_pct: contextPct,
|
|
4327
|
+
context_session_id: contextSessionId ?? null,
|
|
4300
4328
|
permission_mode: snap.permission_mode,
|
|
4301
4329
|
stable:
|
|
4302
4330
|
!busy && snapStatus !== "processing" && snap.stable === true,
|
package/src/tmux.mjs
CHANGED
|
@@ -78,6 +78,18 @@ export function isUnderHubProjectsBase(p) {
|
|
|
78
78
|
return true
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Hub workspace またはその worktree から、案件指示ファイルを置く親 workspace を返す。
|
|
83
|
+
* worktree 内の AGENTS.md / CLAUDE.md を別管理すると Hub 正本が分岐するため、
|
|
84
|
+
* `<workspace>/.claude/worktrees/<name>` で起動しても常に `<workspace>` を対象にする。
|
|
85
|
+
*/
|
|
86
|
+
export function projectInstructionDir(p) {
|
|
87
|
+
if (!isUnderHubProjectsBase(p)) return path.resolve(p)
|
|
88
|
+
const base = path.resolve(hubProjectsBase())
|
|
89
|
+
const [workspace] = path.relative(base, path.resolve(p)).split(path.sep)
|
|
90
|
+
return path.join(base, workspace)
|
|
91
|
+
}
|
|
92
|
+
|
|
81
93
|
/**
|
|
82
94
|
* git branch 名から worktree dir 名 (= tmux session 名) を導出する。
|
|
83
95
|
* 例: "feature/multi-area" -> "feature-multi-area"
|
|
@@ -788,15 +800,18 @@ export async function createSession(name, cwd, opts = {}) {
|
|
|
788
800
|
// 何もしない(テナント標準 CLAUDE.md の運用を尊重する)。
|
|
789
801
|
// どちらも失敗しても session 起動自体は止めない (warn のみ)。
|
|
790
802
|
if (isUnderHubProjectsBase(resolvedCwd)) {
|
|
791
|
-
const
|
|
803
|
+
const instructionDir = projectInstructionDir(resolvedCwd)
|
|
804
|
+
const dirName = path.basename(instructionDir)
|
|
805
|
+
const configFilename = opts.cliKind === "codex" ? "AGENTS.md" : "CLAUDE.md"
|
|
792
806
|
try {
|
|
793
807
|
await ensureClaudeMd({
|
|
794
|
-
targetDir:
|
|
808
|
+
targetDir: instructionDir,
|
|
795
809
|
dirName,
|
|
796
810
|
hubUrl: opts.hubUrl,
|
|
797
811
|
accessToken: opts.hubAccessToken,
|
|
798
812
|
logger: opts.logger,
|
|
799
813
|
fetchImpl: opts.fetchImpl,
|
|
814
|
+
configFilename,
|
|
800
815
|
})
|
|
801
816
|
} catch (err) {
|
|
802
817
|
opts.logger?.warn?.(
|
|
@@ -806,12 +821,13 @@ export async function createSession(name, cwd, opts = {}) {
|
|
|
806
821
|
}
|
|
807
822
|
try {
|
|
808
823
|
const syncResult = await syncClaudeMdWithHub({
|
|
809
|
-
targetDir:
|
|
824
|
+
targetDir: instructionDir,
|
|
810
825
|
dirName,
|
|
811
826
|
hubUrl: opts.hubUrl,
|
|
812
827
|
accessToken: opts.hubAccessToken,
|
|
813
828
|
logger: opts.logger,
|
|
814
829
|
fetchImpl: opts.fetchImpl,
|
|
830
|
+
configFilename,
|
|
815
831
|
})
|
|
816
832
|
opts.logger?.debug?.(
|
|
817
833
|
{ session: name, dirName, sync: syncResult },
|