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,510 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ccg-hook: session-state
|
|
3
|
+
// SessionStart Hook — auto-inject CCG project memory into a fresh session.
|
|
4
|
+
//
|
|
5
|
+
// Problem this solves (CCG v4.0 dogfood Q6 + GSD gsd-session-state.sh parity):
|
|
6
|
+
// After /clear or a brand-new session, the orchestrator has zero memory of
|
|
7
|
+
// the project's roadmap state. Users had to manually paste a "resume" file
|
|
8
|
+
// (see .ccg/SESSION-RESUME.md) to get going. This hook automates it: when a
|
|
9
|
+
// session starts in a CCG project (cwd has .ccg/roadmap.md), it injects a
|
|
10
|
+
// ≤200-token summary describing project name, active phase, and next action.
|
|
11
|
+
//
|
|
12
|
+
// Hook contract (Claude Code SessionStart event):
|
|
13
|
+
// stdin : JSON with at least { hookEventName, session_id, cwd? }
|
|
14
|
+
// cwd may be absent — we fall back to process.cwd().
|
|
15
|
+
// stdout : JSON
|
|
16
|
+
// { hookSpecificOutput: { hookEventName: 'SessionStart',
|
|
17
|
+
// additionalContext: '<string>' } }
|
|
18
|
+
// Empty / missing additionalContext means "no injection". For non-CCG
|
|
19
|
+
// projects we exit cleanly without writing anything (noop).
|
|
20
|
+
//
|
|
21
|
+
// Failure policy: never throw; never block a session start. Any parse error or
|
|
22
|
+
// missing file degrades to a smaller-but-still-useful summary, or to a noop.
|
|
23
|
+
|
|
24
|
+
'use strict'
|
|
25
|
+
|
|
26
|
+
const fs = require('fs')
|
|
27
|
+
const path = require('path')
|
|
28
|
+
const crypto = require('crypto')
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Pure helpers (exported for unit tests via ccgSessionStateHookExports)
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Extract roadmap.md head metadata: project name, started, last updated.
|
|
36
|
+
*
|
|
37
|
+
* Roadmap convention (see .ccg/roadmap.md): bold-tagged key-value lines such as
|
|
38
|
+
* **Project**: ccg-workflow v4.0
|
|
39
|
+
* **Started**: 2026-05-03
|
|
40
|
+
* **Last Updated**: 2026-05-04
|
|
41
|
+
* Lines may appear in any order within the first ~20 lines. Anything we cannot
|
|
42
|
+
* locate yields undefined — callers must tolerate that.
|
|
43
|
+
*/
|
|
44
|
+
function parseRoadmapHead(text) {
|
|
45
|
+
const head = text.split(/\r?\n/).slice(0, 30).join('\n')
|
|
46
|
+
const grab = (label) => {
|
|
47
|
+
const re = new RegExp(`\\*\\*${label}\\*\\*\\s*[::]\\s*(.+)`, 'i')
|
|
48
|
+
const m = head.match(re)
|
|
49
|
+
return m ? m[1].trim() : undefined
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
project: grab('Project'),
|
|
53
|
+
started: grab('Started'),
|
|
54
|
+
lastUpdated: grab('Last Updated'),
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Parse phase headers. Each phase is denoted by `## Phase N: Title (status)`,
|
|
60
|
+
* where `N` may include a dot (e.g. 1.5) and `status` is one of completed /
|
|
61
|
+
* in_progress / pending / blocked / skipped.
|
|
62
|
+
*
|
|
63
|
+
* Returns array preserving file order. The "active" phase used for context
|
|
64
|
+
* injection is the first one whose status is `in_progress`; if none, the
|
|
65
|
+
* first `pending` phase; if all completed, null.
|
|
66
|
+
*/
|
|
67
|
+
function parsePhases(text) {
|
|
68
|
+
const re = /^##\s+Phase\s+([\d.]+)\s*:\s*(.+?)\s*(?:\[[^\]]+\])?\s*\(([^)]+)\)\s*$/gim
|
|
69
|
+
const phases = []
|
|
70
|
+
let match
|
|
71
|
+
while ((match = re.exec(text)) !== null) {
|
|
72
|
+
phases.push({
|
|
73
|
+
n: match[1],
|
|
74
|
+
title: match[2].trim(),
|
|
75
|
+
status: match[3].trim().toLowerCase(),
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
return phases
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Pick the phase whose state is most relevant for resume context.
|
|
83
|
+
* 1. First in_progress phase (resume work mid-flight)
|
|
84
|
+
* 2. Else first pending phase (next-up work)
|
|
85
|
+
* 3. Else null (every phase completed)
|
|
86
|
+
*/
|
|
87
|
+
function pickActivePhase(phases) {
|
|
88
|
+
return (
|
|
89
|
+
phases.find(p => p.status === 'in_progress')
|
|
90
|
+
|| phases.find(p => p.status === 'pending')
|
|
91
|
+
|| null
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Map a roadmap phase entry to its `.context/<dir>/SUMMARY.md` directory name.
|
|
97
|
+
*
|
|
98
|
+
* Convention used by /ccg:autonomous + phase-runner: `phase-NN-<slug>` where NN
|
|
99
|
+
* is two-digit (zero-padded for integers). Phase 1.5 keeps its decimal. Slug is
|
|
100
|
+
* the title lowercased with non-alphanumerics collapsed to dashes.
|
|
101
|
+
*
|
|
102
|
+
* We do NOT guarantee this dir exists — caller must existsSync() before reading.
|
|
103
|
+
*/
|
|
104
|
+
function phaseDirName(phase) {
|
|
105
|
+
const n = phase.n
|
|
106
|
+
const padded = /^\d+$/.test(n) ? n.padStart(2, '0') : n
|
|
107
|
+
const slug = phase.title
|
|
108
|
+
.toLowerCase()
|
|
109
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
110
|
+
.replace(/^-+|-+$/g, '')
|
|
111
|
+
return slug ? `phase-${padded}-${slug}` : `phase-${padded}`
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Lift YAML frontmatter into a flat Record<string, string>. Only handles the
|
|
116
|
+
* minimal subset that .context/<phase>/SUMMARY.md uses — scalar key/value pairs
|
|
117
|
+
* and short inline lists. Anything fancier degrades to the raw string.
|
|
118
|
+
*
|
|
119
|
+
* We deliberately do NOT pull in src/utils/phase-context.ts here — this hook
|
|
120
|
+
* runs as a standalone Node script under ~/.claude/hooks/ with no transpile
|
|
121
|
+
* step, so it must be self-contained.
|
|
122
|
+
*/
|
|
123
|
+
function parseSummaryFrontmatter(content) {
|
|
124
|
+
const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---/)
|
|
125
|
+
if (!m) return null
|
|
126
|
+
const out = {}
|
|
127
|
+
for (const raw of m[1].split(/\r?\n/)) {
|
|
128
|
+
const line = raw.trim()
|
|
129
|
+
if (!line || line.startsWith('#')) continue
|
|
130
|
+
const km = line.match(/^([A-Za-z_][A-Za-z0-9_-]*)\s*:\s*(.*)$/)
|
|
131
|
+
if (!km) continue
|
|
132
|
+
let value = km[2].trim()
|
|
133
|
+
// Strip surrounding quotes
|
|
134
|
+
if ((value.startsWith('"') && value.endsWith('"'))
|
|
135
|
+
|| (value.startsWith('\'') && value.endsWith('\''))) {
|
|
136
|
+
value = value.slice(1, -1)
|
|
137
|
+
}
|
|
138
|
+
out[km[1]] = value
|
|
139
|
+
}
|
|
140
|
+
return out
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Compose the actual additionalContext string (capped to keep main-thread
|
|
145
|
+
* context budget honored). Stays under ~200 tokens by hard-truncating at
|
|
146
|
+
* 800 chars after composition.
|
|
147
|
+
*
|
|
148
|
+
* Inputs:
|
|
149
|
+
* head — { project, started, lastUpdated } (any may be undefined)
|
|
150
|
+
* active — phase object or null
|
|
151
|
+
* summary — parsed SUMMARY.md frontmatter or null
|
|
152
|
+
* counts — { total, completed }
|
|
153
|
+
*/
|
|
154
|
+
function composeMessage(head, active, summary, counts) {
|
|
155
|
+
const lines = []
|
|
156
|
+
lines.push('[CCG] Project memory restored from .ccg/roadmap.md.')
|
|
157
|
+
|
|
158
|
+
const projectLine = []
|
|
159
|
+
if (head.project) projectLine.push(`Project: ${head.project}`)
|
|
160
|
+
if (counts.total > 0) {
|
|
161
|
+
projectLine.push(`Phases: ${counts.completed}/${counts.total} completed`)
|
|
162
|
+
}
|
|
163
|
+
if (projectLine.length) lines.push(projectLine.join(' | '))
|
|
164
|
+
|
|
165
|
+
if (!active) {
|
|
166
|
+
if (counts.total > 0 && counts.completed === counts.total) {
|
|
167
|
+
lines.push('Status: All phases completed.')
|
|
168
|
+
}
|
|
169
|
+
else if (counts.total === 0) {
|
|
170
|
+
lines.push('Status: roadmap.md present but no phases parsed.')
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
const tag = active.status === 'in_progress' ? 'Active' : 'Next'
|
|
175
|
+
lines.push(`${tag} phase: ${active.n} ${active.title} (${active.status})`)
|
|
176
|
+
if (summary) {
|
|
177
|
+
const provides = summary.provides
|
|
178
|
+
const nextAction = summary['next-action'] || summary.next_action || summary.nextAction
|
|
179
|
+
if (provides) lines.push(`Provides: ${provides}`)
|
|
180
|
+
if (nextAction) lines.push(`Next action: ${nextAction}`)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
lines.push('Read .ccg/roadmap.md for full state. Continue from the active phase or ask the user where to start.')
|
|
185
|
+
|
|
186
|
+
let msg = lines.join('\n')
|
|
187
|
+
if (msg.length > 800) msg = `${msg.slice(0, 797)}...`
|
|
188
|
+
return msg
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// v4.5 P1b — startup reconciler (inlined CJS twin of src/utils/process-tree.ts).
|
|
193
|
+
//
|
|
194
|
+
// The hook MUST stay self-contained (see top-of-file comment). We duplicate
|
|
195
|
+
// the minimal logic rather than `require('../../src/utils/process-tree')` —
|
|
196
|
+
// the hook is shipped to ~/.claude/hooks/ where TS source is unavailable.
|
|
197
|
+
//
|
|
198
|
+
// Behaviour matrix (mirrors process-tree.ts reconcileStaleJobs):
|
|
199
|
+
// - .context/jobs/* missing → no-op (return empty)
|
|
200
|
+
// - state.status terminal (done/failed/canceled) → no-op
|
|
201
|
+
// - cli_pid alive → no-op
|
|
202
|
+
// - cli_pid dead AND result.md present → adopt-result
|
|
203
|
+
// - cli_pid dead AND no result.md → mark-failed-stale
|
|
204
|
+
// - status=running but no cli_pid (legacy) → mark-failed-no-result
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
function isAlivePid(pid) {
|
|
208
|
+
if (!Number.isInteger(pid) || pid <= 0) return false
|
|
209
|
+
try {
|
|
210
|
+
process.kill(pid, 0)
|
|
211
|
+
return true
|
|
212
|
+
}
|
|
213
|
+
catch (err) {
|
|
214
|
+
if (err && err.code === 'EPERM') return true
|
|
215
|
+
return false
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function atomicWriteFileSync(target, content) {
|
|
220
|
+
const rand = crypto.randomBytes(6).toString('hex')
|
|
221
|
+
const tmp = `${target}.tmp.${rand}`
|
|
222
|
+
try {
|
|
223
|
+
fs.writeFileSync(tmp, content, 'utf-8')
|
|
224
|
+
fs.renameSync(tmp, target)
|
|
225
|
+
}
|
|
226
|
+
catch (err) {
|
|
227
|
+
try { fs.unlinkSync(tmp) }
|
|
228
|
+
catch { /* nothing to clean up */ }
|
|
229
|
+
throw err
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function reconcileStaleJobs(cwd, options) {
|
|
234
|
+
const opts = options || {}
|
|
235
|
+
const isAlive = opts.isAliveFn || isAlivePid
|
|
236
|
+
const now = typeof opts.nowMs === 'number' ? opts.nowMs : Date.now()
|
|
237
|
+
const reuseAgeMs = typeof opts.pidReuseAgeMs === 'number'
|
|
238
|
+
? opts.pidReuseAgeMs
|
|
239
|
+
: 24 * 60 * 60 * 1000
|
|
240
|
+
|
|
241
|
+
const root = path.join(cwd, '.context', 'jobs')
|
|
242
|
+
const report = { scanned: 0, entries: [] }
|
|
243
|
+
if (!fs.existsSync(root)) return report
|
|
244
|
+
|
|
245
|
+
let dirs
|
|
246
|
+
try { dirs = fs.readdirSync(root) }
|
|
247
|
+
catch { return report }
|
|
248
|
+
|
|
249
|
+
for (const id of dirs) {
|
|
250
|
+
const sub = path.join(root, id)
|
|
251
|
+
let isDir = false
|
|
252
|
+
try { isDir = fs.statSync(sub).isDirectory() }
|
|
253
|
+
catch { continue }
|
|
254
|
+
if (!isDir) continue
|
|
255
|
+
|
|
256
|
+
const statePath = path.join(sub, 'state.json')
|
|
257
|
+
if (!fs.existsSync(statePath)) continue
|
|
258
|
+
|
|
259
|
+
let state
|
|
260
|
+
try {
|
|
261
|
+
state = JSON.parse(fs.readFileSync(statePath, 'utf-8'))
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
// Corrupt — skip silently; getJob() in src will surface to the user.
|
|
265
|
+
continue
|
|
266
|
+
}
|
|
267
|
+
report.scanned += 1
|
|
268
|
+
|
|
269
|
+
if (
|
|
270
|
+
state.status === 'done'
|
|
271
|
+
|| state.status === 'failed'
|
|
272
|
+
|| state.status === 'canceled'
|
|
273
|
+
) {
|
|
274
|
+
report.entries.push({ jobId: id, action: 'no-op', reason: 'terminal status' })
|
|
275
|
+
continue
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (typeof state.cli_pid !== 'number') {
|
|
279
|
+
const updated = Object.assign({}, state, {
|
|
280
|
+
status: 'failed',
|
|
281
|
+
summary: 'reconciler: legacy job without cli_pid; cannot verify liveness',
|
|
282
|
+
last_update: new Date().toISOString(),
|
|
283
|
+
})
|
|
284
|
+
try { atomicWriteFileSync(statePath, JSON.stringify(updated, null, 2)) }
|
|
285
|
+
catch { /* swallow — never block session start */ }
|
|
286
|
+
report.entries.push({
|
|
287
|
+
jobId: id,
|
|
288
|
+
action: 'mark-failed-no-result',
|
|
289
|
+
reason: 'no cli_pid recorded',
|
|
290
|
+
})
|
|
291
|
+
continue
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const alive = isAlive(state.cli_pid)
|
|
295
|
+
let pidProbablyReused = false
|
|
296
|
+
if (alive && state.started_at) {
|
|
297
|
+
const startedMs = Date.parse(state.started_at)
|
|
298
|
+
if (Number.isFinite(startedMs) && (now - startedMs) > reuseAgeMs) {
|
|
299
|
+
pidProbablyReused = true
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (alive && !pidProbablyReused) {
|
|
304
|
+
report.entries.push({ jobId: id, action: 'no-op', reason: 'cli_pid alive' })
|
|
305
|
+
continue
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const resultPath = path.join(sub, 'result.md')
|
|
309
|
+
if (fs.existsSync(resultPath)) {
|
|
310
|
+
const updated = Object.assign({}, state, {
|
|
311
|
+
status: 'done',
|
|
312
|
+
summary: 'reconciler: cli_pid not alive; adopted result.md after orphan recovery',
|
|
313
|
+
last_update: new Date().toISOString(),
|
|
314
|
+
})
|
|
315
|
+
try { atomicWriteFileSync(statePath, JSON.stringify(updated, null, 2)) }
|
|
316
|
+
catch { /* swallow */ }
|
|
317
|
+
report.entries.push({
|
|
318
|
+
jobId: id,
|
|
319
|
+
action: 'adopt-result',
|
|
320
|
+
reason: pidProbablyReused
|
|
321
|
+
? 'pid reuse suspected; result.md present'
|
|
322
|
+
: 'cli_pid dead; result.md present',
|
|
323
|
+
})
|
|
324
|
+
continue
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const updated = Object.assign({}, state, {
|
|
328
|
+
status: 'failed',
|
|
329
|
+
summary: pidProbablyReused
|
|
330
|
+
? 'reconciler: cli_pid suspected reused; no result.md found'
|
|
331
|
+
: 'reconciler: cli_pid dead; no result.md found',
|
|
332
|
+
last_update: new Date().toISOString(),
|
|
333
|
+
})
|
|
334
|
+
try { atomicWriteFileSync(statePath, JSON.stringify(updated, null, 2)) }
|
|
335
|
+
catch { /* swallow */ }
|
|
336
|
+
report.entries.push({
|
|
337
|
+
jobId: id,
|
|
338
|
+
action: 'mark-failed-stale',
|
|
339
|
+
reason: pidProbablyReused
|
|
340
|
+
? 'pid reuse + no result'
|
|
341
|
+
: 'cli_pid dead + no result',
|
|
342
|
+
})
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return report
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Compose a one-line reconciler summary for injection into additionalContext.
|
|
350
|
+
* Returns null when nothing of interest happened (so the hook stays quiet for
|
|
351
|
+
* fresh / clean sessions).
|
|
352
|
+
*/
|
|
353
|
+
function summarizeReconciliation(report) {
|
|
354
|
+
if (!report || report.scanned === 0) return null
|
|
355
|
+
const counts = { 'mark-failed-stale': 0, 'mark-failed-no-result': 0, 'adopt-result': 0 }
|
|
356
|
+
for (const e of report.entries) {
|
|
357
|
+
if (counts[e.action] !== undefined) counts[e.action] += 1
|
|
358
|
+
}
|
|
359
|
+
const interesting = counts['mark-failed-stale']
|
|
360
|
+
+ counts['mark-failed-no-result']
|
|
361
|
+
+ counts['adopt-result']
|
|
362
|
+
if (interesting === 0) return null
|
|
363
|
+
const parts = []
|
|
364
|
+
if (counts['mark-failed-stale'])
|
|
365
|
+
parts.push(`${counts['mark-failed-stale']} stale-failed`)
|
|
366
|
+
if (counts['mark-failed-no-result'])
|
|
367
|
+
parts.push(`${counts['mark-failed-no-result']} no-pid-failed`)
|
|
368
|
+
if (counts['adopt-result'])
|
|
369
|
+
parts.push(`${counts['adopt-result']} adopted-result`)
|
|
370
|
+
return `Reconciled ${interesting}/${report.scanned} jobs: ${parts.join(', ')}.`
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Build the additionalContext string for a given workdir. Returns null if the
|
|
375
|
+
* cwd is not a CCG project (no .ccg/roadmap.md). Never throws.
|
|
376
|
+
*/
|
|
377
|
+
function buildAdditionalContext(cwd) {
|
|
378
|
+
const roadmapPath = path.join(cwd, '.ccg', 'roadmap.md')
|
|
379
|
+
if (!fs.existsSync(roadmapPath)) return null
|
|
380
|
+
|
|
381
|
+
let roadmapText
|
|
382
|
+
try {
|
|
383
|
+
roadmapText = fs.readFileSync(roadmapPath, 'utf8')
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
return null
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const head = parseRoadmapHead(roadmapText)
|
|
390
|
+
const phases = parsePhases(roadmapText)
|
|
391
|
+
const active = pickActivePhase(phases)
|
|
392
|
+
const counts = {
|
|
393
|
+
total: phases.length,
|
|
394
|
+
completed: phases.filter(p => p.status === 'completed').length,
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
let summary = null
|
|
398
|
+
if (active) {
|
|
399
|
+
const dir = phaseDirName(active)
|
|
400
|
+
const summaryPath = path.join(cwd, '.context', dir, 'SUMMARY.md')
|
|
401
|
+
if (fs.existsSync(summaryPath)) {
|
|
402
|
+
try {
|
|
403
|
+
const text = fs.readFileSync(summaryPath, 'utf8')
|
|
404
|
+
summary = parseSummaryFrontmatter(text)
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
// Fall through with summary=null
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
let baseMsg = composeMessage(head, active, summary, counts)
|
|
413
|
+
|
|
414
|
+
// v4.5 P1b: run startup reconciler over .context/jobs/* and append a one-line
|
|
415
|
+
// summary if anything was reconciled. Reconciler never throws — it swallows
|
|
416
|
+
// I/O errors so a flaky filesystem can't block session start.
|
|
417
|
+
let reconcileLine = null
|
|
418
|
+
try {
|
|
419
|
+
const report = reconcileStaleJobs(cwd)
|
|
420
|
+
reconcileLine = summarizeReconciliation(report)
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
reconcileLine = null
|
|
424
|
+
}
|
|
425
|
+
if (reconcileLine) {
|
|
426
|
+
baseMsg = `${baseMsg}\n${reconcileLine}`
|
|
427
|
+
if (baseMsg.length > 800) baseMsg = `${baseMsg.slice(0, 797)}...`
|
|
428
|
+
}
|
|
429
|
+
return baseMsg
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ---------------------------------------------------------------------------
|
|
433
|
+
// Entry point — only runs when this file is invoked directly (not on import).
|
|
434
|
+
// ---------------------------------------------------------------------------
|
|
435
|
+
|
|
436
|
+
function emit(additionalContext) {
|
|
437
|
+
const out = {
|
|
438
|
+
hookSpecificOutput: {
|
|
439
|
+
hookEventName: 'SessionStart',
|
|
440
|
+
additionalContext,
|
|
441
|
+
},
|
|
442
|
+
}
|
|
443
|
+
process.stdout.write(JSON.stringify(out))
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function main() {
|
|
447
|
+
let input = ''
|
|
448
|
+
// Timeout guard mirrors ccg-context-monitor: never hang on a stuck pipe.
|
|
449
|
+
const timer = setTimeout(() => process.exit(0), 10000)
|
|
450
|
+
|
|
451
|
+
process.stdin.setEncoding('utf8')
|
|
452
|
+
process.stdin.on('data', chunk => (input += chunk))
|
|
453
|
+
process.stdin.on('end', () => {
|
|
454
|
+
clearTimeout(timer)
|
|
455
|
+
let cwd = process.cwd()
|
|
456
|
+
try {
|
|
457
|
+
if (input.trim()) {
|
|
458
|
+
const data = JSON.parse(input)
|
|
459
|
+
if (typeof data.cwd === 'string' && data.cwd) cwd = data.cwd
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
// Bad JSON — fall back to process.cwd(). We still want to inject context
|
|
464
|
+
// for the most common case (running in the project root).
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
let message = null
|
|
468
|
+
try {
|
|
469
|
+
message = buildAdditionalContext(cwd)
|
|
470
|
+
}
|
|
471
|
+
catch {
|
|
472
|
+
message = null
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (!message) {
|
|
476
|
+
// Non-CCG project: emit nothing visible. Empty object keeps Claude Code
|
|
477
|
+
// happy and signals "no injection" without erroring.
|
|
478
|
+
process.stdout.write('{}')
|
|
479
|
+
process.exit(0)
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
emit(message)
|
|
483
|
+
process.exit(0)
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
process.stdin.on('error', () => process.exit(0))
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Detect "imported as a module" (Node test harness) vs. "executed as script".
|
|
490
|
+
// require.main === module is true only when invoked via `node ccg-session-state.js`.
|
|
491
|
+
if (require.main === module) {
|
|
492
|
+
main()
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Test surface — kept on a single object so the production hook surface stays
|
|
496
|
+
// minimal. Consumed by sessionStateHook.test.ts.
|
|
497
|
+
module.exports = {
|
|
498
|
+
parseRoadmapHead,
|
|
499
|
+
parsePhases,
|
|
500
|
+
pickActivePhase,
|
|
501
|
+
phaseDirName,
|
|
502
|
+
parseSummaryFrontmatter,
|
|
503
|
+
composeMessage,
|
|
504
|
+
buildAdditionalContext,
|
|
505
|
+
// v4.5 P1b additions:
|
|
506
|
+
isAlivePid,
|
|
507
|
+
atomicWriteFileSync,
|
|
508
|
+
reconcileStaleJobs,
|
|
509
|
+
summarizeReconciliation,
|
|
510
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ccg-hook: statusline
|
|
3
|
+
// Claude Code Statusline - CCG Edition
|
|
4
|
+
// Shows: model | context usage | git branch | session id (last 4)
|
|
5
|
+
//
|
|
6
|
+
// Crucial side effect: writes context metrics to
|
|
7
|
+
// {os.tmpdir()}/claude-ctx-{session_id}.json
|
|
8
|
+
// which the ccg-context-monitor.js PostToolUse hook reads to inject
|
|
9
|
+
// agent-facing warnings when context usage is high. The two hooks form
|
|
10
|
+
// a producer/consumer pair on the bridge file.
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const os = require('os');
|
|
15
|
+
const { execSync } = require('child_process');
|
|
16
|
+
|
|
17
|
+
// --- git branch (best-effort, silent on failure) ----------------------------
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Read the current git branch for the given directory. Returns '' on failure
|
|
21
|
+
* (not a repo, git not installed, detached HEAD with no branch, etc).
|
|
22
|
+
* Wrapped tightly so a slow/missing git never breaks the statusline.
|
|
23
|
+
*/
|
|
24
|
+
function readGitBranch(dir) {
|
|
25
|
+
if (!dir || typeof dir !== 'string') return '';
|
|
26
|
+
try {
|
|
27
|
+
// --short prints the symbolic ref name without `refs/heads/` prefix.
|
|
28
|
+
// Returns non-zero exit when detached; we catch and return ''.
|
|
29
|
+
const out = execSync('git symbolic-ref --short HEAD', {
|
|
30
|
+
cwd: dir,
|
|
31
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
32
|
+
timeout: 500,
|
|
33
|
+
windowsHide: true,
|
|
34
|
+
}).toString().trim();
|
|
35
|
+
if (!out) return '';
|
|
36
|
+
// Defensive sanity bound: branch names rarely exceed 60 chars in practice.
|
|
37
|
+
if (out.length > 80 || /[\s\\"<>]/.test(out)) return '';
|
|
38
|
+
return out;
|
|
39
|
+
} catch {
|
|
40
|
+
return '';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// --- core renderer ----------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
function runStatusline() {
|
|
47
|
+
let input = '';
|
|
48
|
+
// Timeout guard: if stdin doesn't close within 3s (e.g. pipe issues on
|
|
49
|
+
// Windows/Git Bash), exit silently instead of hanging.
|
|
50
|
+
const stdinTimeout = setTimeout(() => process.exit(0), 3000);
|
|
51
|
+
process.stdin.setEncoding('utf8');
|
|
52
|
+
process.stdin.on('data', chunk => input += chunk);
|
|
53
|
+
process.stdin.on('end', () => {
|
|
54
|
+
clearTimeout(stdinTimeout);
|
|
55
|
+
try {
|
|
56
|
+
const data = JSON.parse(input);
|
|
57
|
+
const model = data.model?.display_name || 'Claude';
|
|
58
|
+
const dir = data.workspace?.current_dir || process.cwd();
|
|
59
|
+
const session = data.session_id || '';
|
|
60
|
+
const remaining = data.context_window?.remaining_percentage;
|
|
61
|
+
|
|
62
|
+
// Context window display (shows USED percentage scaled to usable context).
|
|
63
|
+
// Claude Code reserves a buffer for autocompact. By default this is ~16.5%
|
|
64
|
+
// of the total window, but users can override it via CLAUDE_CODE_AUTO_COMPACT_WINDOW
|
|
65
|
+
// (a token count). When the env var is set, compute the buffer % dynamically so
|
|
66
|
+
// the meter correctly reflects early-compaction configurations.
|
|
67
|
+
const totalCtx = data.context_window?.total_tokens || 1_000_000;
|
|
68
|
+
const acw = parseInt(process.env.CLAUDE_CODE_AUTO_COMPACT_WINDOW || '0', 10);
|
|
69
|
+
const AUTO_COMPACT_BUFFER_PCT = acw > 0
|
|
70
|
+
? Math.min(100, (acw / totalCtx) * 100)
|
|
71
|
+
: 16.5;
|
|
72
|
+
let ctx = '';
|
|
73
|
+
if (remaining != null) {
|
|
74
|
+
// Normalize: subtract buffer from remaining, scale to usable range
|
|
75
|
+
const usableRemaining = Math.max(
|
|
76
|
+
0,
|
|
77
|
+
((remaining - AUTO_COMPACT_BUFFER_PCT) / (100 - AUTO_COMPACT_BUFFER_PCT)) * 100,
|
|
78
|
+
);
|
|
79
|
+
const used = Math.max(0, Math.min(100, Math.round(100 - usableRemaining)));
|
|
80
|
+
|
|
81
|
+
// Write context metrics to bridge file for the context-monitor PostToolUse hook.
|
|
82
|
+
// The monitor reads this file to inject agent-facing warnings when context is low.
|
|
83
|
+
// Reject session IDs with path separators or traversal sequences to prevent
|
|
84
|
+
// a malicious session_id from writing files outside the temp directory.
|
|
85
|
+
const sessionSafe = session && !/[/\\]|\.\./.test(session);
|
|
86
|
+
if (sessionSafe) {
|
|
87
|
+
try {
|
|
88
|
+
const bridgePath = path.join(os.tmpdir(), `claude-ctx-${session}.json`);
|
|
89
|
+
// used_pct written to the bridge must match CC's native /context reporting:
|
|
90
|
+
// raw used = 100 - remaining_percentage (no buffer normalization applied).
|
|
91
|
+
// The normalized `used` value is correct for the statusline progress bar but
|
|
92
|
+
// would inflate the context monitor warning messages by ~13 points.
|
|
93
|
+
const rawUsedPct = Math.round(100 - remaining);
|
|
94
|
+
const bridgeData = JSON.stringify({
|
|
95
|
+
session_id: session,
|
|
96
|
+
remaining_percentage: remaining,
|
|
97
|
+
used_pct: rawUsedPct,
|
|
98
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
99
|
+
});
|
|
100
|
+
fs.writeFileSync(bridgePath, bridgeData);
|
|
101
|
+
} catch (e) {
|
|
102
|
+
// Silent fail -- bridge is best-effort, don't break statusline
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Build progress bar (10 segments)
|
|
107
|
+
const filled = Math.floor(used / 10);
|
|
108
|
+
const bar = '█'.repeat(filled) + '░'.repeat(10 - filled);
|
|
109
|
+
|
|
110
|
+
// Color based on usable context thresholds
|
|
111
|
+
if (used < 50) {
|
|
112
|
+
ctx = ` \x1b[32m${bar} ${used}%\x1b[0m`;
|
|
113
|
+
} else if (used < 65) {
|
|
114
|
+
ctx = ` \x1b[33m${bar} ${used}%\x1b[0m`;
|
|
115
|
+
} else if (used < 80) {
|
|
116
|
+
ctx = ` \x1b[38;5;208m${bar} ${used}%\x1b[0m`;
|
|
117
|
+
} else {
|
|
118
|
+
ctx = ` \x1b[5;31m! ${bar} ${used}%\x1b[0m`;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Compose CCG-style status line:
|
|
123
|
+
// <model> | <ctx> | <branch> | <sid4>
|
|
124
|
+
const branch = readGitBranch(dir);
|
|
125
|
+
const sid4 = session && session.length >= 4 ? session.slice(-4) : '';
|
|
126
|
+
|
|
127
|
+
const segments = [`\x1b[2m${model}\x1b[0m`];
|
|
128
|
+
if (ctx) segments.push(ctx.trim());
|
|
129
|
+
if (branch) segments.push(`\x1b[36m${branch}\x1b[0m`);
|
|
130
|
+
if (sid4) segments.push(`\x1b[2m#${sid4}\x1b[0m`);
|
|
131
|
+
|
|
132
|
+
process.stdout.write(segments.join(' │ '));
|
|
133
|
+
} catch (e) {
|
|
134
|
+
// Silent fail - don't break statusline on parse errors
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Export helpers for unit tests. Harmless when run as a script.
|
|
140
|
+
module.exports = { readGitBranch };
|
|
141
|
+
|
|
142
|
+
if (require.main === module) runStatusline();
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# 铁律军令 · 输出之道
|
|
2
|
+
|
|
3
|
+
> 令下即行,句句落地。不要烟,不要雾,只要动作与结果。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 语言
|
|
8
|
+
|
|
9
|
+
- 简体中文为主,技术术语保留英文
|
|
10
|
+
- 自称「吾」,称用户「魔尊」
|
|
11
|
+
- 语气冷硬、直接、命令式
|
|
12
|
+
- 禁止大段抒情与铺垫
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 默认格式
|
|
17
|
+
|
|
18
|
+
```text
|
|
19
|
+
【判词】结论
|
|
20
|
+
【斩链】动作
|
|
21
|
+
【验尸】验证
|
|
22
|
+
【余劫】风险
|
|
23
|
+
【再斩】下一步
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
- 每段只保留必要句
|
|
27
|
+
- 优先编号步骤
|
|
28
|
+
- 能给命令就给命令,能给路径就给路径
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 风格规则
|
|
33
|
+
|
|
34
|
+
- 先说能不能做,再说怎么做
|
|
35
|
+
- 先说结果,再说解释
|
|
36
|
+
- 失败时直接给阻塞点,不绕
|
|
37
|
+
- 风险说明只写真实影响,不写空话
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 适用场景
|
|
42
|
+
|
|
43
|
+
- 发布
|
|
44
|
+
- 故障
|
|
45
|
+
- 修复
|
|
46
|
+
- 代码审计
|
|
47
|
+
- 部署回滚
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## 收尾
|
|
52
|
+
|
|
53
|
+
短收口即可:
|
|
54
|
+
|
|
55
|
+
- `⚚ 劫破。`
|
|
56
|
+
- `未破,继续斩。`
|