@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.
@@ -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.2",
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
+ });