@cyber-dash-tech/revela 0.18.3 → 0.18.5
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/README.md +7 -5
- package/README.zh-CN.md +7 -5
- package/lib/commands/refine.ts +1 -1
- package/lib/decks-state.ts +1 -0
- package/lib/document-materials/extract.ts +25 -20
- package/lib/material-intake.ts +9 -4
- package/lib/narrative-vault/constants.ts +1 -1
- package/lib/narrative-vault/paths.ts +7 -2
- package/lib/refine/comment-requests.ts +1 -1
- package/lib/refine/prompt-bridge.ts +94 -25
- package/lib/refine/review-comments.ts +203 -0
- package/lib/refine/server.ts +1073 -216
- package/lib/runtime/index.ts +3 -2
- package/lib/workspace-meta.ts +32 -0
- package/package.json +1 -1
- package/plugin.ts +4 -3
- package/plugins/revela/.mcp.json +1 -1
- package/plugins/revela/hooks/revela_guard.ts +2 -2
- package/plugins/revela/mcp/revela-server.ts +1 -1
- package/plugins/revela/skills/revela-export/SKILL.md +30 -1
- package/plugins/revela/skills/revela-helper/SKILL.md +48 -0
- package/plugins/revela/skills/revela-make-deck/SKILL.md +93 -15
- package/plugins/revela/skills/revela-research/SKILL.md +57 -15
- package/plugins/revela/skills/{revela-review-deck → revela-review}/SKILL.md +28 -7
- package/tools/workspace-scan.ts +1 -1
- package/plugins/revela/skills/revela-design/SKILL.md +0 -46
- package/plugins/revela/skills/revela-domain/SKILL.md +0 -30
- package/plugins/revela/skills/revela-init/SKILL.md +0 -31
- package/plugins/revela/skills/revela-upgrade/SKILL.md +0 -33
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**English** | [中文](README.zh-CN.md)
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/@cyber-dash-tech/revela) [](LICENSE) [](https://www.npmjs.com/package/@cyber-dash-tech/revela) [](LICENSE) [](tests/) [](https://opencode.ai) [](https://bun.sh)
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
8
|
<img src="assets/img/logo.png" alt="Revela" width="560" />
|
|
@@ -34,8 +34,8 @@ To install globally, add the same entry to `~/.config/opencode/opencode.json`.
|
|
|
34
34
|
Requirements:
|
|
35
35
|
|
|
36
36
|
- The Codex CLI must be installed and the `codex` command must be available in your shell.
|
|
37
|
-
- Your environment must be able to run `npx`; Revela uses `npx -y @cyber-dash-tech/revela@0.18.
|
|
38
|
-
- For interactive Review actions, `codex exec` must also work because the Review UI uses it
|
|
37
|
+
- Your environment must be able to run `npx`; Revela uses `npx -y @cyber-dash-tech/revela@0.18.5 mcp` to start the MCP server.
|
|
38
|
+
- For interactive Review Apply actions, `codex exec` must also work because the Review UI uses it after saved comments are applied.
|
|
39
39
|
|
|
40
40
|
Optional preflight:
|
|
41
41
|
|
|
@@ -55,16 +55,18 @@ npm_config_cache=/tmp/revela-npm-cache bun run smoke:mcp-pack
|
|
|
55
55
|
Install Revela through the Codex Git marketplace:
|
|
56
56
|
|
|
57
57
|
```bash
|
|
58
|
-
codex plugin marketplace add https://github.com/cyber-dash-tech/revela --ref v0.18.
|
|
58
|
+
codex plugin marketplace add https://github.com/cyber-dash-tech/revela --ref v0.18.5
|
|
59
59
|
codex plugin add revela@revela
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
The Git marketplace install provides the Codex plugin shell, skills, hooks, and MCP configuration. When Codex starts the Revela MCP server for the first time, it runs `npx -y @cyber-dash-tech/revela@0.18.
|
|
62
|
+
The Git marketplace install provides the Codex plugin shell, skills, hooks, and MCP configuration. When Codex starts the Revela MCP server for the first time, it runs `npx -y @cyber-dash-tech/revela@0.18.5 mcp` so npm can fetch the published package and its dependencies.
|
|
63
63
|
|
|
64
64
|
You do not need to run `bun install` inside the Codex marketplace clone.
|
|
65
65
|
|
|
66
66
|
Start a new Codex thread after installing so Codex loads the Revela skills, MCP tools, and hooks.
|
|
67
67
|
|
|
68
|
+
Codex uses five Revela skills: `revela-helper` for status and active design/domain, `revela-research` for local and web research saved under `researches/`, `revela-make-deck` for design-aware `deck-plan.md` plus `decks/*.html`, `revela-review` for the Review UI, and `revela-export` for PDF/PPTX/PNG.
|
|
69
|
+
|
|
68
70
|
For release-aligned local validation, run `bun run smoke:mcp-pack`. It packs the current checkout to a temporary npm tarball and starts the MCP server through `npx`, matching the published Codex launcher path without requiring a registry publish.
|
|
69
71
|
|
|
70
72
|
#### Codex Upgrade
|
package/README.zh-CN.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | **中文**
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/@cyber-dash-tech/revela) [](LICENSE) [](https://www.npmjs.com/package/@cyber-dash-tech/revela) [](LICENSE) [](tests/) [](https://opencode.ai) [](https://bun.sh)
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
8
|
<img src="assets/img/logo.png" alt="Revela" width="560" />
|
|
@@ -34,8 +34,8 @@ Revela 可在 [OpenCode](https://opencode.ai) 和 Codex 中使用,把来源材
|
|
|
34
34
|
环境要求:
|
|
35
35
|
|
|
36
36
|
- 需要已安装 Codex CLI,并且 shell 中可以执行 `codex`。
|
|
37
|
-
- 环境中需要可以执行 `npx`;Revela 会用 `npx -y @cyber-dash-tech/revela@0.18.
|
|
38
|
-
- 如果使用 Review UI 的
|
|
37
|
+
- 环境中需要可以执行 `npx`;Revela 会用 `npx -y @cyber-dash-tech/revela@0.18.5 mcp` 启动 MCP server。
|
|
38
|
+
- 如果使用 Review UI 的 Apply,需要 `codex exec` 可用;评论会先保存,点击 Apply 后才执行修复。
|
|
39
39
|
|
|
40
40
|
可选的安装前检查:
|
|
41
41
|
|
|
@@ -55,16 +55,18 @@ npm_config_cache=/tmp/revela-npm-cache bun run smoke:mcp-pack
|
|
|
55
55
|
通过 Codex Git marketplace 安装 Revela:
|
|
56
56
|
|
|
57
57
|
```bash
|
|
58
|
-
codex plugin marketplace add https://github.com/cyber-dash-tech/revela --ref v0.18.
|
|
58
|
+
codex plugin marketplace add https://github.com/cyber-dash-tech/revela --ref v0.18.5
|
|
59
59
|
codex plugin add revela@revela
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
Git marketplace 安装的是 Codex plugin 壳、skills、hooks 和 MCP 配置。Codex 第一次启动 Revela MCP server 时,会运行 `npx -y @cyber-dash-tech/revela@0.18.
|
|
62
|
+
Git marketplace 安装的是 Codex plugin 壳、skills、hooks 和 MCP 配置。Codex 第一次启动 Revela MCP server 时,会运行 `npx -y @cyber-dash-tech/revela@0.18.5 mcp`,由 npm 获取已发布 package 及其 dependencies。
|
|
63
63
|
|
|
64
64
|
不需要在 Codex marketplace clone 里运行 `bun install`。
|
|
65
65
|
|
|
66
66
|
安装后开启一个新的 Codex thread,让 Codex 加载 Revela 的 skills、MCP tools 和 hooks。
|
|
67
67
|
|
|
68
|
+
Codex 使用五个 Revela skills:`revela-helper` 查看状态和 active design/domain,`revela-research` 调研本地与网络资料并保存到 `researches/`,`revela-make-deck` 基于 design 工具生成 `deck-plan.md` 和 `decks/*.html`,`revela-review` 打开 Review UI,`revela-export` 导出 PDF/PPTX/PNG。
|
|
69
|
+
|
|
68
70
|
如果要按发布路径做本地验证,运行 `bun run smoke:mcp-pack`。它会把当前 checkout 打成临时 npm tarball,再通过 `npx` 启动 MCP server,不需要先发布到 registry。
|
|
69
71
|
|
|
70
72
|
#### Codex 升级
|
package/lib/commands/refine.ts
CHANGED
|
@@ -18,7 +18,7 @@ export async function handleRefine(
|
|
|
18
18
|
`File: \`${result.deck.file}\`\n` +
|
|
19
19
|
`${result.stateNote}\n` +
|
|
20
20
|
`URL: ${result.url}\n\n` +
|
|
21
|
-
`Use Ctrl/Cmd-click in the browser to reference deck elements. The Comment tab
|
|
21
|
+
`Use Ctrl/Cmd-click in the browser to reference deck elements. The Comment tab saves targeted comments; use Apply on a saved comment to run deck edits.`
|
|
22
22
|
)
|
|
23
23
|
} catch (e: any) {
|
|
24
24
|
await send(`**Review failed:** ${e.message || String(e)}`)
|
package/lib/decks-state.ts
CHANGED
|
@@ -1651,6 +1651,7 @@ function isIgnorableSourceMaterial(path: string): boolean {
|
|
|
1651
1651
|
normalized.startsWith("decks/") ||
|
|
1652
1652
|
normalized.startsWith("researches/") ||
|
|
1653
1653
|
normalized.startsWith("assets/") ||
|
|
1654
|
+
normalized.startsWith(".revela/") ||
|
|
1654
1655
|
normalized.startsWith(".opencode/"),
|
|
1655
1656
|
)
|
|
1656
1657
|
}
|
|
@@ -11,6 +11,7 @@ import { extractXlsx } from "../read-hooks/extractors/xlsx"
|
|
|
11
11
|
import { hasDecksState, readDecksState, writeDecksState } from "../decks-state"
|
|
12
12
|
import { computeSourceFingerprint, sourceMaterialMetadata, upsertSourceMaterial } from "../source-materials"
|
|
13
13
|
import { recordWorkspaceAction } from "../workspace-state/actions"
|
|
14
|
+
import { existingWorkspaceMetaPath, workspaceMetaPath } from "../workspace-meta"
|
|
14
15
|
|
|
15
16
|
export type DocumentMaterial = {
|
|
16
17
|
path: string
|
|
@@ -852,7 +853,7 @@ async function extractPdfImages(buf: Buffer, cacheDir: string, workspaceDir: str
|
|
|
852
853
|
async function processPdfFile(filePath: string, workspaceDir: string): Promise<DocumentMaterialsResult> {
|
|
853
854
|
const relativeSource = workspaceRelative(filePath, workspaceDir)
|
|
854
855
|
const fingerprint = buildFingerprint(filePath)
|
|
855
|
-
const cacheDir =
|
|
856
|
+
const cacheDir = existingWorkspaceMetaPath(workspaceDir, "doc-materials", fingerprint)
|
|
856
857
|
const manifestPath = join(cacheDir, "manifest.json")
|
|
857
858
|
|
|
858
859
|
if (existsSync(manifestPath)) {
|
|
@@ -874,19 +875,21 @@ async function processPdfFile(filePath: string, workspaceDir: string): Promise<D
|
|
|
874
875
|
}
|
|
875
876
|
}
|
|
876
877
|
|
|
877
|
-
|
|
878
|
-
|
|
878
|
+
const writeCacheDir = workspaceMetaPath(workspaceDir, "doc-materials", fingerprint)
|
|
879
|
+
const writeManifestPath = join(writeCacheDir, "manifest.json")
|
|
880
|
+
mkdirSync(join(writeCacheDir, "images"), { recursive: true })
|
|
881
|
+
mkdirSync(join(writeCacheDir, "tables"), { recursive: true })
|
|
879
882
|
|
|
880
883
|
const buf = readFileSync(filePath)
|
|
881
884
|
const text = await extractPdfText(buf)
|
|
882
|
-
const textPath = join(
|
|
885
|
+
const textPath = join(writeCacheDir, "text.txt")
|
|
883
886
|
writeFileSync(textPath, `[Extracted from: ${basename(filePath)}]\n\n${text}`, "utf-8")
|
|
884
887
|
|
|
885
|
-
const images = await extractPdfImages(buf,
|
|
886
|
-
const relativeManifestPath = workspaceRelative(
|
|
888
|
+
const images = await extractPdfImages(buf, writeCacheDir, workspaceDir)
|
|
889
|
+
const relativeManifestPath = workspaceRelative(writeManifestPath, workspaceDir)
|
|
887
890
|
const relativeTextPath = workspaceRelative(textPath, workspaceDir)
|
|
888
891
|
const readViewPath = writeReadView({
|
|
889
|
-
cacheDir,
|
|
892
|
+
cacheDir: writeCacheDir,
|
|
890
893
|
workspaceDir,
|
|
891
894
|
source: relativeSource,
|
|
892
895
|
type: "pdf",
|
|
@@ -905,7 +908,7 @@ async function processPdfFile(filePath: string, workspaceDir: string): Promise<D
|
|
|
905
908
|
cache_status: "miss",
|
|
906
909
|
source: relativeSource,
|
|
907
910
|
type: "pdf",
|
|
908
|
-
cache_dir: workspaceRelative(
|
|
911
|
+
cache_dir: workspaceRelative(writeCacheDir, workspaceDir),
|
|
909
912
|
manifest_path: relativeManifestPath,
|
|
910
913
|
text_path: relativeTextPath,
|
|
911
914
|
read_view_path: readViewPath,
|
|
@@ -929,14 +932,14 @@ async function processPdfFile(filePath: string, workspaceDir: string): Promise<D
|
|
|
929
932
|
tables: [],
|
|
930
933
|
}
|
|
931
934
|
|
|
932
|
-
writeFileSync(
|
|
935
|
+
writeFileSync(writeManifestPath, JSON.stringify(manifest, null, 2), "utf-8")
|
|
933
936
|
return result
|
|
934
937
|
}
|
|
935
938
|
|
|
936
939
|
async function processOfficeFile(filePath: string, workspaceDir: string, type: SupportedType): Promise<DocumentMaterialsResult> {
|
|
937
940
|
const relativeSource = workspaceRelative(filePath, workspaceDir)
|
|
938
941
|
const fingerprint = buildFingerprint(filePath)
|
|
939
|
-
const cacheDir =
|
|
942
|
+
const cacheDir = existingWorkspaceMetaPath(workspaceDir, "doc-materials", fingerprint)
|
|
940
943
|
const manifestPath = join(cacheDir, "manifest.json")
|
|
941
944
|
|
|
942
945
|
if (existsSync(manifestPath)) {
|
|
@@ -958,8 +961,10 @@ async function processOfficeFile(filePath: string, workspaceDir: string, type: S
|
|
|
958
961
|
}
|
|
959
962
|
}
|
|
960
963
|
|
|
961
|
-
|
|
962
|
-
|
|
964
|
+
const writeCacheDir = workspaceMetaPath(workspaceDir, "doc-materials", fingerprint)
|
|
965
|
+
const writeManifestPath = join(writeCacheDir, "manifest.json")
|
|
966
|
+
mkdirSync(join(writeCacheDir, "images"), { recursive: true })
|
|
967
|
+
mkdirSync(join(writeCacheDir, "tables"), { recursive: true })
|
|
963
968
|
|
|
964
969
|
const buf = readFileSync(filePath)
|
|
965
970
|
const files = unzipSync(new Uint8Array(buf))
|
|
@@ -970,26 +975,26 @@ async function processOfficeFile(filePath: string, workspaceDir: string, type: S
|
|
|
970
975
|
? await extractDocx(buf)
|
|
971
976
|
: await extractXlsx(buf)
|
|
972
977
|
|
|
973
|
-
const textPath = join(
|
|
978
|
+
const textPath = join(writeCacheDir, "text.txt")
|
|
974
979
|
writeFileSync(textPath, `[Extracted from: ${basename(filePath)}]\n\n${text}`, "utf-8")
|
|
975
980
|
|
|
976
981
|
const pptxAssets = type === "pptx"
|
|
977
|
-
? extractPptxImages(files,
|
|
982
|
+
? extractPptxImages(files, writeCacheDir, workspaceDir)
|
|
978
983
|
: null
|
|
979
984
|
const images = type === "pptx"
|
|
980
985
|
? pptxAssets!.images
|
|
981
986
|
: type === "docx"
|
|
982
|
-
? extractDocxImages(files,
|
|
983
|
-
: extractXlsxImages(files,
|
|
987
|
+
? extractDocxImages(files, writeCacheDir, workspaceDir)
|
|
988
|
+
: extractXlsxImages(files, writeCacheDir, workspaceDir)
|
|
984
989
|
const slides = type === "pptx"
|
|
985
990
|
? extractPptxSlides(files, images, pptxAssets!.skipped_assets)
|
|
986
991
|
: undefined
|
|
987
|
-
const relativeManifestPath = workspaceRelative(
|
|
992
|
+
const relativeManifestPath = workspaceRelative(writeManifestPath, workspaceDir)
|
|
988
993
|
const relativeTextPath = workspaceRelative(textPath, workspaceDir)
|
|
989
994
|
const tables = extractTables(type, relativeTextPath)
|
|
990
995
|
const skippedAssets = pptxAssets?.skipped_assets ?? []
|
|
991
996
|
const readViewPath = writeReadView({
|
|
992
|
-
cacheDir,
|
|
997
|
+
cacheDir: writeCacheDir,
|
|
993
998
|
workspaceDir,
|
|
994
999
|
source: relativeSource,
|
|
995
1000
|
type,
|
|
@@ -1008,7 +1013,7 @@ async function processOfficeFile(filePath: string, workspaceDir: string, type: S
|
|
|
1008
1013
|
cache_status: "miss",
|
|
1009
1014
|
source: relativeSource,
|
|
1010
1015
|
type,
|
|
1011
|
-
cache_dir: workspaceRelative(
|
|
1016
|
+
cache_dir: workspaceRelative(writeCacheDir, workspaceDir),
|
|
1012
1017
|
manifest_path: relativeManifestPath,
|
|
1013
1018
|
text_path: relativeTextPath,
|
|
1014
1019
|
read_view_path: readViewPath,
|
|
@@ -1032,7 +1037,7 @@ async function processOfficeFile(filePath: string, workspaceDir: string, type: S
|
|
|
1032
1037
|
tables: result.tables ?? [],
|
|
1033
1038
|
}
|
|
1034
1039
|
|
|
1035
|
-
writeFileSync(
|
|
1040
|
+
writeFileSync(writeManifestPath, JSON.stringify(manifest, null, 2), "utf-8")
|
|
1036
1041
|
return result
|
|
1037
1042
|
}
|
|
1038
1043
|
|
package/lib/material-intake.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { basename, extname, isAbsolute, join, relative, resolve, sep } from "pat
|
|
|
3
3
|
import { extractDocumentMaterials, type DocumentMaterialsResult } from "./document-materials/extract"
|
|
4
4
|
import { sourceMaterialMetadata, sourceMaterialType } from "./source-materials"
|
|
5
5
|
import type { SourceMaterial } from "./decks-state"
|
|
6
|
+
import { existingWorkspaceMetaPath, workspaceMetaPath } from "./workspace-meta"
|
|
6
7
|
|
|
7
8
|
export type MaterialIntakeStatus =
|
|
8
9
|
| "scanned"
|
|
@@ -107,24 +108,28 @@ export interface CheckMaterialIntakeResult {
|
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
const DOC_EXTENSIONS = new Set([".pdf", ".docx", ".doc", ".xlsx", ".xls", ".pptx", ".ppt", ".csv", ".md", ".txt"])
|
|
110
|
-
const EXCLUDE_DIRS = new Set(["node_modules", ".git", "dist", ".opencode", "researches", "revela-narrative", "designs", "domains"])
|
|
111
|
+
const EXCLUDE_DIRS = new Set(["node_modules", ".git", "dist", ".opencode", ".revela", "researches", "revela-narrative", "designs", "domains"])
|
|
111
112
|
const EXCLUDE_FILENAMES = new Set(["AGENTS.md", "DECKS.md", "README.md", "README.zh-CN.md"])
|
|
112
113
|
const EXTRACTION_EXTENSIONS = new Set(["pdf", "ppt", "pptx", "doc", "docx", "xls", "xlsx"])
|
|
113
114
|
const SUPPORTED_EXTRACTION_EXTENSIONS = new Set(["pdf", "pptx", "docx", "xlsx"])
|
|
114
115
|
|
|
115
116
|
export function materialRegistryPath(workspaceRoot: string): string {
|
|
116
|
-
return
|
|
117
|
+
return workspaceMetaPath(workspaceRoot, "material-intake", "registry.json")
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function existingMaterialRegistryPath(workspaceRoot: string): string {
|
|
121
|
+
return existingWorkspaceMetaPath(workspaceRoot, "material-intake", "registry.json")
|
|
117
122
|
}
|
|
118
123
|
|
|
119
124
|
export function readMaterialRegistry(workspaceRoot: string): MaterialRegistry {
|
|
120
|
-
const path =
|
|
125
|
+
const path = existingMaterialRegistryPath(workspaceRoot)
|
|
121
126
|
if (!existsSync(path)) return { version: 1, updatedAt: new Date(0).toISOString(), sources: [] }
|
|
122
127
|
return JSON.parse(readFileSync(path, "utf-8")) as MaterialRegistry
|
|
123
128
|
}
|
|
124
129
|
|
|
125
130
|
export function writeMaterialRegistry(workspaceRoot: string, registry: MaterialRegistry): string {
|
|
126
131
|
const path = materialRegistryPath(workspaceRoot)
|
|
127
|
-
mkdirSync(join(workspaceRoot, ".
|
|
132
|
+
mkdirSync(join(workspaceRoot, ".revela", "material-intake"), { recursive: true })
|
|
128
133
|
writeFileSync(path, JSON.stringify({ ...registry, updatedAt: new Date().toISOString() }, null, 2), "utf-8")
|
|
129
134
|
return workspaceRelative(path, workspaceRoot)
|
|
130
135
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const NARRATIVE_VAULT_DIR = "revela-narrative"
|
|
2
|
-
export const NARRATIVE_VAULT_CACHE_DIR = ".
|
|
2
|
+
export const NARRATIVE_VAULT_CACHE_DIR = ".revela/narrative-cache"
|
|
3
3
|
|
|
4
4
|
export const NARRATIVE_VAULT_NODE_DIRS = ["claims", "evidence", "objections", "risks", "research-gaps"] as const
|
|
5
5
|
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import { existsSync } from "fs"
|
|
2
2
|
import { join } from "path"
|
|
3
|
-
import {
|
|
3
|
+
import { existingWorkspaceMetaPath, workspaceMetaPath } from "../workspace-meta"
|
|
4
|
+
import { NARRATIVE_VAULT_DIR } from "./constants"
|
|
4
5
|
|
|
5
6
|
export function narrativeVaultPath(workspaceRoot: string): string {
|
|
6
7
|
return join(workspaceRoot, NARRATIVE_VAULT_DIR)
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
export function narrativeVaultCachePath(workspaceRoot: string): string {
|
|
10
|
-
return
|
|
11
|
+
return workspaceMetaPath(workspaceRoot, "narrative-cache")
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function existingNarrativeVaultCachePath(workspaceRoot: string): string {
|
|
15
|
+
return existingWorkspaceMetaPath(workspaceRoot, "narrative-cache")
|
|
11
16
|
}
|
|
12
17
|
|
|
13
18
|
export function hasNarrativeVault(workspaceRoot: string): boolean {
|
|
@@ -13,7 +13,7 @@ export interface PendingCommentRequest {
|
|
|
13
13
|
raw?: string
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
const REQUEST_TTL_MS =
|
|
16
|
+
const REQUEST_TTL_MS = 360 * 1000
|
|
17
17
|
const requests = new Map<string, PendingCommentRequest>()
|
|
18
18
|
const subscribers = new Map<string, Set<(event: ReviewBridgeEvent) => void>>()
|
|
19
19
|
|
|
@@ -36,6 +36,9 @@ export interface CodexExecRunResult {
|
|
|
36
36
|
stderr: string
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
const DEFAULT_COMMENT_TIMEOUT_MS = 300_000
|
|
40
|
+
const DEFAULT_INSPECT_TIMEOUT_MS = 120_000
|
|
41
|
+
|
|
39
42
|
export type CodexExecRunner = (input: {
|
|
40
43
|
action: ReviewPromptAction
|
|
41
44
|
prompt: string
|
|
@@ -71,39 +74,57 @@ export function createOpenCodeReviewPromptBridge(client: any, sessionID: string)
|
|
|
71
74
|
export function createCodexExecReviewPromptBridge(options: {
|
|
72
75
|
runner?: CodexExecRunner
|
|
73
76
|
timeoutMs?: number
|
|
77
|
+
heartbeatMs?: number
|
|
74
78
|
} = {}): ReviewPromptBridge {
|
|
75
79
|
const runner = options.runner ?? runCodexExec
|
|
76
|
-
const
|
|
80
|
+
const heartbeatMs = options.heartbeatMs ?? 10_000
|
|
77
81
|
return {
|
|
78
82
|
kind: "codex-exec",
|
|
79
83
|
async send(input) {
|
|
80
84
|
const sandboxMode = input.action === "comment" ? "workspace-write" : "read-only"
|
|
85
|
+
const timeoutMs = input.timeoutMs ?? options.timeoutMs ?? (input.action === "comment" ? DEFAULT_COMMENT_TIMEOUT_MS : DEFAULT_INSPECT_TIMEOUT_MS)
|
|
81
86
|
input.onEvent?.(bridgeEvent("started", "Starting Codex..."))
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
87
|
+
const startedAt = Date.now()
|
|
88
|
+
const heartbeat = input.onEvent
|
|
89
|
+
? setInterval(() => {
|
|
90
|
+
const elapsedSeconds = Math.max(1, Math.round((Date.now() - startedAt) / 1000))
|
|
91
|
+
input.onEvent?.(bridgeEvent("codex_event", "Codex is still working...", `elapsedSeconds=${elapsedSeconds}`))
|
|
92
|
+
}, heartbeatMs)
|
|
93
|
+
: undefined
|
|
94
|
+
let output: CodexExecRunResult
|
|
95
|
+
try {
|
|
96
|
+
output = await runner({
|
|
97
|
+
action: input.action,
|
|
98
|
+
prompt: input.prompt,
|
|
99
|
+
workspaceRoot: input.workspaceRoot,
|
|
100
|
+
timeoutMs,
|
|
101
|
+
sandboxMode,
|
|
102
|
+
skipGitRepoCheck: true,
|
|
103
|
+
onEvent: input.onEvent,
|
|
104
|
+
})
|
|
105
|
+
} finally {
|
|
106
|
+
if (heartbeat) clearInterval(heartbeat)
|
|
107
|
+
}
|
|
91
108
|
const raw = [output.stdout, output.stderr].filter(Boolean).join("\n")
|
|
92
|
-
if (
|
|
93
|
-
input.onEvent?.(bridgeEvent("failed",
|
|
109
|
+
if (input.action === "comment" && isCodexWriteBlocked(raw)) {
|
|
110
|
+
input.onEvent?.(bridgeEvent("failed", "codex exec could not write the deck because its sandbox blocked file changes.", boundedTail(raw)))
|
|
94
111
|
return {
|
|
95
112
|
ok: false,
|
|
96
113
|
status: "failed",
|
|
97
|
-
error:
|
|
114
|
+
error: "codex exec could not write the deck because its sandbox blocked file changes.",
|
|
98
115
|
raw,
|
|
99
116
|
}
|
|
100
117
|
}
|
|
101
|
-
if (
|
|
102
|
-
input.
|
|
118
|
+
if (output.exitCode !== 0) {
|
|
119
|
+
if (input.action === "comment" && output.exitCode === 124 && hasTrustedCodexCompletion(output.stdout)) {
|
|
120
|
+
input.onEvent?.(bridgeEvent("completed", "Codex completed."))
|
|
121
|
+
return { ok: true, status: "completed", raw }
|
|
122
|
+
}
|
|
123
|
+
input.onEvent?.(bridgeEvent("failed", `codex exec failed with exit code ${output.exitCode ?? "unknown"}.`, boundedTail(raw)))
|
|
103
124
|
return {
|
|
104
125
|
ok: false,
|
|
105
126
|
status: "failed",
|
|
106
|
-
error:
|
|
127
|
+
error: `codex exec failed with exit code ${output.exitCode ?? "unknown"}.`,
|
|
107
128
|
raw,
|
|
108
129
|
}
|
|
109
130
|
}
|
|
@@ -146,6 +167,7 @@ async function runCodexExec(input: {
|
|
|
146
167
|
let stdout = ""
|
|
147
168
|
let stderr = ""
|
|
148
169
|
let stdoutLineBuffer = ""
|
|
170
|
+
let sawTrustedCompletion = false
|
|
149
171
|
let resolved = false
|
|
150
172
|
const resolveOnce = (output: CodexExecRunResult) => {
|
|
151
173
|
if (resolved) return
|
|
@@ -155,7 +177,11 @@ async function runCodexExec(input: {
|
|
|
155
177
|
const timer = setTimeout(() => {
|
|
156
178
|
child.kill()
|
|
157
179
|
const nextStderr = `${stderr}${stderr ? "\n" : ""}codex exec timed out after ${input.timeoutMs}ms.`
|
|
158
|
-
input.
|
|
180
|
+
if (input.action === "comment" && sawTrustedCompletion) {
|
|
181
|
+
input.onEvent?.(bridgeEvent("completed", "Codex completed."))
|
|
182
|
+
} else {
|
|
183
|
+
input.onEvent?.(bridgeEvent("timeout", "Codex timed out before completing.", boundedTail(nextStderr)))
|
|
184
|
+
}
|
|
159
185
|
resolveOnce({
|
|
160
186
|
exitCode: 124,
|
|
161
187
|
stdout,
|
|
@@ -165,7 +191,9 @@ async function runCodexExec(input: {
|
|
|
165
191
|
child.stdout?.on("data", (chunk) => {
|
|
166
192
|
const text = chunk.toString()
|
|
167
193
|
stdout += text
|
|
168
|
-
|
|
194
|
+
const progress = emitCodexJsonProgress(stdoutLineBuffer + text, input.action, input.onEvent)
|
|
195
|
+
stdoutLineBuffer = progress.remainder
|
|
196
|
+
sawTrustedCompletion = sawTrustedCompletion || progress.sawTrustedCompletion
|
|
169
197
|
})
|
|
170
198
|
child.stderr?.on("data", (chunk) => {
|
|
171
199
|
const text = chunk.toString()
|
|
@@ -179,17 +207,20 @@ async function runCodexExec(input: {
|
|
|
179
207
|
})
|
|
180
208
|
child.on("close", (code) => {
|
|
181
209
|
clearTimeout(timer)
|
|
182
|
-
emitCodexJsonProgress(`${stdoutLineBuffer}\n`, input.action, input.onEvent)
|
|
210
|
+
const progress = emitCodexJsonProgress(`${stdoutLineBuffer}\n`, input.action, input.onEvent)
|
|
211
|
+
sawTrustedCompletion = sawTrustedCompletion || progress.sawTrustedCompletion
|
|
183
212
|
resolveOnce({ exitCode: code, stdout, stderr })
|
|
184
213
|
})
|
|
185
214
|
})
|
|
186
215
|
}
|
|
187
216
|
|
|
188
|
-
function emitCodexJsonProgress(buffer: string, action: ReviewPromptAction, onEvent?: (event: ReviewBridgeEvent) => void): string {
|
|
217
|
+
function emitCodexJsonProgress(buffer: string, action: ReviewPromptAction, onEvent?: (event: ReviewBridgeEvent) => void): { remainder: string; sawTrustedCompletion: boolean } {
|
|
189
218
|
const lines = buffer.split(/\r?\n/)
|
|
190
219
|
const remainder = lines.pop() ?? ""
|
|
220
|
+
let sawTrustedCompletion = false
|
|
191
221
|
for (const line of lines) {
|
|
192
222
|
const parsed = parseJson(line)
|
|
223
|
+
sawTrustedCompletion = sawTrustedCompletion || isTrustedCodexCompletionRecord(parsed)
|
|
193
224
|
const message = codexProgressMessage(parsed, action)
|
|
194
225
|
if (message) {
|
|
195
226
|
onEvent?.(bridgeEvent("codex_event", message, boundedTail(line)))
|
|
@@ -197,7 +228,7 @@ function emitCodexJsonProgress(buffer: string, action: ReviewPromptAction, onEve
|
|
|
197
228
|
onEvent?.(bridgeEvent("stdout", "Codex wrote output.", boundedTail(line)))
|
|
198
229
|
}
|
|
199
230
|
}
|
|
200
|
-
return remainder
|
|
231
|
+
return { remainder, sawTrustedCompletion }
|
|
201
232
|
}
|
|
202
233
|
|
|
203
234
|
function codexProgressMessage(value: unknown, action: ReviewPromptAction): string | undefined {
|
|
@@ -207,14 +238,18 @@ function codexProgressMessage(value: unknown, action: ReviewPromptAction): strin
|
|
|
207
238
|
const event = typeof record.event === "string" ? record.event.toLowerCase() : ""
|
|
208
239
|
const name = `${type} ${event}`
|
|
209
240
|
if (!name.trim()) return undefined
|
|
210
|
-
|
|
241
|
+
const normalized = name.replace(/[._-]+/g, " ")
|
|
242
|
+
if (normalized.includes("turn completed") || normalized.includes("thread completed") || normalized.includes("completed")) {
|
|
211
243
|
return action === "comment" ? undefined : "Codex completed the inspection."
|
|
212
244
|
}
|
|
213
|
-
if (
|
|
245
|
+
if (normalized.includes("thread started") || normalized.includes("turn started") || normalized.includes("session started")) {
|
|
246
|
+
return "Codex started reading the deck..."
|
|
247
|
+
}
|
|
248
|
+
if (normalized.includes("exec") || normalized.includes("patch") || normalized.includes("tool") || normalized.includes("apply")) {
|
|
214
249
|
return action === "comment" ? "Codex is applying the requested edit..." : "Codex is reading the deck..."
|
|
215
250
|
}
|
|
216
|
-
if (
|
|
217
|
-
if (
|
|
251
|
+
if (normalized.includes("session") || normalized.includes("thread") || normalized.includes("turn") || normalized.includes("start")) return "Codex is reading the deck..."
|
|
252
|
+
if (normalized.includes("message") || normalized.includes("delta") || normalized.includes("agent")) return "Codex is working..."
|
|
218
253
|
return "Codex is working..."
|
|
219
254
|
}
|
|
220
255
|
|
|
@@ -227,6 +262,40 @@ function boundedTail(text: string, limit = 4096): string {
|
|
|
227
262
|
return text.slice(text.length - limit)
|
|
228
263
|
}
|
|
229
264
|
|
|
265
|
+
function hasTrustedCodexCompletion(stdout: string): boolean {
|
|
266
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
267
|
+
const parsed = parseJson(line)
|
|
268
|
+
if (isTrustedCodexCompletionRecord(parsed)) return true
|
|
269
|
+
}
|
|
270
|
+
for (const block of extractJsonBlocks(stdout)) {
|
|
271
|
+
const parsed = parseJson(block)
|
|
272
|
+
if (isTrustedCodexCompletionRecord(parsed)) return true
|
|
273
|
+
}
|
|
274
|
+
return false
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function isTrustedCodexCompletionRecord(value: unknown): boolean {
|
|
278
|
+
if (!value) return false
|
|
279
|
+
if (typeof value === "string") return isTrustedCodexCompletionRecord(parseJson(value))
|
|
280
|
+
if (Array.isArray(value)) return value.some((item) => isTrustedCodexCompletionRecord(item))
|
|
281
|
+
if (typeof value !== "object") return false
|
|
282
|
+
|
|
283
|
+
const record = value as Record<string, unknown>
|
|
284
|
+
const type = typeof record.type === "string" ? record.type.toLowerCase() : ""
|
|
285
|
+
const event = typeof record.event === "string" ? record.event.toLowerCase() : ""
|
|
286
|
+
const status = typeof record.status === "string" ? record.status.toLowerCase() : ""
|
|
287
|
+
const normalized = `${type} ${event}`.replace(/[._-]+/g, " ")
|
|
288
|
+
const exitCode = typeof record.exit_code === "number" ? record.exit_code : typeof record.exitCode === "number" ? record.exitCode : undefined
|
|
289
|
+
|
|
290
|
+
if (normalized.includes("turn completed") || normalized.includes("thread completed")) return true
|
|
291
|
+
if (status === "completed" && exitCode === 0) return true
|
|
292
|
+
|
|
293
|
+
for (const key of ["item", "result", "output", "event", "payload"]) {
|
|
294
|
+
if (isTrustedCodexCompletionRecord(record[key])) return true
|
|
295
|
+
}
|
|
296
|
+
return false
|
|
297
|
+
}
|
|
298
|
+
|
|
230
299
|
function isCodexWriteBlocked(raw: string): boolean {
|
|
231
300
|
const text = raw.toLowerCase()
|
|
232
301
|
return (
|