@logicseek/hello-cc 0.1.2 → 0.1.4
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/CHANGELOG.md +145 -3
- package/README.md +4 -1
- package/README.zh-CN.md +5 -1
- package/bin/hcc.mjs +2701 -384
- package/docs/README.md +5 -3
- package/docs/README.zh-CN.md +5 -3
- package/docs/commands.md +38 -7
- package/docs/commands.zh-CN.md +31 -6
- package/docs/design.md +12 -4
- package/docs/guide.md +55 -10
- package/docs/guide.zh-CN.md +47 -9
- package/docs/implementation.md +21 -1
- package/lib/guidance.mjs +115 -0
- package/lib/package-meta.mjs +24 -0
- package/lib/release-notes.mjs +40 -0
- package/package.json +3 -1
- package/scripts/github-release.mjs +152 -0
- package/scripts/regression.mjs +965 -33
- package/scripts/release-notes.mjs +6 -38
package/lib/guidance.mjs
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export function guidanceMarkdown() {
|
|
5
|
+
return `# hello-cc Coordination Rules
|
|
6
|
+
|
|
7
|
+
This project may be edited by multiple Claude Code and Codex CLI sessions.
|
|
8
|
+
|
|
9
|
+
Use \`hcc\` as the source of truth for cross-session coordination. If the user
|
|
10
|
+
asks what other sessions are doing, do not answer from generic model knowledge.
|
|
11
|
+
Run the project-local commands below and summarize the result.
|
|
12
|
+
|
|
13
|
+
Each terminal session must use a stable peer id, for example codex-a,
|
|
14
|
+
codex-b, claude-a, claude-b, or gpu-runner-a.
|
|
15
|
+
|
|
16
|
+
Status checks:
|
|
17
|
+
- \`hcc peers\` shows known Claude/Codex/shell sessions.
|
|
18
|
+
- \`hcc task list\` shows all open tasks. Tasks stay visible until \`done\` or
|
|
19
|
+
\`abandoned\`; they are not read/unread items.
|
|
20
|
+
- \`hcc msg inbox\` shows unread messages for this session.
|
|
21
|
+
- \`hcc lock list\` shows active advisory locks.
|
|
22
|
+
- \`hcc status\` summarizes peers, tasks, locks, inbox, and recent events.
|
|
23
|
+
- \`hcc team status --task N\` summarizes explicit subtasks for a parent task.
|
|
24
|
+
|
|
25
|
+
Read-only work:
|
|
26
|
+
- Reading files, searching, inspecting \`git diff\` / \`git status\`, reviewing
|
|
27
|
+
uncommitted changes, and running non-mutating checks such as \`node --check\`
|
|
28
|
+
do not require locks.
|
|
29
|
+
- For read-only review, do not acquire file locks. If another live peer already
|
|
30
|
+
holds write locks on changed files, report that the review is only a current
|
|
31
|
+
snapshot and not a final commit-ready verdict.
|
|
32
|
+
- Use tasks, messages, events, or handoffs to make read-only review visible;
|
|
33
|
+
do not use advisory locks to reserve files you are only reading.
|
|
34
|
+
|
|
35
|
+
Review and monitoring:
|
|
36
|
+
- Reviewing another peer's work is a read-only activity. You may inspect current
|
|
37
|
+
diffs, task state, messages, handoffs, and non-mutating checks while that peer
|
|
38
|
+
keeps its write locks.
|
|
39
|
+
- If you find a concrete issue in another peer's task, proactively send that
|
|
40
|
+
peer an \`hcc msg\` with the task id, affected file or behavior, why it is a
|
|
41
|
+
problem, and a suggested fix or verification step.
|
|
42
|
+
- Continue monitoring until the peer fixes the issue, records a follow-up task,
|
|
43
|
+
or hands off. Do not silently treat a snapshot review as final approval.
|
|
44
|
+
|
|
45
|
+
Before mutating work:
|
|
46
|
+
- Register with hcc.
|
|
47
|
+
- Read current status with \`hcc status\`, \`hcc state\`, \`hcc task list\`, and \`hcc msg inbox\`.
|
|
48
|
+
- If \`hcc state\` shows a current task for this peer, continue that task until
|
|
49
|
+
handoff, done, or blocked instead of claiming a new task.
|
|
50
|
+
- Claim one task before editing or mutating shared state when this peer has no
|
|
51
|
+
current task.
|
|
52
|
+
- If work needs multiple peers, use \`hcc team plan\` and \`hcc team start\`
|
|
53
|
+
explicitly; do not create or claim hidden extra tasks.
|
|
54
|
+
|
|
55
|
+
Before editing or mutating shared resources:
|
|
56
|
+
- Acquire an advisory lock for the file, directory, module, or shared resource.
|
|
57
|
+
- If another live peer holds the lock, message that peer instead of editing.
|
|
58
|
+
- Acquire locks only before writes or shared-resource mutation, including file
|
|
59
|
+
edits, formatting, staging/committing through \`.git/index\`, intentional DB
|
|
60
|
+
data/schema changes, starting/stopping/restarting shared runtimes, or taking
|
|
61
|
+
over tmux/session/port resources.
|
|
62
|
+
- Commit-readiness checks are read-only until staging begins. Do not lock source
|
|
63
|
+
files for review. Before committing, ensure no live peer holds write locks on
|
|
64
|
+
the files to be committed, then lock \`.git/index\` only while staging and
|
|
65
|
+
committing.
|
|
66
|
+
|
|
67
|
+
During work:
|
|
68
|
+
- Keep changes scoped to the claimed task.
|
|
69
|
+
- Send progress messages when another session needs context.
|
|
70
|
+
|
|
71
|
+
Before stopping:
|
|
72
|
+
- Mark the task done or blocked.
|
|
73
|
+
- Create a handoff with changed files, tests, and remaining risks.
|
|
74
|
+
- Release locks you no longer need.
|
|
75
|
+
`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function writeGuidance(root) {
|
|
79
|
+
const guidePath = path.join(root, '.hello-cc', 'HCC.md');
|
|
80
|
+
fs.mkdirSync(path.dirname(guidePath), { recursive: true });
|
|
81
|
+
const content = guidanceMarkdown();
|
|
82
|
+
fs.writeFileSync(guidePath, content);
|
|
83
|
+
const clause = `\n<!-- hello-cc:start -->\n\n${content}\n<!-- hello-cc:end -->\n`;
|
|
84
|
+
for (const file of ['CLAUDE.md', 'AGENTS.md']) {
|
|
85
|
+
const target = path.join(root, file);
|
|
86
|
+
if (!fs.existsSync(target)) {
|
|
87
|
+
fs.writeFileSync(target, clause.trimStart());
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const existing = fs.readFileSync(target, 'utf8');
|
|
91
|
+
const updated = existing.includes('<!-- hello-cc:start -->')
|
|
92
|
+
? existing.replace(/<!-- hello-cc:start -->[\s\S]*?<!-- hello-cc:end -->/, clause.trim())
|
|
93
|
+
: `${existing.trimEnd()}\n${clause}`;
|
|
94
|
+
fs.writeFileSync(target, updated);
|
|
95
|
+
}
|
|
96
|
+
return guidePath;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function removeGuidanceBlocks(root) {
|
|
100
|
+
const changed = [];
|
|
101
|
+
for (const file of ['CLAUDE.md', 'AGENTS.md']) {
|
|
102
|
+
const target = path.join(root, file);
|
|
103
|
+
if (!fs.existsSync(target)) continue;
|
|
104
|
+
const existing = fs.readFileSync(target, 'utf8');
|
|
105
|
+
const updated = existing
|
|
106
|
+
.replace(/\n?<!-- hello-cc:start -->[\s\S]*?<!-- hello-cc:end -->\n?/g, '\n')
|
|
107
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
108
|
+
.trimEnd();
|
|
109
|
+
if (updated !== existing) {
|
|
110
|
+
fs.writeFileSync(target, updated ? `${updated}\n` : '');
|
|
111
|
+
changed.push(target);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return changed;
|
|
115
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
export function packageRoot(fromUrl = import.meta.url) {
|
|
6
|
+
return path.resolve(fileURLToPath(fromUrl), '..', '..');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function readJson(file) {
|
|
10
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function readPackageJson(root) {
|
|
14
|
+
return readJson(path.join(root, 'package.json'));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function readPackageMeta(root) {
|
|
18
|
+
const pkg = readPackageJson(root);
|
|
19
|
+
return {
|
|
20
|
+
name: pkg.name,
|
|
21
|
+
version: pkg.version,
|
|
22
|
+
packageJson: pkg
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export function normalizeVersion(version) {
|
|
2
|
+
return String(version || '').replace(/^v/, '');
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function releaseSection(markdown, version) {
|
|
6
|
+
const lines = markdown.split(/\r?\n/);
|
|
7
|
+
const heading = `## ${version}`;
|
|
8
|
+
const start = lines.findIndex((line) => line.trim() === heading);
|
|
9
|
+
if (start < 0) return null;
|
|
10
|
+
|
|
11
|
+
let end = lines.length;
|
|
12
|
+
for (let i = start + 1; i < lines.length; i += 1) {
|
|
13
|
+
if (/^##\s+/.test(lines[i])) {
|
|
14
|
+
end = i;
|
|
15
|
+
break;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return lines.slice(start, end).join('\n').trimEnd();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function validateReleaseSection(section, version) {
|
|
22
|
+
const problems = [];
|
|
23
|
+
if (!section) {
|
|
24
|
+
problems.push(`CHANGELOG.md is missing section ## ${version}`);
|
|
25
|
+
return problems;
|
|
26
|
+
}
|
|
27
|
+
if (!section.includes('### Summary')) problems.push(`## ${version} is missing ### Summary`);
|
|
28
|
+
if (!section.includes('### Highlights')) problems.push(`## ${version} is missing ### Highlights`);
|
|
29
|
+
if (section.includes('TODO') || section.includes('TBD')) problems.push(`## ${version} still contains TODO/TBD text`);
|
|
30
|
+
const bulletCount = (section.match(/^- /gm) || []).length;
|
|
31
|
+
if (bulletCount < 2) problems.push(`## ${version} should include at least two release-note bullets`);
|
|
32
|
+
return problems;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function repoFromPackage(pkg) {
|
|
36
|
+
const repo = typeof pkg.repository === 'string' ? pkg.repository : pkg.repository?.url;
|
|
37
|
+
if (!repo) return null;
|
|
38
|
+
const match = repo.match(/github\.com[:/](.+?\/.+?)(?:\.git)?$/);
|
|
39
|
+
return match ? match[1] : null;
|
|
40
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@logicseek/hello-cc",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Local control plane for coordinating Claude Code, Codex, and other coding CLI sessions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
"scripts": {
|
|
25
25
|
"release:notes": "node ./scripts/release-notes.mjs",
|
|
26
26
|
"release:check": "node ./scripts/release-notes.mjs --check",
|
|
27
|
+
"release:github": "node ./scripts/github-release.mjs",
|
|
28
|
+
"release:github:dry-run": "node ./scripts/github-release.mjs --dry-run",
|
|
27
29
|
"smoke": "node ./bin/hcc.mjs --help",
|
|
28
30
|
"test": "node ./scripts/regression.mjs",
|
|
29
31
|
"test:regression": "node ./scripts/regression.mjs"
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import process from 'node:process';
|
|
6
|
+
import { packageRoot, readPackageJson } from '../lib/package-meta.mjs';
|
|
7
|
+
import {
|
|
8
|
+
normalizeVersion,
|
|
9
|
+
releaseSection,
|
|
10
|
+
repoFromPackage,
|
|
11
|
+
validateReleaseSection
|
|
12
|
+
} from '../lib/release-notes.mjs';
|
|
13
|
+
|
|
14
|
+
const repoRoot = packageRoot(import.meta.url);
|
|
15
|
+
const githubApi = 'https://api.github.com';
|
|
16
|
+
|
|
17
|
+
function usage() {
|
|
18
|
+
console.log(`github-release
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
node scripts/github-release.mjs [--version VERSION] [--repo OWNER/REPO] [--dry-run]
|
|
22
|
+
|
|
23
|
+
Creates or updates the GitHub Release for vVERSION using the matching
|
|
24
|
+
CHANGELOG.md section as the release body.
|
|
25
|
+
|
|
26
|
+
Authentication:
|
|
27
|
+
Set GH_TOKEN or GITHUB_TOKEN to a token with repo release permissions.
|
|
28
|
+
`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseArgs(argv) {
|
|
32
|
+
const opts = { dryRun: false, help: false, repo: null, version: null };
|
|
33
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
34
|
+
const arg = argv[i];
|
|
35
|
+
if (arg === '--help' || arg === '-h') {
|
|
36
|
+
opts.help = true;
|
|
37
|
+
} else if (arg === '--dry-run') {
|
|
38
|
+
opts.dryRun = true;
|
|
39
|
+
} else if (arg === '--repo') {
|
|
40
|
+
opts.repo = argv[++i];
|
|
41
|
+
if (opts.repo === undefined) throw new Error('Missing value for --repo');
|
|
42
|
+
} else if (arg.startsWith('--repo=')) {
|
|
43
|
+
opts.repo = arg.slice('--repo='.length);
|
|
44
|
+
} else if (arg === '--version') {
|
|
45
|
+
opts.version = argv[++i];
|
|
46
|
+
if (opts.version === undefined) throw new Error('Missing value for --version');
|
|
47
|
+
} else if (arg.startsWith('--version=')) {
|
|
48
|
+
opts.version = arg.slice('--version='.length);
|
|
49
|
+
} else {
|
|
50
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return opts;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function envToken() {
|
|
57
|
+
return process.env.GH_TOKEN || process.env.GITHUB_TOKEN || '';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function githubRequest(method, route, token, body = null) {
|
|
61
|
+
const response = await fetch(`${githubApi}${route}`, {
|
|
62
|
+
method,
|
|
63
|
+
headers: {
|
|
64
|
+
Accept: 'application/vnd.github+json',
|
|
65
|
+
Authorization: `Bearer ${token}`,
|
|
66
|
+
'Content-Type': 'application/json',
|
|
67
|
+
'User-Agent': 'hello-cc-release-script'
|
|
68
|
+
},
|
|
69
|
+
body: body ? JSON.stringify(body) : undefined
|
|
70
|
+
});
|
|
71
|
+
const text = await response.text();
|
|
72
|
+
const data = text ? JSON.parse(text) : null;
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
const detail = data?.message ? `: ${data.message}` : '';
|
|
75
|
+
const error = new Error(`GitHub API ${method} ${route} failed with ${response.status}${detail}`);
|
|
76
|
+
error.status = response.status;
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
return data;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function main() {
|
|
83
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
84
|
+
if (opts.help) {
|
|
85
|
+
usage();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const pkg = readPackageJson(repoRoot);
|
|
90
|
+
const version = normalizeVersion(opts.version || pkg.version);
|
|
91
|
+
const tag = `v${version}`;
|
|
92
|
+
const repo = opts.repo || process.env.GITHUB_REPOSITORY || repoFromPackage(pkg);
|
|
93
|
+
if (!repo) throw new Error('Missing repository; pass --repo OWNER/REPO');
|
|
94
|
+
|
|
95
|
+
const changelog = fs.readFileSync(path.join(repoRoot, 'CHANGELOG.md'), 'utf8');
|
|
96
|
+
const body = releaseSection(changelog, version);
|
|
97
|
+
const problems = validateReleaseSection(body, version);
|
|
98
|
+
if (problems.length) {
|
|
99
|
+
console.error(problems.join('\n'));
|
|
100
|
+
process.exitCode = 1;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const payload = {
|
|
105
|
+
tag_name: tag,
|
|
106
|
+
name: tag,
|
|
107
|
+
body,
|
|
108
|
+
draft: false,
|
|
109
|
+
prerelease: false
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
if (opts.dryRun) {
|
|
113
|
+
console.log(JSON.stringify({
|
|
114
|
+
ok: true,
|
|
115
|
+
dry_run: true,
|
|
116
|
+
repo,
|
|
117
|
+
tag,
|
|
118
|
+
name: payload.name,
|
|
119
|
+
body_length: body.length,
|
|
120
|
+
body_preview: body.split(/\r?\n/).slice(0, 8).join('\n')
|
|
121
|
+
}, null, 2));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const token = envToken();
|
|
126
|
+
if (!token) throw new Error('Missing GH_TOKEN or GITHUB_TOKEN');
|
|
127
|
+
|
|
128
|
+
let existing = null;
|
|
129
|
+
try {
|
|
130
|
+
existing = await githubRequest('GET', `/repos/${repo}/releases/tags/${tag}`, token);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
if (err.status !== 404) throw err;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const result = existing
|
|
136
|
+
? await githubRequest('PATCH', `/repos/${repo}/releases/${existing.id}`, token, payload)
|
|
137
|
+
: await githubRequest('POST', `/repos/${repo}/releases`, token, payload);
|
|
138
|
+
|
|
139
|
+
console.log(JSON.stringify({
|
|
140
|
+
ok: true,
|
|
141
|
+
action: existing ? 'updated' : 'created',
|
|
142
|
+
repo,
|
|
143
|
+
tag,
|
|
144
|
+
url: result.html_url,
|
|
145
|
+
body_length: body.length
|
|
146
|
+
}, null, 2));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
main().catch((err) => {
|
|
150
|
+
console.error(err.message || String(err));
|
|
151
|
+
process.exitCode = 1;
|
|
152
|
+
});
|