sprag-cli 3.70.0 → 3.71.0
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.ko.md +7 -1
- package/README.md +7 -1
- package/bin/cli.js +47 -5
- package/docs/CODEX.ko.md +2 -2
- package/docs/CODEX.md +4 -4
- package/docs/TEAM.ko.md +74 -19
- package/docs/TEAM.md +75 -19
- package/package.json +2 -1
- package/presets/agents.json +118 -0
- package/presets/doc2md/convert.py +1 -1
- package/presets/ratchet-rules.json +12 -2
- package/src/cli-entry.js +40 -0
- package/src/codex/hooks.js +11 -10
- package/src/codex/panel-setup.js +2 -1
- package/src/codex/panel.js +2 -1
- package/src/commands/brief.js +2 -0
- package/src/commands/codex.js +3 -2
- package/src/commands/harness.js +2 -3
- package/src/commands/install.js +19 -18
- package/src/commands/presets.js +158 -0
- package/src/commands/profile-map.js +2 -2
- package/src/commands/route-scan.js +9 -7
- package/src/commands/team.js +82 -26
- package/src/commands/update-check.js +2 -2
- package/src/debug.js +3 -2
- package/src/delegation-agents.js +48 -60
- package/src/doc2md.cjs +4 -3
- package/src/env.cjs +7 -0
- package/src/env.js +19 -0
- package/src/first-run-note.js +3 -2
- package/src/harness.js +4 -14
- package/src/hook.cjs +4 -3
- package/src/preset-sign.js +106 -0
- package/src/preset-sync.js +133 -0
- package/src/presets-source.js +209 -0
- package/src/prompt.js +2 -1
- package/src/route-scan.js +14 -1
- package/src/seed-rules.js +14 -16
- package/src/star-note.js +3 -2
- package/src/statusline-guard.js +144 -0
- package/src/stdin-payload.js +33 -4
- package/src/team-prompts.js +353 -0
- package/src/team-ratchet.js +116 -16
- package/src/team-sync.js +336 -37
- package/src/update-check.js +3 -2
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
{
|
|
2
|
+
"note": [
|
|
3
|
+
"Delegation-only subagents written to ~/.claude/agents by syncDelegationAgents().",
|
|
4
|
+
"Same shape as the \"agents\" array of a published preset bundle, so the bundled copy",
|
|
5
|
+
"and a remote bundle render through one path. A remote bundle replaces this list",
|
|
6
|
+
"when its signature verifies; otherwise this file is used.",
|
|
7
|
+
"Models are aliases on purpose (a gateway resolves them through ANTHROPIC_DEFAULT_*_MODEL),",
|
|
8
|
+
"except opus-worker, which names the full id: a bare alias resolves to an older model.",
|
|
9
|
+
"Effort follows how the work is verified: low for lookups and command runs, medium for",
|
|
10
|
+
"implementation against tests, high for the larger opus share.",
|
|
11
|
+
"The files are rendered in English (description.en, body.en): they are model-facing",
|
|
12
|
+
"prompts, and the Korean copy is what the /ops console shows next to it."
|
|
13
|
+
],
|
|
14
|
+
"agents": [
|
|
15
|
+
{
|
|
16
|
+
"name": "haiku-explore",
|
|
17
|
+
"model": "haiku",
|
|
18
|
+
"effort": "low",
|
|
19
|
+
"tools": [
|
|
20
|
+
"Read",
|
|
21
|
+
"Grep",
|
|
22
|
+
"Glob",
|
|
23
|
+
"Bash"
|
|
24
|
+
],
|
|
25
|
+
"description": {
|
|
26
|
+
"ko": "위임 작업용 저비용 읽기 전용 탐색, 조회, 상태 확인, 요약 에이전트입니다. MCP 도구는 없습니다.",
|
|
27
|
+
"en": "Cheap read-only exploration, lookup, status checks and summaries for delegated tasks. It has no MCP tools."
|
|
28
|
+
},
|
|
29
|
+
"body": {
|
|
30
|
+
"ko": "맡은 역할: 읽기 전용 탐색, 조회, 상태 확인, 붙여넣은 로그나 텍스트를 두고 묻는 질문에 답합니다.\n위임받은 작업만 하고, 호출자가 프롬프트에 적은 상한(도구 호출 횟수, 출력 토큰)을 넘기지 마십시오.\n호출자 프롬프트의 언어로 간결하게 보고하십시오.\nMCP 도구나 목록에 없는 도구가 필요하면 즉시 멈추고 \"needs general-purpose: <필요한 도구>\" 한 줄로 보고하십시오. 부모가 다시 위임합니다.\n확인하지 못한 내용은 추측하지 말고 확인하지 못했다고 말하십시오.",
|
|
31
|
+
"en": "Your role: read-only exploration, lookups, status checks, and questions about pasted logs or text.\nDo only the delegated task, and stay within the caps (tool calls, output tokens) the caller wrote in the prompt.\nReport concisely, in the language of the caller's prompt.\nIf you need an MCP tool or any tool not on your list, stop at once and report one line: \"needs general-purpose: <tools needed>\". The parent will delegate again.\nDo not guess at anything you could not verify; say that you could not verify it."
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"name": "haiku-runner",
|
|
36
|
+
"model": "haiku",
|
|
37
|
+
"effort": "low",
|
|
38
|
+
"tools": [
|
|
39
|
+
"Bash",
|
|
40
|
+
"Read",
|
|
41
|
+
"Grep",
|
|
42
|
+
"Glob"
|
|
43
|
+
],
|
|
44
|
+
"description": {
|
|
45
|
+
"ko": "위임 작업의 빌드·테스트·git 명령을 실행하는 저비용 에이전트입니다. MCP 도구는 없습니다.",
|
|
46
|
+
"en": "Cheap runner for build, test and git commands in delegated tasks. It has no MCP tools."
|
|
47
|
+
},
|
|
48
|
+
"body": {
|
|
49
|
+
"ko": "맡은 역할: 전달받은 빌드·테스트·git 명령을 실행하고 결과를 보고합니다.\n위임받은 작업만 하고, 호출자가 프롬프트에 적은 상한(도구 호출 횟수, 출력 토큰)을 넘기지 마십시오.\n호출자 프롬프트의 언어로 간결하게 보고하십시오.\nMCP 도구나 목록에 없는 도구가 필요하면 즉시 멈추고 \"needs general-purpose: <필요한 도구>\" 한 줄로 보고하십시오. 부모가 다시 위임합니다.\n확인하지 못한 내용은 추측하지 말고 확인하지 못했다고 말하십시오.",
|
|
50
|
+
"en": "Your role: run the build, test, or git commands you are given and report the result.\nDo only the delegated task, and stay within the caps (tool calls, output tokens) the caller wrote in the prompt.\nReport concisely, in the language of the caller's prompt.\nIf you need an MCP tool or any tool not on your list, stop at once and report one line: \"needs general-purpose: <tools needed>\". The parent will delegate again.\nDo not guess at anything you could not verify; say that you could not verify it."
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"name": "haiku-translate",
|
|
55
|
+
"model": "haiku",
|
|
56
|
+
"effort": "low",
|
|
57
|
+
"tools": [
|
|
58
|
+
"Read",
|
|
59
|
+
"Grep",
|
|
60
|
+
"Glob",
|
|
61
|
+
"Edit",
|
|
62
|
+
"Write"
|
|
63
|
+
],
|
|
64
|
+
"description": {
|
|
65
|
+
"ko": "위임 작업용 저비용 일괄 번역·정형 텍스트 변환 에이전트입니다. MCP 도구는 없습니다.",
|
|
66
|
+
"en": "Cheap batch translation and structured text conversion for delegated tasks. It has no MCP tools."
|
|
67
|
+
},
|
|
68
|
+
"body": {
|
|
69
|
+
"ko": "맡은 역할: 전달받은 파일이나 텍스트의 일괄 번역과 정형 텍스트 변환입니다.\n위임받은 작업만 하고, 호출자가 프롬프트에 적은 상한(도구 호출 횟수, 출력 토큰)을 넘기지 마십시오.\n호출자 프롬프트의 언어로 간결하게 보고하십시오.\nMCP 도구나 목록에 없는 도구가 필요하면 즉시 멈추고 \"needs general-purpose: <필요한 도구>\" 한 줄로 보고하십시오. 부모가 다시 위임합니다.\n확인하지 못한 내용은 추측하지 말고 확인하지 못했다고 말하십시오.",
|
|
70
|
+
"en": "Your role: batch translation and mechanical text conversion of the files or text you are given.\nDo only the delegated task, and stay within the caps (tool calls, output tokens) the caller wrote in the prompt.\nReport concisely, in the language of the caller's prompt.\nIf you need an MCP tool or any tool not on your list, stop at once and report one line: \"needs general-purpose: <tools needed>\". The parent will delegate again.\nDo not guess at anything you could not verify; say that you could not verify it."
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"name": "sonnet-worker",
|
|
75
|
+
"model": "sonnet",
|
|
76
|
+
"effort": "medium",
|
|
77
|
+
"tools": [
|
|
78
|
+
"Read",
|
|
79
|
+
"Grep",
|
|
80
|
+
"Glob",
|
|
81
|
+
"Bash",
|
|
82
|
+
"Edit",
|
|
83
|
+
"Write",
|
|
84
|
+
"NotebookEdit"
|
|
85
|
+
],
|
|
86
|
+
"description": {
|
|
87
|
+
"ko": "여러 단계 작업과 설계가 확정된 구현을 맡는 중간 티어 워커입니다. MCP 도구는 없습니다.",
|
|
88
|
+
"en": "Mid-tier worker for multi-step tasks and implementation handed over with a fixed design. It has no MCP tools."
|
|
89
|
+
},
|
|
90
|
+
"body": {
|
|
91
|
+
"ko": "맡은 역할: 여러 단계로 이어지는 작업과, 호출자가 설계를 확정한 구현을 맡습니다.\n위임받은 작업만 하고, 호출자가 프롬프트에 적은 상한(도구 호출 횟수, 출력 토큰)을 넘기지 마십시오.\n호출자 프롬프트의 언어로 간결하게 보고하십시오.\nMCP 도구나 목록에 없는 도구가 필요하면 즉시 멈추고 \"needs general-purpose: <필요한 도구>\" 한 줄로 보고하십시오. 부모가 다시 위임합니다.\n확인하지 못한 내용은 추측하지 말고 확인하지 못했다고 말하십시오.",
|
|
92
|
+
"en": "Your role: multi-step work and the implementation half of a design the caller already fixed.\nDo only the delegated task, and stay within the caps (tool calls, output tokens) the caller wrote in the prompt.\nReport concisely, in the language of the caller's prompt.\nIf you need an MCP tool or any tool not on your list, stop at once and report one line: \"needs general-purpose: <tools needed>\". The parent will delegate again.\nDo not guess at anything you could not verify; say that you could not verify it."
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"name": "opus-worker",
|
|
97
|
+
"model": "claude-opus-5-5",
|
|
98
|
+
"effort": "high",
|
|
99
|
+
"tools": [
|
|
100
|
+
"Read",
|
|
101
|
+
"Grep",
|
|
102
|
+
"Glob",
|
|
103
|
+
"Bash",
|
|
104
|
+
"Edit",
|
|
105
|
+
"Write",
|
|
106
|
+
"NotebookEdit"
|
|
107
|
+
],
|
|
108
|
+
"description": {
|
|
109
|
+
"ko": "Fable 세션용 고성능 워커로, 설계가 확정된 대규모 구현·디버깅·리팩터링을 맡습니다. MCP 도구는 없습니다.",
|
|
110
|
+
"en": "Heavy worker for Fable sessions: large implementation, debugging and refactoring handed over with a fixed design. It has no MCP tools."
|
|
111
|
+
},
|
|
112
|
+
"body": {
|
|
113
|
+
"ko": "맡은 역할: 호출자가 설계를 확정한 대규모 구현과 디버깅입니다. 설계 판단과 되돌릴 수 없는 작업은 호출자에게 넘기십시오.\n위임받은 작업만 하고, 호출자가 프롬프트에 적은 상한(도구 호출 횟수, 출력 토큰)을 넘기지 마십시오.\n호출자 프롬프트의 언어로 간결하게 보고하십시오.\nMCP 도구나 목록에 없는 도구가 필요하면 즉시 멈추고 \"needs general-purpose: <필요한 도구>\" 한 줄로 보고하십시오. 부모가 다시 위임합니다.\n확인하지 못한 내용은 추측하지 말고 확인하지 못했다고 말하십시오.",
|
|
114
|
+
"en": "Your role: large implementation and debugging work whose design the caller already fixed. Hand design judgement and irreversible actions back to the caller.\nDo only the delegated task, and stay within the caps (tool calls, output tokens) the caller wrote in the prompt.\nReport concisely, in the language of the caller's prompt.\nIf you need an MCP tool or any tool not on your list, stop at once and report one line: \"needs general-purpose: <tools needed>\". The parent will delegate again.\nDo not guess at anything you could not verify; say that you could not verify it."
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
]
|
|
118
|
+
}
|
|
@@ -34,7 +34,7 @@ import zipfile
|
|
|
34
34
|
|
|
35
35
|
MAX_UNCOMPRESSED = 500 * 1024 * 1024
|
|
36
36
|
MAX_RATIO = 200
|
|
37
|
-
ROW_CAP = int(os.environ.get("CTS_DOC2MD_ROW_CAP", "50000"))
|
|
37
|
+
ROW_CAP = int(os.environ.get("SPRAG_DOC2MD_ROW_CAP", os.environ.get("CTS_DOC2MD_ROW_CAP", "50000")))
|
|
38
38
|
ZIP_EXTS = {".pptx", ".xlsx", ".docx"}
|
|
39
39
|
|
|
40
40
|
|
|
@@ -6,31 +6,39 @@
|
|
|
6
6
|
"each answer. Both read this file, so there is one source for the wording.",
|
|
7
7
|
"Each rule carries both languages because the text is injected into the model's",
|
|
8
8
|
"context, where a Korean-only rule would steer an English session into Korean.",
|
|
9
|
-
"
|
|
10
|
-
"
|
|
9
|
+
"Each rule carries a stable `id`. It was derived once from the `ko` text",
|
|
10
|
+
"(fix-<sha1(ko)[0:6]>) and is kept when the wording changes, so a rule keeps its",
|
|
11
|
+
"identity, and a user's 'no', across rewording. This file is also the fallback for",
|
|
12
|
+
"the centrally published preset bundle; `npm run presets:pull` refreshes it."
|
|
11
13
|
],
|
|
12
14
|
"rules": [
|
|
13
15
|
{
|
|
16
|
+
"id": "fix-bd7836",
|
|
14
17
|
"ko": "웹 fetch/search에서 robots·UA 차단('unable to fetch' 등)이 발생하면 → curl에 브라우저 UA(Mozilla/5.0 ... Chrome/...) 헤더를 붙여 재시도. reddit 등은 .json 엔드포인트 + UA 조합으로 접근 가능",
|
|
15
18
|
"en": "When a web fetch/search is blocked by robots or user-agent checks ('unable to fetch' and similar) → retry with curl carrying a browser UA header (Mozilla/5.0 ... Chrome/...). Sites like reddit also answer on their .json endpoints with that UA"
|
|
16
19
|
},
|
|
17
20
|
{
|
|
21
|
+
"id": "fix-4168c2",
|
|
18
22
|
"ko": "ratchet 룰 promote 호출 직전 사용자에게 scope(global/project)를 먼저 묻는다 — Bash 환경은 non-TTY라 CLI readline 프롬프트가 안 뜨므로, 호출자(LLM)가 대신 묻고 --global/--project 플래그를 명시해 실행",
|
|
19
23
|
"en": "Ask the user for the scope (global or project) before calling ratchet promote — the Bash environment is non-TTY, so the CLI's readline prompt never appears; the caller (the model) has to ask and pass --global/--project explicitly"
|
|
20
24
|
},
|
|
21
25
|
{
|
|
26
|
+
"id": "fix-ce98b4",
|
|
22
27
|
"ko": "자동화 도구가 grep·정규식으로 찾는 마커 텍스트(헤더·앵커·키워드 라인)는 리팩터링·압축·수정 금지 — 정확 매칭 검출은 한 글자만 바뀌어도 깨짐",
|
|
23
28
|
"en": "Never refactor, compress or reword marker text that an automated tool finds by grep or regex (headings, anchors, keyword lines) — exact-match detection breaks on a single changed character"
|
|
24
29
|
},
|
|
25
30
|
{
|
|
31
|
+
"id": "fix-b1da7a",
|
|
26
32
|
"ko": "자동화 설계에서 vision/screenshot 루프가 보이면 → 호출 가능한 함수·API·MCP tool부터 찾는다. 통제 가능한 내부 도구는 무조건 API, 통제 불가 외부 SaaS만 vision 차선책. 인터페이스를 바꾼 뒤에 모델을 내린다 — 거꾸로 가면 토큰 폭발",
|
|
27
33
|
"en": "When an automation design calls for a vision/screenshot loop → look for a callable function, API or MCP tool first. Anything you control gets an API; vision is the fallback only for external SaaS you cannot change. Fix the interface before lowering the model — doing it the other way round explodes the token cost"
|
|
28
34
|
},
|
|
29
35
|
{
|
|
36
|
+
"id": "fix-1dee5f",
|
|
30
37
|
"ko": "입력 데이터(ground-truth) 변경 시 그에 의존하는 모든 파생 자산을 끝까지 재생성한다 — 중간 단계를 생략하면 stale 산출물이 새 입력 위에 얹혀 결과가 깨짐",
|
|
31
38
|
"en": "When the ground-truth input changes, regenerate every derived asset all the way through — skipping an intermediate step leaves a stale artifact sitting on top of the new input and the result breaks"
|
|
32
39
|
},
|
|
33
40
|
{
|
|
41
|
+
"id": "fix-f558f9",
|
|
34
42
|
"ko": "탐색·조회·상태 확인·명령 실행 같은 단순 요청은 `model: haiku` 서브에이전트로 위임해 상위 모델 토큰을 아낀다 (읽기 전용 preset 에이전트가 이미 있으면 그걸 쓰고, 없으면 티어만 내려서 위임)",
|
|
35
43
|
"en": "Delegate simple requests — lookup, status checks, running commands — to a `model: haiku` subagent to keep the expensive model's tokens for real work (use an existing read-only preset agent when there is one; otherwise delegate with the tier lowered)",
|
|
36
44
|
"agents": [
|
|
@@ -38,10 +46,12 @@
|
|
|
38
46
|
]
|
|
39
47
|
},
|
|
40
48
|
{
|
|
49
|
+
"id": "fix-aa18bc",
|
|
41
50
|
"ko": "지침이나 룰이 지켜지지 않아 같은 품질 문제가 재발하면 문구를 다시 쓰지 말 것 → 그 조항이 기계로 판정 가능한지부터 따지고(문자열 비교나 정규식으로 결정되면 판정 가능), 가능하면 쓰기 시점 훅이나 테스트 같은 차단 지점을 추가한다. 판단이 필요한 잔여 부분만 지침에 남기고 그 경계를 지침에 적는다. 실제 코퍼스에 돌려 오탐 건수를 센 뒤 좁히고, 회귀 테스트를 같은 커밋에 넣는다",
|
|
42
51
|
"en": "When a guideline keeps being violated and the same quality defect recurs, do NOT reword the instruction → first ask whether the clause is machine-decidable (a string comparison or regex settles it), and if so add an enforcement point such as a write-time hook or a test. Leave only the judgement remainder in the prose and state that boundary there. Measure the false-positive count on a real corpus, narrow until it is acceptable, and land a regression test in the same commit"
|
|
43
52
|
},
|
|
44
53
|
{
|
|
54
|
+
"id": "fix-777a15",
|
|
45
55
|
"ko": "여러 파일에 걸친 구현을 맡으면 → 설계와 스펙 확정은 메인 모델이 하고 코드 작성은 서브에이전트에 위임한다(Claude는 `model: sonnet`, Codex는 명시적 매핑을 우선해 해석한 T1을 쓰며 haiku나 T2에는 구현을 맡기지 않는다). 위임 프롬프트에는 설계, 수정할 파일, 제약, 완료 기준, 테스트 명령, 예산, 권장 모델과 그 이유를 적어 다른 문맥 없이 실행할 수 있게 한다. 이미 읽은 파일의 약 50줄 이하 변경이나 의존 단계가 하나뿐인 짧은 구현, 그리고 위임 도구나 하위 모델을 쓸 수 없는 경우는 메인 모델이 직접 처리한다. 위임한 구현은 끝날 때까지 기다리고 같은 구현을 중복 실행하지 않으며, 돌아온 diff와 테스트 결과를 메인 모델이 직접 검증한 뒤에 완료로 보고한다",
|
|
46
56
|
"en": "When an implementation spans several files → the main model settles the design and specification, then delegates the code writing to a subagent (Claude: `model: sonnet`; Codex: its resolved T1, explicit mapping first; never haiku or T2 for implementation). Write the delegation prompt so it runs without other context: design, files to change, constraints, completion criteria, test commands, budget, and the recommended model with a one-line reason. Keep changes of roughly 50 lines or less in files already read, a single short dependency chain, or cases where the subagent tools or the lower model are unavailable on the main model. Wait for the delegated work instead of duplicating it, and verify the returned diff and the actual test results before reporting completion",
|
|
47
57
|
"agents": [
|
package/src/cli-entry.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The script a detached child of this CLI must run.
|
|
3
|
+
*
|
|
4
|
+
* The preset sync and the two team syncs start a detached child to do their
|
|
5
|
+
* network work, and they used to pass `process.argv[1]` as its script. Inside
|
|
6
|
+
* the CLI that is bin/cli.js. Inside anything else that imports these modules
|
|
7
|
+
* it is that caller's script: under `node --test` a test file relaunched
|
|
8
|
+
* itself, detached, on every session-start case, and each copy did the same.
|
|
9
|
+
* 4,055 processes in 59 seconds on 2026-10-20, and three memory-exhaustion
|
|
10
|
+
* stalls in two days. So the entry is fixed here from this module's own
|
|
11
|
+
* location, and a spawned child carries a marker that refuses to spawn again.
|
|
12
|
+
*/
|
|
13
|
+
import * as childProcess from 'node:child_process';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { envVar } from './env.js';
|
|
16
|
+
|
|
17
|
+
export const CLI_ENTRY = fileURLToPath(new URL('../bin/cli.js', import.meta.url));
|
|
18
|
+
|
|
19
|
+
/** The marker a detached child carries; read through envVar('DETACHED_CHILD'). */
|
|
20
|
+
export const DETACHED_CHILD_VAR = 'SPRAG_DETACHED_CHILD';
|
|
21
|
+
|
|
22
|
+
export function isDetachedChild(env = process.env) {
|
|
23
|
+
return envVar('DETACHED_CHILD', env) === '1';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The environment for a detached child: the caller's, plus the marker. */
|
|
27
|
+
export function detachedChildEnv(env = process.env) {
|
|
28
|
+
return { ...env, [DETACHED_CHILD_VAR]: '1' };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Start `sprag <args>` detached, never awaited, never from a detached child.
|
|
33
|
+
* Returns true when a child was started. Every background refresh goes
|
|
34
|
+
* through here so that no caller can name the script itself.
|
|
35
|
+
*/
|
|
36
|
+
export function spawnDetachedCli(args, { env = process.env, spawn = childProcess.spawn, execPath = process.execPath } = {}) {
|
|
37
|
+
if (isDetachedChild(env)) return false;
|
|
38
|
+
spawn(execPath, [CLI_ENTRY, ...args], { detached: true, stdio: 'ignore', windowsHide: true, env: detachedChildEnv(env) }).unref();
|
|
39
|
+
return true;
|
|
40
|
+
}
|
package/src/codex/hooks.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { envVar } from '../env.js';
|
|
2
|
+
import { spawnDetachedCli } from '../cli-entry.js';
|
|
1
3
|
import { createRequire } from 'node:module';
|
|
2
|
-
import { spawn } from 'node:child_process';
|
|
3
4
|
import { createHash } from 'node:crypto';
|
|
4
5
|
import { mkdirSync, openSync, readFileSync, readSync, closeSync, statSync } from 'node:fs';
|
|
5
6
|
import { join, resolve } from 'node:path';
|
|
@@ -116,9 +117,8 @@ async function routeNotice(root, { spawnRescan = defaultRescan } = {}) {
|
|
|
116
117
|
}
|
|
117
118
|
|
|
118
119
|
function defaultRescan() {
|
|
119
|
-
if (
|
|
120
|
-
|
|
121
|
-
{ detached: true, stdio: 'ignore', windowsHide: true }).unref();
|
|
120
|
+
if (envVar('NO_ROUTE_SCAN') === '1') return;
|
|
121
|
+
spawnDetachedCli(['route-scan', '--refresh', '--quiet', '--agent', 'codex']);
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
/**
|
|
@@ -130,9 +130,8 @@ function defaultRescan() {
|
|
|
130
130
|
* of the parent is the earliest point that knows one was spawned.
|
|
131
131
|
*/
|
|
132
132
|
function defaultLedgerRefresh() {
|
|
133
|
-
if (
|
|
134
|
-
|
|
135
|
-
{ detached: true, stdio: 'ignore', windowsHide: true }).unref();
|
|
133
|
+
if (envVar('NO_ROUTE_SCAN') === '1') return;
|
|
134
|
+
spawnDetachedCli(['route-scan', 'savings', '--refresh', '--quiet', '--agent', 'codex']);
|
|
136
135
|
}
|
|
137
136
|
|
|
138
137
|
export function lintCodexTool(payload, { scope = 'all' } = {}) {
|
|
@@ -180,6 +179,7 @@ export async function codexHookOutput(event, payload, { cfg = loadConfig(), refr
|
|
|
180
179
|
const parts = [];
|
|
181
180
|
// Keep the preset file current with this version's wording before it is read.
|
|
182
181
|
if (event === 'session-start') {
|
|
182
|
+
try { (await import('../preset-sync.js')).spawnPresetSyncIfDue(); } catch (e) { debug('codex:preset-sync', e); }
|
|
183
183
|
try { (await import('../preset-ratchet.js')).preparePresetRatchet({ agent: 'codex' }); } catch (e) { debug('codex:preset', e); }
|
|
184
184
|
}
|
|
185
185
|
const { presetRatchetPath } = await import('../preset-ratchet.js');
|
|
@@ -219,7 +219,7 @@ export async function codexHookOutput(event, payload, { cfg = loadConfig(), refr
|
|
|
219
219
|
try { korean = syncCodexKoreanBlock({ cfg }); } catch (e) { debug('codex:korean-sync', e); }
|
|
220
220
|
if (korean?.action !== 'unchanged') parts.push({ priority: 2, spill: true, text: koreanStyleInjection({ cfg }) });
|
|
221
221
|
parts.push({ priority: 3, spill: true, text: await cohesionInjection({ cfg }) });
|
|
222
|
-
if (
|
|
222
|
+
if (envVar('NO_DOC2MD') !== '1' && cfg?.codex?.doc2md !== false) parts.push({ priority: 4, spill: true, text: codexDocumentNote() });
|
|
223
223
|
// Offers ask the user a question; a `codex exec` run has no one to answer it.
|
|
224
224
|
if (event === 'session-start' && payload.source !== 'compact' && !isCodexExecSession(payload.transcript_path)) {
|
|
225
225
|
if (version) parts.push({ priority: 5, text: updateOffer(version, { agent: 'codex', lang: userLanguage() }) });
|
|
@@ -231,6 +231,7 @@ export async function codexHookOutput(event, payload, { cfg = loadConfig(), refr
|
|
|
231
231
|
}
|
|
232
232
|
if (event === 'prompt') {
|
|
233
233
|
const parts = [];
|
|
234
|
+
try { (await import('../team-ratchet.js')).spawnTeamSyncOnPrompt(); } catch (e) { debug('codex:team-prompts', e); }
|
|
234
235
|
if (codexDelegateEnabled(cfg)) {
|
|
235
236
|
// Before the hint for this prompt is recorded: a hint from an earlier turn
|
|
236
237
|
// that no child claimed is closed, and children that did run get priced.
|
|
@@ -247,14 +248,14 @@ export async function codexHookOutput(event, payload, { cfg = loadConfig(), refr
|
|
|
247
248
|
}
|
|
248
249
|
catch (e) { debug('codex:brief', e); }
|
|
249
250
|
}
|
|
250
|
-
if (
|
|
251
|
+
if (envVar('NO_DOC2MD') !== '1' && cfg?.codex?.doc2md !== false) {
|
|
251
252
|
const identity = codexDocumentContext(payload);
|
|
252
253
|
parts.push(doc2md.contextForPrompt(payload, { lang: userLanguage(), agent: 'codex', convert: (source) => convertCodexDocument(source, identity) }));
|
|
253
254
|
}
|
|
254
255
|
return context('UserPromptSubmit', parts.filter(Boolean).join('\n\n'));
|
|
255
256
|
}
|
|
256
257
|
if (event === 'pre-tool') {
|
|
257
|
-
if (
|
|
258
|
+
if (envVar('NO_DOC2MD') !== '1' && cfg?.codex?.doc2md !== false) {
|
|
258
259
|
const result = codexDocumentTool(payload);
|
|
259
260
|
if (result) return result;
|
|
260
261
|
}
|
package/src/codex/panel-setup.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* not zsh, or the user already has their own `codex` alias/function.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { envVar } from '../env.js';
|
|
11
12
|
import { execFileSync } from 'node:child_process';
|
|
12
13
|
import { existsSync } from 'node:fs';
|
|
13
14
|
import { basename, dirname, join } from 'node:path';
|
|
@@ -21,7 +22,7 @@ export function tmuxAvailable({ exec = execFileSync } = {}) {
|
|
|
21
22
|
|
|
22
23
|
export function ensureTmux({ platform = process.platform, env = process.env, exec = execFileSync, exists = existsSync } = {}) {
|
|
23
24
|
// Checked first, before probing anything, so tests and CI never spawn brew.
|
|
24
|
-
if (env
|
|
25
|
+
if (envVar('NO_TMUX', env) === '1' || (env.CI && env.CI !== 'false')) return { status: 'skipped' };
|
|
25
26
|
if (tmuxAvailable({ exec })) return { status: 'present' };
|
|
26
27
|
if (platform !== 'darwin') {
|
|
27
28
|
return { status: 'unavailable', detail: 'install tmux with your package manager (e.g. sudo apt install tmux)' };
|
package/src/codex/panel.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { envVar } from '../env.js';
|
|
1
2
|
import { resolve, relative, isAbsolute, basename } from 'node:path';
|
|
2
3
|
import { realpathSync, statSync } from 'node:fs';
|
|
3
4
|
import { stat } from 'node:fs/promises';
|
|
@@ -151,7 +152,7 @@ export function createPanelReader({ root = process.cwd(), home, sessionId: pinne
|
|
|
151
152
|
cohesion: cfg?.cohesion?.enabled === true,
|
|
152
153
|
brief: cfg?.codex?.brief !== false,
|
|
153
154
|
delegate: codexDelegateEnabled(cfg),
|
|
154
|
-
doc2md: cfg?.codex?.doc2md !== false &&
|
|
155
|
+
doc2md: cfg?.codex?.doc2md !== false && envVar('NO_DOC2MD') !== '1',
|
|
155
156
|
update: version ? readUpdateChip(version, { refresh: refreshUpdate }) : null,
|
|
156
157
|
updatedAt: new Date() };
|
|
157
158
|
};
|
package/src/commands/brief.js
CHANGED
|
@@ -23,6 +23,8 @@ export async function run({ hasFlag }) {
|
|
|
23
23
|
}
|
|
24
24
|
const ctx = readStdinJson() || {};
|
|
25
25
|
const parts = [];
|
|
26
|
+
// Team prompt upload (Enterprise, opt-in): a stat-light gate, the work runs in a detached child.
|
|
27
|
+
try { (await import('../team-ratchet.js')).spawnTeamSyncOnPrompt(); } catch (e) { debug('brief:team-prompts', e); }
|
|
26
28
|
try {
|
|
27
29
|
const { runBrief } = await import('../brief.js');
|
|
28
30
|
// `root` is the session's cwd for the same reason routeHint takes it below:
|
package/src/commands/codex.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { envVar } from '../env.js';
|
|
1
2
|
import { createArgs } from '../cli-args.js';
|
|
2
3
|
import { configureCodexHooks } from '../codex/installer.js';
|
|
3
4
|
import { initCodexHarness, uninitCodexHarness, codexHarnessStatus, syncCodexKoreanBlock } from '../codex/harness.js';
|
|
@@ -412,7 +413,7 @@ export async function run({ args, version }) {
|
|
|
412
413
|
const hook = configureCodexHooks({ panelAuto });
|
|
413
414
|
console.log(`Codex hooks: ${hook.file} (${hook.action})`);
|
|
414
415
|
for (const line of describePanelSetup(panel)) console.log(`Codex ${line}`);
|
|
415
|
-
if (
|
|
416
|
+
if (envVar('NO_HARNESS') !== '1') {
|
|
416
417
|
const h = initCodexHarness({ root, scope: 'global' });
|
|
417
418
|
console.log(`Codex harness: ${h.file}`);
|
|
418
419
|
try {
|
|
@@ -544,7 +545,7 @@ export async function run({ args, version }) {
|
|
|
544
545
|
const { createRequire } = await import('node:module');
|
|
545
546
|
const doc2md = createRequire(import.meta.url)('../doc2md.cjs');
|
|
546
547
|
const { codexHookStatus } = await import('../codex/installer.js');
|
|
547
|
-
console.log(`Codex document conversion: ${loadConfig()?.codex?.doc2md === false ||
|
|
548
|
+
console.log(`Codex document conversion: ${loadConfig()?.codex?.doc2md === false || envVar('NO_DOC2MD') === '1' ? 'off' : 'on'}`);
|
|
548
549
|
console.log(`Formats: ${doc2md.TARGET_EXTENSIONS.join(' ')}`);
|
|
549
550
|
console.log(`Converter: ${doc2md.findInterpreter() || 'missing; run sprag doc2md install-converter --agent codex'}`);
|
|
550
551
|
console.log(`Cache: ${doc2md.cacheDir()}`);
|
package/src/commands/harness.js
CHANGED
|
@@ -274,9 +274,8 @@ export async function run({ args, hasFlag }) {
|
|
|
274
274
|
// Event-triggered refresh: establish the new rule's stat baseline
|
|
275
275
|
// right away instead of waiting for the next data-gated rescan.
|
|
276
276
|
try {
|
|
277
|
-
const {
|
|
278
|
-
|
|
279
|
-
{ detached: true, stdio: 'ignore', windowsHide: true }).unref();
|
|
277
|
+
const { spawnDetachedCli } = await import('../cli-entry.js');
|
|
278
|
+
spawnDetachedCli(['route-scan', '--refresh', '--quiet']);
|
|
280
279
|
} catch (e) { debug('promote:spawn-refresh', e); /* baseline arrives on the next gated rescan */ }
|
|
281
280
|
return;
|
|
282
281
|
}
|
package/src/commands/install.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* terminal (postinstall, CI, pipes) it keeps theirs, as before.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
+
import { envVar } from '../env.js';
|
|
21
22
|
import { debug } from '../debug.js';
|
|
22
23
|
|
|
23
24
|
/** Thrown when the user declines an optional step, so it is not reported as a failure. */
|
|
@@ -50,7 +51,7 @@ export async function run({ hasFlag }) {
|
|
|
50
51
|
// English — the one outcome a Korean-locale machine should not get by
|
|
51
52
|
// default. Already decided means never asked again.
|
|
52
53
|
try {
|
|
53
|
-
if (!languageDecided() && !
|
|
54
|
+
if (!languageDecided() && !envVar('LANG')) {
|
|
54
55
|
const { koreanLocaleDetected } = await import('../korean-style.js');
|
|
55
56
|
const detected = koreanLocaleDetected() ? 'ko' : 'en';
|
|
56
57
|
console.log('');
|
|
@@ -63,14 +64,14 @@ export async function run({ hasFlag }) {
|
|
|
63
64
|
}
|
|
64
65
|
setUserLanguage(chosen);
|
|
65
66
|
console.log(` language: set to ${chosen === 'ko' ? '한국어' : 'English'} — change it any time with \`sprag mode lang=${chosen === 'ko' ? 'en' : 'ko'}\``);
|
|
66
|
-
} else if (
|
|
67
|
+
} else if (envVar('LANG')) {
|
|
67
68
|
// Escape hatch for scripted installs, which cannot answer a prompt but
|
|
68
69
|
// do know which language the machine's user reads.
|
|
69
|
-
const forced = setUserLanguage(
|
|
70
|
+
const forced = setUserLanguage(envVar('LANG'));
|
|
70
71
|
console.log('');
|
|
71
72
|
console.log(forced
|
|
72
|
-
? ` language: set to ${forced} (
|
|
73
|
-
: ` language: ignored
|
|
73
|
+
? ` language: set to ${forced} (SPRAG_LANG)`
|
|
74
|
+
: ` language: ignored SPRAG_LANG=${envVar('LANG')} — use 'ko' or 'en'`);
|
|
74
75
|
}
|
|
75
76
|
} catch (e) {
|
|
76
77
|
debug('install:language', e); // the English fallback still works
|
|
@@ -166,16 +167,16 @@ export async function run({ hasFlag }) {
|
|
|
166
167
|
// any one project, and the harness principles are project-independent.
|
|
167
168
|
// Only ever ADDS — harnessInit appends its own marked block, backs up the
|
|
168
169
|
// previous file, and leaves surrounding content untouched. Skipped when a
|
|
169
|
-
// block is already there (nothing to do) and when
|
|
170
|
+
// block is already there (nothing to do) and when SPRAG_NO_HARNESS=1 is set,
|
|
170
171
|
// for anyone who wants the statusline without the CLAUDE.md rules.
|
|
171
172
|
try {
|
|
172
173
|
const { harnessInit, harnessStatus } = await import('../harness.js');
|
|
173
174
|
const before = harnessStatus(undefined, { scope: 'global' });
|
|
174
|
-
if (
|
|
175
|
+
if (envVar('NO_HARNESS') === '1') {
|
|
175
176
|
console.log('');
|
|
176
177
|
console.log(lang === 'ko'
|
|
177
|
-
? ' harness:
|
|
178
|
-
: ' harness: skipped (
|
|
178
|
+
? ' harness: SPRAG_NO_HARNESS=1 이므로 건너뜁니다 (나중에 `harness init --global`).'
|
|
179
|
+
: ' harness: skipped (SPRAG_NO_HARNESS=1) — run `harness init --global` later.');
|
|
179
180
|
} else if (before.hasBlock) {
|
|
180
181
|
console.log('');
|
|
181
182
|
console.log(lang === 'ko'
|
|
@@ -242,11 +243,11 @@ export async function run({ hasFlag }) {
|
|
|
242
243
|
// upgrade never re-enables something they turned off.
|
|
243
244
|
try {
|
|
244
245
|
const ks = await import('../korean-style.js');
|
|
245
|
-
if (
|
|
246
|
+
if (envVar('NO_KOREAN') === '1') {
|
|
246
247
|
console.log('');
|
|
247
248
|
console.log(lang === 'ko'
|
|
248
|
-
? ' korean:
|
|
249
|
-
: ' korean: skipped (
|
|
249
|
+
? ' korean: SPRAG_NO_KOREAN=1 이므로 건너뜁니다 (나중에 `korean on`).'
|
|
250
|
+
: ' korean: skipped (SPRAG_NO_KOREAN=1) — run `korean on` later.');
|
|
250
251
|
} else if (ks.koreanStyleDecided()) {
|
|
251
252
|
console.log('');
|
|
252
253
|
console.log(lang === 'ko'
|
|
@@ -328,11 +329,11 @@ export async function run({ hasFlag }) {
|
|
|
328
329
|
const { createRequire } = await import('node:module');
|
|
329
330
|
const require = createRequire(import.meta.url);
|
|
330
331
|
const doc2md = require('../doc2md.cjs');
|
|
331
|
-
if (
|
|
332
|
+
if (envVar('NO_DOC2MD') === '1') {
|
|
332
333
|
console.log('');
|
|
333
334
|
console.log(lang === 'ko'
|
|
334
|
-
? ' doc2md:
|
|
335
|
-
: ' doc2md: skipped (
|
|
335
|
+
? ' doc2md: SPRAG_NO_DOC2MD=1 이므로 건너뜁니다 (나중에 `doc2md on`).'
|
|
336
|
+
: ' doc2md: skipped (SPRAG_NO_DOC2MD=1) — run `doc2md on` later.');
|
|
336
337
|
} else {
|
|
337
338
|
const { installDoc2mdHook } = await import('../installer.js');
|
|
338
339
|
const res = installDoc2mdHook();
|
|
@@ -430,7 +431,7 @@ export async function run({ hasFlag }) {
|
|
|
430
431
|
try {
|
|
431
432
|
const { codexUserDir } = await import('../agent.js');
|
|
432
433
|
const { existsSync } = await import('node:fs');
|
|
433
|
-
if (
|
|
434
|
+
if (envVar('NO_CODEX') !== '1' && existsSync(codexUserDir())) {
|
|
434
435
|
let proceed = true;
|
|
435
436
|
if (interactive) {
|
|
436
437
|
console.log('');
|
|
@@ -451,7 +452,7 @@ export async function run({ hasFlag }) {
|
|
|
451
452
|
if (!tmuxAvailable() && !await confirm(lang === 'ko'
|
|
452
453
|
? ' codex: tmux 가 없습니다. Homebrew 로 설치해 같은 창에 패널을 붙일까요?'
|
|
453
454
|
: ' codex: tmux is missing. Install it with Homebrew so the panel shares the Codex window?', { defaultValue: true })) {
|
|
454
|
-
env = { ...process.env,
|
|
455
|
+
env = { ...process.env, SPRAG_NO_TMUX: '1' };
|
|
455
456
|
}
|
|
456
457
|
}
|
|
457
458
|
let panel;
|
|
@@ -465,7 +466,7 @@ export async function run({ hasFlag }) {
|
|
|
465
466
|
console.log('');
|
|
466
467
|
console.log(` codex: hooks ${hook.action} (${hook.file})`);
|
|
467
468
|
for (const line of describePanelSetup(panel, lang)) console.log(` codex: ${line}`);
|
|
468
|
-
if (
|
|
469
|
+
if (envVar('NO_HARNESS') !== '1') {
|
|
469
470
|
const h = initCodexHarness({ scope: 'global' });
|
|
470
471
|
console.log(` codex: harness ${h.file}`);
|
|
471
472
|
try {
|