ccgx-workflow 1.0.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/LICENSE +22 -0
- package/README.md +469 -0
- package/README.zh-CN.md +466 -0
- package/bin/ccg.mjs +2 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.mjs +173 -0
- package/dist/index.d.mts +1774 -0
- package/dist/index.d.ts +1774 -0
- package/dist/index.mjs +2029 -0
- package/dist/shared/ccgx-workflow.WgUzkiC3.mjs +5248 -0
- package/package.json +129 -0
- package/templates/commands/agents/assumptions-analyzer.md +129 -0
- package/templates/commands/agents/code-fixer.md +292 -0
- package/templates/commands/agents/codebase-mapper.md +152 -0
- package/templates/commands/agents/debug-session-manager.md +247 -0
- package/templates/commands/agents/debugger.md +111 -0
- package/templates/commands/agents/eval-auditor.md +171 -0
- package/templates/commands/agents/framework-selector.md +152 -0
- package/templates/commands/agents/get-current-datetime.md +29 -0
- package/templates/commands/agents/init-architect.md +114 -0
- package/templates/commands/agents/integration-checker.md +163 -0
- package/templates/commands/agents/interface-auditor.md +170 -0
- package/templates/commands/agents/nyquist-auditor.md +131 -0
- package/templates/commands/agents/pattern-mapper.md +111 -0
- package/templates/commands/agents/phase-runner.md +321 -0
- package/templates/commands/agents/plan-checker.md +255 -0
- package/templates/commands/agents/planner.md +320 -0
- package/templates/commands/agents/team-architect.md +186 -0
- package/templates/commands/agents/team-qa.md +121 -0
- package/templates/commands/agents/team-reviewer.md +157 -0
- package/templates/commands/agents/ui-ux-designer.md +573 -0
- package/templates/commands/agents/verifier.md +274 -0
- package/templates/commands/analyze.md +210 -0
- package/templates/commands/autonomous.md +792 -0
- package/templates/commands/cancel.md +132 -0
- package/templates/commands/clean-branches.md +117 -0
- package/templates/commands/codex-exec.md +404 -0
- package/templates/commands/commit.md +151 -0
- package/templates/commands/context.md +332 -0
- package/templates/commands/debate.md +165 -0
- package/templates/commands/debug.md +226 -0
- package/templates/commands/enhance.md +64 -0
- package/templates/commands/execute.md +380 -0
- package/templates/commands/init.md +123 -0
- package/templates/commands/optimize.md +217 -0
- package/templates/commands/plan.md +373 -0
- package/templates/commands/result.md +106 -0
- package/templates/commands/review.md +338 -0
- package/templates/commands/rollback.md +116 -0
- package/templates/commands/spec-impl.md +139 -0
- package/templates/commands/spec-init.md +101 -0
- package/templates/commands/spec-plan.md +210 -0
- package/templates/commands/spec-research.md +152 -0
- package/templates/commands/spec-review.md +120 -0
- package/templates/commands/status.md +206 -0
- package/templates/commands/team-exec.md +265 -0
- package/templates/commands/test.md +236 -0
- package/templates/commands/verify-work.md +338 -0
- package/templates/commands/verify.md +66 -0
- package/templates/commands/workflow.md +190 -0
- package/templates/commands/worktree.md +128 -0
- package/templates/hooks/ccg-context-monitor.js +159 -0
- package/templates/hooks/ccg-session-state.cjs +510 -0
- package/templates/hooks/ccg-statusline.js +142 -0
- package/templates/output-styles/abyss-command.md +56 -0
- package/templates/output-styles/abyss-concise.md +89 -0
- package/templates/output-styles/abyss-cultivator.md +302 -0
- package/templates/output-styles/abyss-ritual.md +70 -0
- package/templates/output-styles/engineer-professional.md +89 -0
- package/templates/output-styles/laowang-engineer.md +127 -0
- package/templates/output-styles/nekomata-engineer.md +120 -0
- package/templates/output-styles/ojousama-engineer.md +121 -0
- package/templates/prompts/claude/analyzer.md +59 -0
- package/templates/prompts/claude/architect.md +54 -0
- package/templates/prompts/claude/debugger.md +71 -0
- package/templates/prompts/claude/optimizer.md +73 -0
- package/templates/prompts/claude/reviewer.md +63 -0
- package/templates/prompts/claude/tester.md +69 -0
- package/templates/prompts/codex/analyzer.md +58 -0
- package/templates/prompts/codex/architect.md +54 -0
- package/templates/prompts/codex/debugger.md +74 -0
- package/templates/prompts/codex/optimizer.md +81 -0
- package/templates/prompts/codex/reviewer.md +73 -0
- package/templates/prompts/codex/tester.md +62 -0
- package/templates/prompts/gemini/analyzer.md +61 -0
- package/templates/prompts/gemini/architect.md +55 -0
- package/templates/prompts/gemini/debugger.md +78 -0
- package/templates/prompts/gemini/frontend.md +64 -0
- package/templates/prompts/gemini/optimizer.md +84 -0
- package/templates/prompts/gemini/reviewer.md +80 -0
- package/templates/prompts/gemini/tester.md +68 -0
- package/templates/rules/ccg-skill-routing.md +83 -0
- package/templates/rules/ccg-skills.md +71 -0
- package/templates/scripts/ccg-phase-runner-launcher.mjs +467 -0
- package/templates/scripts/invoke-model.mjs +949 -0
- package/templates/scripts/repatch-gemini-plugin.mjs +194 -0
- package/templates/skills/SKILL.md +92 -0
- package/templates/skills/domains/ai/SKILL.md +35 -0
- package/templates/skills/domains/ai/agent-dev.md +242 -0
- package/templates/skills/domains/ai/llm-security.md +288 -0
- package/templates/skills/domains/ai/prompt-and-eval.md +279 -0
- package/templates/skills/domains/ai/rag-system.md +542 -0
- package/templates/skills/domains/architecture/SKILL.md +43 -0
- package/templates/skills/domains/architecture/api-design.md +225 -0
- package/templates/skills/domains/architecture/caching.md +299 -0
- package/templates/skills/domains/architecture/cloud-native.md +285 -0
- package/templates/skills/domains/architecture/message-queue.md +329 -0
- package/templates/skills/domains/architecture/security-arch.md +297 -0
- package/templates/skills/domains/data-engineering/SKILL.md +208 -0
- package/templates/skills/domains/development/SKILL.md +47 -0
- package/templates/skills/domains/development/cpp.md +246 -0
- package/templates/skills/domains/development/go.md +323 -0
- package/templates/skills/domains/development/java.md +277 -0
- package/templates/skills/domains/development/python.md +288 -0
- package/templates/skills/domains/development/rust.md +313 -0
- package/templates/skills/domains/development/shell.md +313 -0
- package/templates/skills/domains/development/typescript.md +277 -0
- package/templates/skills/domains/devops/SKILL.md +40 -0
- package/templates/skills/domains/devops/cost-optimization.md +272 -0
- package/templates/skills/domains/devops/database.md +217 -0
- package/templates/skills/domains/devops/devsecops.md +198 -0
- package/templates/skills/domains/devops/git-workflow.md +181 -0
- package/templates/skills/domains/devops/observability.md +280 -0
- package/templates/skills/domains/devops/performance.md +336 -0
- package/templates/skills/domains/devops/testing.md +283 -0
- package/templates/skills/domains/frontend-design/SKILL.md +244 -0
- package/templates/skills/domains/frontend-design/agents/openai.yaml +4 -0
- package/templates/skills/domains/frontend-design/claymorphism/SKILL.md +121 -0
- package/templates/skills/domains/frontend-design/claymorphism/references/tokens.css +52 -0
- package/templates/skills/domains/frontend-design/component-patterns.md +202 -0
- package/templates/skills/domains/frontend-design/engineering.md +287 -0
- package/templates/skills/domains/frontend-design/glassmorphism/SKILL.md +142 -0
- package/templates/skills/domains/frontend-design/glassmorphism/references/tokens.css +32 -0
- package/templates/skills/domains/frontend-design/liquid-glass/SKILL.md +139 -0
- package/templates/skills/domains/frontend-design/liquid-glass/references/tokens.css +81 -0
- package/templates/skills/domains/frontend-design/neubrutalism/SKILL.md +145 -0
- package/templates/skills/domains/frontend-design/neubrutalism/references/tokens.css +44 -0
- package/templates/skills/domains/frontend-design/reference/color-and-contrast.md +132 -0
- package/templates/skills/domains/frontend-design/reference/interaction-design.md +195 -0
- package/templates/skills/domains/frontend-design/reference/motion-design.md +99 -0
- package/templates/skills/domains/frontend-design/reference/responsive-design.md +114 -0
- package/templates/skills/domains/frontend-design/reference/spatial-design.md +100 -0
- package/templates/skills/domains/frontend-design/reference/typography.md +133 -0
- package/templates/skills/domains/frontend-design/reference/ux-writing.md +107 -0
- package/templates/skills/domains/frontend-design/state-management.md +680 -0
- package/templates/skills/domains/frontend-design/ui-aesthetics.md +110 -0
- package/templates/skills/domains/frontend-design/ux-principles.md +156 -0
- package/templates/skills/domains/infrastructure/SKILL.md +201 -0
- package/templates/skills/domains/mobile/SKILL.md +225 -0
- package/templates/skills/domains/orchestration/SKILL.md +30 -0
- package/templates/skills/domains/orchestration/multi-agent.md +263 -0
- package/templates/skills/domains/security/SKILL.md +73 -0
- package/templates/skills/domains/security/blue-team.md +436 -0
- package/templates/skills/domains/security/code-audit.md +265 -0
- package/templates/skills/domains/security/pentest.md +226 -0
- package/templates/skills/domains/security/red-team.md +374 -0
- package/templates/skills/domains/security/threat-intel.md +372 -0
- package/templates/skills/domains/security/vuln-research.md +369 -0
- package/templates/skills/impeccable/adapt/SKILL.md +201 -0
- package/templates/skills/impeccable/animate/SKILL.md +176 -0
- package/templates/skills/impeccable/arrange/SKILL.md +126 -0
- package/templates/skills/impeccable/audit/SKILL.md +149 -0
- package/templates/skills/impeccable/bolder/SKILL.md +118 -0
- package/templates/skills/impeccable/clarify/SKILL.md +185 -0
- package/templates/skills/impeccable/colorize/SKILL.md +144 -0
- package/templates/skills/impeccable/critique/SKILL.md +203 -0
- package/templates/skills/impeccable/critique/reference/cognitive-load.md +106 -0
- package/templates/skills/impeccable/critique/reference/heuristics-scoring.md +234 -0
- package/templates/skills/impeccable/critique/reference/personas.md +178 -0
- package/templates/skills/impeccable/delight/SKILL.md +305 -0
- package/templates/skills/impeccable/distill/SKILL.md +123 -0
- package/templates/skills/impeccable/extract/SKILL.md +94 -0
- package/templates/skills/impeccable/harden/SKILL.md +357 -0
- package/templates/skills/impeccable/normalize/SKILL.md +72 -0
- package/templates/skills/impeccable/onboard/SKILL.md +248 -0
- package/templates/skills/impeccable/optimize/SKILL.md +268 -0
- package/templates/skills/impeccable/overdrive/SKILL.md +143 -0
- package/templates/skills/impeccable/polish/SKILL.md +205 -0
- package/templates/skills/impeccable/quieter/SKILL.md +104 -0
- package/templates/skills/impeccable/teach-impeccable/SKILL.md +72 -0
- package/templates/skills/impeccable/typeset/SKILL.md +117 -0
- package/templates/skills/orchestration/multi-agent/SKILL.md +533 -0
- package/templates/skills/run_skill.js +129 -0
- package/templates/skills/scrapling/SKILL.md +98 -0
- package/templates/skills/scrapling/references/api-quick-ref.md +127 -0
- package/templates/skills/scrapling/references/cookie-vault.md +56 -0
- package/templates/skills/scrapling/references/maintenance.md +79 -0
- package/templates/skills/scrapling/references/site-patterns.md +86 -0
- package/templates/skills/scrapling/references/troubleshooting.md +95 -0
- package/templates/skills/scrapling/templates/basic_fetch.py +19 -0
- package/templates/skills/scrapling/templates/parse_only.py +15 -0
- package/templates/skills/scrapling/templates/session_login.py +19 -0
- package/templates/skills/scrapling/templates/stealth_cloudflare.py +28 -0
- package/templates/skills/tools/extract-learnings/SKILL.md +77 -0
- package/templates/skills/tools/forensics/SKILL.md +89 -0
- package/templates/skills/tools/gen-docs/SKILL.md +116 -0
- package/templates/skills/tools/gen-docs/scripts/doc_generator.js +435 -0
- package/templates/skills/tools/health/SKILL.md +121 -0
- package/templates/skills/tools/lib/shared.js +98 -0
- package/templates/skills/tools/map-codebase/SKILL.md +118 -0
- package/templates/skills/tools/override-refusal/SKILL.md +53 -0
- package/templates/skills/tools/override-refusal/agents/openai.yaml +4 -0
- package/templates/skills/tools/override-refusal/scripts/refusal_rewriter.js +226 -0
- package/templates/skills/tools/verify-change/SKILL.md +143 -0
- package/templates/skills/tools/verify-change/scripts/change_analyzer.js +289 -0
- package/templates/skills/tools/verify-module/SKILL.md +130 -0
- package/templates/skills/tools/verify-module/scripts/module_scanner.js +171 -0
- package/templates/skills/tools/verify-quality/SKILL.md +163 -0
- package/templates/skills/tools/verify-quality/scripts/quality_checker.js +337 -0
- package/templates/skills/tools/verify-security/SKILL.md +146 -0
- package/templates/skills/tools/verify-security/scripts/security_scanner.js +283 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// ccg-phase-runner-launcher.mjs v4.5 P1b
|
|
3
|
+
// -----------------------------------------------------------------------------
|
|
4
|
+
// Supervised launcher for `claude -p --agent ccg/phase-runner ...` subprocess.
|
|
5
|
+
//
|
|
6
|
+
// Why this exists (codex C2 + C4):
|
|
7
|
+
// The autonomous main thread cannot itself supervise an OS subprocess —
|
|
8
|
+
// Claude is an LLM running tool calls, not a process manager. The naive
|
|
9
|
+
// `Bash(claude -p ...)` shell call is fire-and-forget: if Claude crashes
|
|
10
|
+
// after spawn, the subprocess can orphan; if the user hits Ctrl+C, nested
|
|
11
|
+
// plugin processes survive; if the subprocess hangs, no one notices.
|
|
12
|
+
//
|
|
13
|
+
// This launcher wraps the spawn so that:
|
|
14
|
+
// 1. Job state file is written atomically *before* spawn (parent_pid,
|
|
15
|
+
// cli_pid, process_group_id, cwd, cmd, started_at).
|
|
16
|
+
// 2. The CLI subprocess is launched in its own session/process group
|
|
17
|
+
// (POSIX `detached: true` → setsid()) so the whole tree can be
|
|
18
|
+
// signalled as a unit.
|
|
19
|
+
// 3. On exit (success / error / signal), terminal state is written
|
|
20
|
+
// atomically; the CCG status command can report the truth.
|
|
21
|
+
// 4. On the launcher receiving SIGINT/SIGTERM (Ctrl+C from the parent),
|
|
22
|
+
// the cancel.flag is observed cooperatively, then the process tree
|
|
23
|
+
// is killed after the grace period.
|
|
24
|
+
//
|
|
25
|
+
// Usage (called by `Bash(node ~/.claude/.ccg/scripts/ccg-phase-runner-launcher.mjs ...)`):
|
|
26
|
+
//
|
|
27
|
+
// node ccg-phase-runner-launcher.mjs \
|
|
28
|
+
// --job-id <id> \
|
|
29
|
+
// --workdir <path> \
|
|
30
|
+
// --prompt-file <path> \
|
|
31
|
+
// --tier <fast|triple|debate> \
|
|
32
|
+
// [--max-budget-usd <N>] \
|
|
33
|
+
// [--grace-ms <N>] # SIGTERM -> SIGKILL grace, default 5000
|
|
34
|
+
//
|
|
35
|
+
// Exit code: forwarded from the inner `claude -p` child. Launcher own errors
|
|
36
|
+
// surface as exit 64 (EX_USAGE) or 70 (EX_SOFTWARE).
|
|
37
|
+
//
|
|
38
|
+
// Cross-cutting:
|
|
39
|
+
// - Pure stdlib (fs / child_process / crypto / path / os). No deps.
|
|
40
|
+
// - State writes are temp + rename (atomicWriteFileSync, ported below).
|
|
41
|
+
// - Stream-json output streams to .context/jobs/<id>/progress.jsonl as the
|
|
42
|
+
// child runs; we don't transform it, we just plumb stdout/stderr through.
|
|
43
|
+
//
|
|
44
|
+
// ⚠ Schema contract for state.json (consumed by reconciler in src/utils/
|
|
45
|
+
// process-tree.ts → SupervisedJobState):
|
|
46
|
+
//
|
|
47
|
+
// {
|
|
48
|
+
// task_id, kind: "phase-runner", status,
|
|
49
|
+
// started_at, last_update,
|
|
50
|
+
// parent_pid, cli_pid, process_group_id, cwd, cmd
|
|
51
|
+
// }
|
|
52
|
+
// =============================================================================
|
|
53
|
+
|
|
54
|
+
import { spawn } from 'node:child_process'
|
|
55
|
+
import { randomBytes, randomUUID } from 'node:crypto'
|
|
56
|
+
import {
|
|
57
|
+
createWriteStream,
|
|
58
|
+
existsSync,
|
|
59
|
+
mkdirSync,
|
|
60
|
+
readFileSync,
|
|
61
|
+
realpathSync,
|
|
62
|
+
renameSync,
|
|
63
|
+
unlinkSync,
|
|
64
|
+
writeFileSync,
|
|
65
|
+
} from 'node:fs'
|
|
66
|
+
import { join } from 'node:path'
|
|
67
|
+
import { fileURLToPath } from 'node:url'
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Internal helpers (kept verbatim local to keep launcher dependency-free)
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
function atomicWriteFileSync(target, content) {
|
|
74
|
+
const rand = randomBytes(6).toString('hex')
|
|
75
|
+
const tmp = `${target}.tmp.${rand}`
|
|
76
|
+
try {
|
|
77
|
+
writeFileSync(tmp, content, 'utf-8')
|
|
78
|
+
renameSync(tmp, target)
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
try { unlinkSync(tmp) }
|
|
82
|
+
catch { /* nothing */ }
|
|
83
|
+
throw err
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isWindows() {
|
|
88
|
+
return process.platform === 'win32'
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function nowIso() {
|
|
92
|
+
return new Date().toISOString()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function ensureDir(p) {
|
|
96
|
+
if (!existsSync(p)) mkdirSync(p, { recursive: true })
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Argument parsing — minimal, KISS, no external CLI lib.
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
function parseArgs(argv) {
|
|
104
|
+
const opts = {
|
|
105
|
+
jobId: null,
|
|
106
|
+
workdir: null,
|
|
107
|
+
promptFile: null,
|
|
108
|
+
tier: 'triple',
|
|
109
|
+
maxBudgetUsd: null,
|
|
110
|
+
graceMs: 5000,
|
|
111
|
+
}
|
|
112
|
+
for (let i = 2; i < argv.length; i++) {
|
|
113
|
+
const arg = argv[i]
|
|
114
|
+
const next = () => {
|
|
115
|
+
const v = argv[++i]
|
|
116
|
+
if (v === undefined) {
|
|
117
|
+
throw new Error(`flag ${arg} requires a value`)
|
|
118
|
+
}
|
|
119
|
+
return v
|
|
120
|
+
}
|
|
121
|
+
switch (arg) {
|
|
122
|
+
case '--job-id': opts.jobId = next(); break
|
|
123
|
+
case '--workdir': opts.workdir = next(); break
|
|
124
|
+
case '--prompt-file': opts.promptFile = next(); break
|
|
125
|
+
case '--tier': opts.tier = next(); break
|
|
126
|
+
case '--max-budget-usd': opts.maxBudgetUsd = Number.parseFloat(next()); break
|
|
127
|
+
case '--grace-ms': opts.graceMs = Number.parseInt(next(), 10); break
|
|
128
|
+
case '--help':
|
|
129
|
+
case '-h':
|
|
130
|
+
printHelp()
|
|
131
|
+
process.exit(0)
|
|
132
|
+
default:
|
|
133
|
+
throw new Error(`unknown flag: ${arg}`)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (!opts.jobId) throw new Error('--job-id is required')
|
|
137
|
+
if (!opts.workdir) throw new Error('--workdir is required')
|
|
138
|
+
if (!opts.promptFile) throw new Error('--prompt-file is required')
|
|
139
|
+
if (!['fast', 'triple', 'debate'].includes(opts.tier)) {
|
|
140
|
+
throw new Error(`invalid --tier: ${opts.tier}`)
|
|
141
|
+
}
|
|
142
|
+
return opts
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function printHelp() {
|
|
146
|
+
process.stderr.write(`Usage: ccg-phase-runner-launcher.mjs [flags]
|
|
147
|
+
|
|
148
|
+
Required:
|
|
149
|
+
--job-id <id> Job identifier (becomes .context/jobs/<id>/)
|
|
150
|
+
--workdir <path> Phase workdir; subprocess cwd
|
|
151
|
+
--prompt-file <path> Prompt body file (relative to workdir or absolute)
|
|
152
|
+
|
|
153
|
+
Optional:
|
|
154
|
+
--tier <fast|triple|debate> Quality tier; maps to --max-budget-usd
|
|
155
|
+
(fast=1, triple=2, debate=5). Default: triple.
|
|
156
|
+
--max-budget-usd <N> Override per-call budget cap.
|
|
157
|
+
--grace-ms <N> SIGTERM->SIGKILL grace (default 5000).
|
|
158
|
+
|
|
159
|
+
Exits with the inner claude exit code. Own errors: 64 (usage), 70 (software).
|
|
160
|
+
`)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
// State helpers (tightly mirror src/utils/jobs.ts contract)
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
function jobDir(workdir, jobId) {
|
|
168
|
+
return join(workdir, '.context', 'jobs', jobId)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function statePath(workdir, jobId) {
|
|
172
|
+
return join(jobDir(workdir, jobId), 'state.json')
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function progressPath(workdir, jobId) {
|
|
176
|
+
return join(jobDir(workdir, jobId), 'progress.jsonl')
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function cancelFlagPath(workdir, jobId) {
|
|
180
|
+
return join(jobDir(workdir, jobId), 'cancel.flag')
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function writeState(workdir, jobId, state) {
|
|
184
|
+
ensureDir(jobDir(workdir, jobId))
|
|
185
|
+
const updated = { ...state, last_update: nowIso() }
|
|
186
|
+
atomicWriteFileSync(statePath(workdir, jobId), JSON.stringify(updated, null, 2))
|
|
187
|
+
return updated
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// Build the `claude -p` argv. Mirrors `buildPhaseRunnerBashCommand` in
|
|
192
|
+
// src/utils/quality-router.ts (single source of truth for what flags live in
|
|
193
|
+
// production phase-runner spawn).
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
const TIER_BUDGET = { fast: 1.0, triple: 2.0, debate: 5.0 }
|
|
197
|
+
|
|
198
|
+
function buildClaudeArgs({ promptFile, workdir, tier, maxBudgetUsd }) {
|
|
199
|
+
const budget = maxBudgetUsd ?? TIER_BUDGET[tier]
|
|
200
|
+
const promptBody = readFileSync(promptFile, 'utf-8')
|
|
201
|
+
return [
|
|
202
|
+
'-p', promptBody,
|
|
203
|
+
'--agent', 'ccg/phase-runner',
|
|
204
|
+
'--output-format', 'stream-json',
|
|
205
|
+
'--include-partial-messages',
|
|
206
|
+
'--verbose',
|
|
207
|
+
'--max-budget-usd', String(budget),
|
|
208
|
+
'--dangerously-skip-permissions',
|
|
209
|
+
'--add-dir', workdir,
|
|
210
|
+
]
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
// Main launcher
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
async function main(argv) {
|
|
218
|
+
let opts
|
|
219
|
+
try {
|
|
220
|
+
opts = parseArgs(argv)
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
process.stderr.write(`launcher: ${err.message}\n`)
|
|
224
|
+
printHelp()
|
|
225
|
+
return 64 // EX_USAGE
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const { jobId, workdir, graceMs } = opts
|
|
229
|
+
ensureDir(jobDir(workdir, jobId))
|
|
230
|
+
|
|
231
|
+
let claudeArgs
|
|
232
|
+
try {
|
|
233
|
+
claudeArgs = buildClaudeArgs(opts)
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
process.stderr.write(`launcher: cannot build claude args: ${err.message}\n`)
|
|
237
|
+
writeState(workdir, jobId, {
|
|
238
|
+
task_id: jobId,
|
|
239
|
+
kind: 'phase-runner',
|
|
240
|
+
status: 'failed',
|
|
241
|
+
started_at: nowIso(),
|
|
242
|
+
last_update: nowIso(),
|
|
243
|
+
summary: `launcher build args failed: ${err.message}`,
|
|
244
|
+
parent_pid: process.pid,
|
|
245
|
+
cwd: workdir,
|
|
246
|
+
})
|
|
247
|
+
return 70 // EX_SOFTWARE
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Mint a broker tx_id (v4.5 P1d, codex C3). One V4 UUID per launcher
|
|
251
|
+
// invocation; the CLI subprocess + any nested plugin Agents spawned inside
|
|
252
|
+
// it inherit the same tx_id via env, so broker.log readers can correlate
|
|
253
|
+
// every event back to this one logical phase-runner transaction.
|
|
254
|
+
// crypto.randomUUID is the only acceptable source — Math.random / Date.now /
|
|
255
|
+
// PID all leak entropy and are reused across processes.
|
|
256
|
+
const txId = randomUUID()
|
|
257
|
+
const brokerLogPath = join(workdir, '.context', 'broker.log')
|
|
258
|
+
|
|
259
|
+
// Initial state — written *before* spawn so a crash between here and spawn
|
|
260
|
+
// leaves a recoverable artifact for the reconciler.
|
|
261
|
+
const initial = writeState(workdir, jobId, {
|
|
262
|
+
task_id: jobId,
|
|
263
|
+
kind: 'phase-runner',
|
|
264
|
+
status: 'running',
|
|
265
|
+
started_at: nowIso(),
|
|
266
|
+
last_update: nowIso(),
|
|
267
|
+
parent_pid: process.pid,
|
|
268
|
+
cwd: workdir,
|
|
269
|
+
cmd: `claude ${claudeArgs.slice(0, 6).join(' ')} ... [redacted prompt]`,
|
|
270
|
+
broker_tx_id: txId,
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
// Spawn the child. `detached: true` on POSIX calls setsid() so the child
|
|
274
|
+
// gets its own session/group → we can signal the whole tree later via -pgid.
|
|
275
|
+
// On Windows, `detached: true` calls CreateProcess with DETACHED_PROCESS;
|
|
276
|
+
// we still rely on `taskkill /T /F` for tree termination (codeagent-wrapper
|
|
277
|
+
// precedent — see executor.go:1421).
|
|
278
|
+
//
|
|
279
|
+
// env inheritance: we extend process.env (parent of the launcher) with the
|
|
280
|
+
// broker-correlation triplet so phase-runner subagent code + nested plugin
|
|
281
|
+
// spawns can emit broker events under the same tx_id.
|
|
282
|
+
const child = spawn('claude', claudeArgs, {
|
|
283
|
+
cwd: workdir,
|
|
284
|
+
detached: !isWindows(),
|
|
285
|
+
windowsHide: true,
|
|
286
|
+
// Pipe stdout to progress file; let stderr surface to the launcher's
|
|
287
|
+
// stderr so users see auth / quota errors without grep-ing files.
|
|
288
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
289
|
+
env: {
|
|
290
|
+
...process.env,
|
|
291
|
+
CCG_BROKER_TX_ID: txId,
|
|
292
|
+
CCG_BROKER_LOG_PATH: brokerLogPath,
|
|
293
|
+
CCG_OUTER_CLI_PID: String(process.pid),
|
|
294
|
+
CCG_JOB_ID: jobId,
|
|
295
|
+
CCG_PHASE_RUNNER_TIER: opts.tier,
|
|
296
|
+
},
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
// Persist cli_pid + process_group_id ASAP — race window before spawn returns
|
|
300
|
+
// pid is closed by the time `child.pid` is set (synchronously in Node).
|
|
301
|
+
const pgid = !isWindows() ? child.pid : undefined
|
|
302
|
+
writeState(workdir, jobId, {
|
|
303
|
+
...initial,
|
|
304
|
+
cli_pid: child.pid,
|
|
305
|
+
process_group_id: pgid,
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
// Stream stdout to progress.jsonl. We append to keep crash-resume semantics
|
|
309
|
+
// (don't truncate prior bytes if the launcher itself was restarted).
|
|
310
|
+
const progressFd = openAppendStream(progressPath(workdir, jobId))
|
|
311
|
+
child.stdout.on('data', (chunk) => {
|
|
312
|
+
progressFd.write(chunk)
|
|
313
|
+
// Mirror to the launcher's stdout for the parent Bash poller.
|
|
314
|
+
process.stdout.write(chunk)
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
// Cooperative cancel + signal-driven kill-tree.
|
|
318
|
+
let cancelInjected = false
|
|
319
|
+
const tickCancelPoll = setInterval(() => {
|
|
320
|
+
if (existsSync(cancelFlagPath(workdir, jobId)) && !cancelInjected) {
|
|
321
|
+
cancelInjected = true
|
|
322
|
+
// Step 1 cooperative: many subagents poll cancel.flag themselves.
|
|
323
|
+
// Step 2 kill-tree: we still want to enforce after the grace period.
|
|
324
|
+
scheduleKillTree(child, graceMs)
|
|
325
|
+
}
|
|
326
|
+
}, 1000)
|
|
327
|
+
|
|
328
|
+
const onSignal = (sig) => {
|
|
329
|
+
process.stderr.write(`launcher: received ${sig}; writing cancel.flag + grace ${graceMs}ms\n`)
|
|
330
|
+
try {
|
|
331
|
+
atomicWriteFileSync(
|
|
332
|
+
cancelFlagPath(workdir, jobId),
|
|
333
|
+
`cancel-requested-at: ${nowIso()}\nrequested-by: launcher signal ${sig}\n`,
|
|
334
|
+
)
|
|
335
|
+
}
|
|
336
|
+
catch (err) {
|
|
337
|
+
process.stderr.write(`launcher: cancel.flag write failed: ${err.message}\n`)
|
|
338
|
+
}
|
|
339
|
+
scheduleKillTree(child, graceMs)
|
|
340
|
+
}
|
|
341
|
+
process.on('SIGINT', () => onSignal('SIGINT'))
|
|
342
|
+
process.on('SIGTERM', () => onSignal('SIGTERM'))
|
|
343
|
+
|
|
344
|
+
// Await child exit and write terminal state.
|
|
345
|
+
const exit = await new Promise((resolve) => {
|
|
346
|
+
child.once('exit', (code, signal) => {
|
|
347
|
+
resolve({ code, signal })
|
|
348
|
+
})
|
|
349
|
+
child.once('error', (err) => {
|
|
350
|
+
process.stderr.write(`launcher: spawn error: ${err.message}\n`)
|
|
351
|
+
resolve({ code: 70, signal: null })
|
|
352
|
+
})
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
clearInterval(tickCancelPoll)
|
|
356
|
+
progressFd.end()
|
|
357
|
+
|
|
358
|
+
const terminalStatus
|
|
359
|
+
= exit.code === 0
|
|
360
|
+
? 'done'
|
|
361
|
+
: cancelInjected
|
|
362
|
+
? 'canceled'
|
|
363
|
+
: 'failed'
|
|
364
|
+
|
|
365
|
+
writeState(workdir, jobId, {
|
|
366
|
+
...initial,
|
|
367
|
+
cli_pid: child.pid,
|
|
368
|
+
process_group_id: pgid,
|
|
369
|
+
status: terminalStatus,
|
|
370
|
+
summary: `exit code ${exit.code}${exit.signal ? ` (signal ${exit.signal})` : ''}`,
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
return typeof exit.code === 'number' ? exit.code : 70
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// ---------------------------------------------------------------------------
|
|
377
|
+
// kill-tree implementation (POSIX -pgid + Windows taskkill /T /F).
|
|
378
|
+
// Inline rather than imported because this script is shipped as a flat .mjs
|
|
379
|
+
// to ~/.claude/.ccg/scripts/ — no transpile pipeline.
|
|
380
|
+
// ---------------------------------------------------------------------------
|
|
381
|
+
|
|
382
|
+
function scheduleKillTree(child, graceMs) {
|
|
383
|
+
if (!child || !child.pid) return
|
|
384
|
+
const pid = child.pid
|
|
385
|
+
|
|
386
|
+
// Phase 1: gentle SIGTERM (POSIX) or taskkill /T (Windows, no /F).
|
|
387
|
+
try {
|
|
388
|
+
if (isWindows()) {
|
|
389
|
+
spawn('taskkill', ['/T', '/PID', String(pid)], {
|
|
390
|
+
stdio: 'ignore',
|
|
391
|
+
windowsHide: true,
|
|
392
|
+
})
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
// detached children get their own pgid == pid, so kill(-pid) hits the group.
|
|
396
|
+
try { process.kill(-pid, 'SIGTERM') }
|
|
397
|
+
catch { try { process.kill(pid, 'SIGTERM') } catch { /* gone */ } }
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
catch {
|
|
401
|
+
// Don't let signal failure prevent the forced phase.
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Phase 2 timer: if the child hasn't exited within graceMs, force kill.
|
|
405
|
+
setTimeout(() => {
|
|
406
|
+
if (child.exitCode !== null) return
|
|
407
|
+
try {
|
|
408
|
+
if (isWindows()) {
|
|
409
|
+
spawn('taskkill', ['/T', '/F', '/PID', String(pid)], {
|
|
410
|
+
stdio: 'ignore',
|
|
411
|
+
windowsHide: true,
|
|
412
|
+
})
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
try { process.kill(-pid, 'SIGKILL') }
|
|
416
|
+
catch { try { process.kill(pid, 'SIGKILL') } catch { /* gone */ } }
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
catch {
|
|
420
|
+
// Best effort — exhausted options.
|
|
421
|
+
}
|
|
422
|
+
}, graceMs).unref?.()
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ---------------------------------------------------------------------------
|
|
426
|
+
// Append-mode write stream wrapper (no fs.WriteStream import for clarity)
|
|
427
|
+
// ---------------------------------------------------------------------------
|
|
428
|
+
|
|
429
|
+
function openAppendStream(path) {
|
|
430
|
+
return createWriteStream(path, { flags: 'a' })
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// ---------------------------------------------------------------------------
|
|
434
|
+
// Entry point — only run main() when invoked as a script (not when imported
|
|
435
|
+
// for unit tests). Detection via import.meta.url vs the resolved argv[1].
|
|
436
|
+
// ---------------------------------------------------------------------------
|
|
437
|
+
|
|
438
|
+
function isMainModule() {
|
|
439
|
+
if (!process.argv[1]) return false
|
|
440
|
+
try {
|
|
441
|
+
const here = fileURLToPath(import.meta.url)
|
|
442
|
+
return realpathSync(here) === realpathSync(process.argv[1])
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
return false
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (isMainModule()) {
|
|
450
|
+
main(process.argv)
|
|
451
|
+
.then((code) => {
|
|
452
|
+
process.exit(code)
|
|
453
|
+
})
|
|
454
|
+
.catch((err) => {
|
|
455
|
+
process.stderr.write(`launcher: fatal: ${err.stack || err.message}\n`)
|
|
456
|
+
process.exit(70)
|
|
457
|
+
})
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Test surface — exposed for unit-test consumption via dynamic import().
|
|
461
|
+
// Only the pure helpers; main() is integration-tested separately.
|
|
462
|
+
export const ccgPhaseRunnerLauncherExports = {
|
|
463
|
+
parseArgs,
|
|
464
|
+
buildClaudeArgs,
|
|
465
|
+
TIER_BUDGET,
|
|
466
|
+
atomicWriteFileSync,
|
|
467
|
+
}
|