ccgx-workflow 2.4.1 → 2.5.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.
Files changed (46) hide show
  1. package/README.md +134 -277
  2. package/README.zh-CN.md +134 -272
  3. package/dist/chunks/version-build.mjs +1 -1
  4. package/dist/cli.mjs +1 -1
  5. package/dist/index.d.mts +709 -16
  6. package/dist/index.d.ts +709 -16
  7. package/dist/index.mjs +1061 -30
  8. package/dist/shared/{ccgx-workflow.j1spUsik.mjs → ccgx-workflow.CdHnJLak.mjs} +106 -22
  9. package/package.json +1 -1
  10. package/templates/commands/agents/code-fixer.md +6 -6
  11. package/templates/commands/agents/phase-runner.md +46 -14
  12. package/templates/commands/agents/plan-checker.md +10 -0
  13. package/templates/commands/analyze.md +66 -25
  14. package/templates/commands/autonomous.md +428 -225
  15. package/templates/commands/cancel.md +9 -0
  16. package/templates/commands/codex-exec.md +12 -11
  17. package/templates/commands/context.md +14 -0
  18. package/templates/commands/debate.md +10 -6
  19. package/templates/commands/execute.md +76 -28
  20. package/templates/commands/optimize.md +53 -25
  21. package/templates/commands/plan.md +78 -28
  22. package/templates/commands/review.md +26 -19
  23. package/templates/commands/spec-impl.md +68 -127
  24. package/templates/commands/spec-plan.md +61 -82
  25. package/templates/commands/spec-research.md +35 -92
  26. package/templates/commands/spec-review.md +34 -119
  27. package/templates/commands/status.md +1 -0
  28. package/templates/commands/team-exec.md +45 -13
  29. package/templates/commands/team.md +64 -167
  30. package/templates/commands/test.md +56 -34
  31. package/templates/commands/verify-work.md +36 -13
  32. package/templates/commands/verify.md +35 -0
  33. package/templates/commands/workflow.md +22 -37
  34. package/templates/hooks/ccg-loop-detector.cjs +39 -8
  35. package/templates/hooks/ccg-statusline.js +142 -2
  36. package/templates/hooks/ccg-stop-gate.cjs +248 -19
  37. package/templates/hooks/ccg-subagent-context.cjs +505 -0
  38. package/templates/scripts/ccg-state-lock.cjs +510 -0
  39. package/templates/scripts/ccg-team-schedule.cjs +328 -0
  40. package/templates/scripts/ccgx-call-plugin.mjs +494 -141
  41. package/templates/scripts/invoke-model.mjs +28 -1
  42. package/templates/scripts/task-store.cjs +614 -0
  43. package/templates/skills/tools/verify-change/SKILL.md +7 -0
  44. package/templates/skills/tools/verify-module/SKILL.md +7 -0
  45. package/templates/skills/tools/verify-quality/SKILL.md +7 -0
  46. package/templates/skills/tools/verify-security/SKILL.md +8 -0
@@ -0,0 +1,510 @@
1
+ #!/usr/bin/env node
2
+ // CCG State Lock CLI — P1-6 (O_EXCL 文件锁 + SUMMARY 合并 + 通用锁定写).
3
+ //
4
+ // Standalone Node CJS reimplementation of src/utils/state-lock.ts +
5
+ // phase-context.ts mergeSummary. The TS versions ship in ccgx-workflow's
6
+ // dist for package consumers; but /ccg:team-exec Builders run in the USER's
7
+ // project, which has no ccgx-workflow npm dependency.
8
+ //
9
+ // installer.ts copies this file to ~/.claude/.ccg/scripts/ccg-state-lock.cjs.
10
+ // Command templates call:
11
+ //
12
+ // node ~/.claude/.ccg/scripts/ccg-state-lock.cjs merge-summary \
13
+ // --workdir <wd> --phase <p> --patch-file <json> [--max-wait-ms N]
14
+ // node ~/.claude/.ccg/scripts/ccg-state-lock.cjs locked-write \
15
+ // --target <path> --content-file <file> [--max-wait-ms N]
16
+ //
17
+ // merge-summary: patch JSON = {taskId, plan, provides, affects, keyFiles,
18
+ // tests, completed, notes} (camelCase; --patch-file rather than inline args
19
+ // avoids Windows shell quoting issues). The whole lock→read→merge→atomic
20
+ // write→release cycle happens INSIDE this single short-lived process — that
21
+ // millisecond-scale hold time is the precondition for the 10s lock staleness
22
+ // threshold. NEVER split acquire/release across separate CLI invocations.
23
+ // stdout: merged summary as JSON. Exit 0 = success; 1 = bad args / IO /
24
+ // lock timeout.
25
+ //
26
+ // locked-write: generic lock + tmp+rename atomic write for single-writer
27
+ // files the Lead rewrites each wave (e.g. <WORKDIR>/.ccg/state.md).
28
+ // stdout: {"ok":true,"target":...}. Same exit-code contract.
29
+ //
30
+ // Logic must stay in lockstep with src/utils/state-lock.ts and
31
+ // src/utils/phase-context.ts (mergeSummaryFields / budget truncation).
32
+ // __tests__/stateLockCjs.test.ts pins both against shared fixtures.
33
+
34
+ 'use strict';
35
+
36
+ const fs = require('node:fs');
37
+ const path = require('node:path');
38
+ const crypto = require('node:crypto');
39
+
40
+ // ─────────────────────────────────────────────────────────────────
41
+ // Lock constants (must mirror state-lock.ts)
42
+ // ─────────────────────────────────────────────────────────────────
43
+
44
+ const RETRY_DELAY_MS = 200;
45
+ const STALE_THRESHOLD_MS = 10000;
46
+ const MAX_WAIT_MS = 30000;
47
+ const SUMMARY_TOKEN_BUDGET = 200;
48
+
49
+ const LOCK_RETRY_ERRNOS = new Set([
50
+ 'EPERM', 'EBUSY', 'EAGAIN', 'EINTR', 'EINVAL', 'EIO', 'ENOENT', 'ESTALE',
51
+ ]);
52
+
53
+ // ─────────────────────────────────────────────────────────────────
54
+ // O_EXCL lock (mirrors state-lock.ts acquireLock / releaseLock / withLock)
55
+ // ─────────────────────────────────────────────────────────────────
56
+
57
+ const heldLocks = new Set();
58
+ let exitHookInstalled = false;
59
+
60
+ function installExitHook() {
61
+ if (exitHookInstalled) return;
62
+ exitHookInstalled = true;
63
+ process.on('exit', () => {
64
+ for (const lockPath of heldLocks) {
65
+ try { fs.unlinkSync(lockPath); } catch { /* already gone */ }
66
+ }
67
+ heldLocks.clear();
68
+ });
69
+ }
70
+
71
+ function lockPathFor(target) {
72
+ return `${target}.lock`;
73
+ }
74
+
75
+ function sleepSync(ms) {
76
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
77
+ }
78
+
79
+ function acquireLock(target, opts) {
80
+ const options = opts || {};
81
+ const retryDelayMs = options.retryDelayMs != null ? options.retryDelayMs : RETRY_DELAY_MS;
82
+ const staleThresholdMs = options.staleThresholdMs != null ? options.staleThresholdMs : STALE_THRESHOLD_MS;
83
+ const maxWaitMs = options.maxWaitMs != null ? options.maxWaitMs : MAX_WAIT_MS;
84
+ const lockPath = lockPathFor(target);
85
+ const startedAt = Date.now();
86
+
87
+ while (true) {
88
+ try {
89
+ const fd = fs.openSync(lockPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY);
90
+ fs.writeSync(fd, String(process.pid));
91
+ fs.closeSync(fd);
92
+ heldLocks.add(lockPath);
93
+ installExitHook();
94
+ return lockPath;
95
+ } catch (err) {
96
+ const code = err.code || '';
97
+ if (code === 'EEXIST') {
98
+ // Steal only a stale lock (crashed holder); a fresh lock is live.
99
+ // TOCTOU-safe steal: two waiters can both pass the staleness check,
100
+ // so the loser must never unlink (it would nuke the winner's fresh
101
+ // lock). renameSync is atomic — exactly one waiter wins the steal.
102
+ let stale = false;
103
+ try {
104
+ const stat = fs.statSync(lockPath);
105
+ stale = Date.now() - stat.mtimeMs > staleThresholdMs;
106
+ } catch {
107
+ continue; // released between EEXIST and stat
108
+ }
109
+ if (stale) {
110
+ const tombstone = `${lockPath}.steal-${process.pid}-${Date.now()}`;
111
+ try {
112
+ fs.renameSync(lockPath, tombstone);
113
+ // Steal won — clean the tombstone (best effort) and race for
114
+ // the lock via the normal O_EXCL create.
115
+ try { fs.unlinkSync(tombstone); } catch { /* tombstone cleanup is best-effort */ }
116
+ continue;
117
+ } catch {
118
+ // Another waiter renamed first (ENOENT et al.) — fall through
119
+ // to the wait loop and contend for the new lock normally.
120
+ }
121
+ }
122
+ } else if (!LOCK_RETRY_ERRNOS.has(code)) {
123
+ throw err; // fatal errno — silent bypass causes lost updates
124
+ }
125
+ const waited = Date.now() - startedAt;
126
+ if (waited >= maxWaitMs) {
127
+ throw new Error(
128
+ `state-lock: ${lockPath} not acquired after waiting ${waited}ms ` +
129
+ `(budget ${maxWaitMs}ms, last errno ${code || 'EEXIST'})`
130
+ );
131
+ }
132
+ sleepSync(retryDelayMs + Math.floor(Math.random() * 50));
133
+ }
134
+ }
135
+ }
136
+
137
+ function releaseLock(lockPath) {
138
+ heldLocks.delete(lockPath);
139
+ try { fs.unlinkSync(lockPath); } catch { /* lock already gone */ }
140
+ }
141
+
142
+ function withLock(target, fn, opts) {
143
+ const lockPath = acquireLock(target, opts);
144
+ try {
145
+ return fn();
146
+ } finally {
147
+ releaseLock(lockPath);
148
+ }
149
+ }
150
+
151
+ // ─────────────────────────────────────────────────────────────────
152
+ // Atomic write (mirrors jobs.ts atomicWriteFileSync)
153
+ // ─────────────────────────────────────────────────────────────────
154
+
155
+ function atomicWriteFileSync(target, content) {
156
+ const rand = crypto.randomBytes(6).toString('hex');
157
+ const tmp = `${target}.tmp.${rand}`;
158
+ try {
159
+ fs.writeFileSync(tmp, content, 'utf-8');
160
+ fs.renameSync(tmp, target);
161
+ } catch (err) {
162
+ try { fs.unlinkSync(tmp); } catch { /* nothing to clean up */ }
163
+ throw err;
164
+ }
165
+ }
166
+
167
+ // ─────────────────────────────────────────────────────────────────
168
+ // SUMMARY.md frontmatter helpers (mirror phase-context.ts)
169
+ // ─────────────────────────────────────────────────────────────────
170
+
171
+ function sanitizePhase(phase) {
172
+ return String(phase).trim().replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');
173
+ }
174
+
175
+ function summaryFilePath(workdir, phase) {
176
+ return path.join(workdir, '.context', sanitizePhase(phase), 'SUMMARY.md');
177
+ }
178
+
179
+ function extractFrontmatter(content) {
180
+ const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
181
+ return m ? m[1] : null;
182
+ }
183
+
184
+ function unquote(s) {
185
+ if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
186
+ return s.slice(1, -1);
187
+ }
188
+ return s;
189
+ }
190
+
191
+ function parseFrontmatterFields(yaml) {
192
+ const out = {};
193
+ const lines = yaml.split(/\r?\n/);
194
+ let i = 0;
195
+ while (i < lines.length) {
196
+ const line = lines[i];
197
+ if (!line.trim() || line.trim().startsWith('#')) { i += 1; continue; }
198
+ const match = line.match(/^([A-Za-z_][A-Za-z0-9_-]*)\s*:\s*(.*)$/);
199
+ if (!match) throw new Error(`phase-context: malformed frontmatter line: ${line}`);
200
+ const key = match[1];
201
+ const rest = match[2].trim();
202
+ if (rest === '') {
203
+ const items = [];
204
+ i += 1;
205
+ while (i < lines.length && /^\s+-\s+/.test(lines[i])) {
206
+ items.push(unquote(lines[i].replace(/^\s+-\s+/, '').trim()));
207
+ i += 1;
208
+ }
209
+ out[key] = items;
210
+ continue;
211
+ }
212
+ if (rest.startsWith('[') && rest.endsWith(']')) {
213
+ const inner = rest.slice(1, -1).trim();
214
+ out[key] = inner ? inner.split(',').map((s) => unquote(s.trim())) : [];
215
+ } else if (rest === 'true' || rest === 'false') {
216
+ out[key] = rest === 'true';
217
+ } else {
218
+ out[key] = unquote(rest);
219
+ }
220
+ i += 1;
221
+ }
222
+ return out;
223
+ }
224
+
225
+ function serializeScalar(value) {
226
+ if (/[:#\[\],&*!|>'"%@`]/.test(value) || value === '' || /^\s|\s$/.test(value)) {
227
+ return `"${value.replace(/"/g, '\\"')}"`;
228
+ }
229
+ return value;
230
+ }
231
+
232
+ function serializeList(values) {
233
+ if (values.length === 0) return '[]';
234
+ return `[${values.map(serializeScalar).join(', ')}]`;
235
+ }
236
+
237
+ function toList(value) {
238
+ if (Array.isArray(value)) return value.map(String);
239
+ if (typeof value === 'string' && value) return [value];
240
+ return [];
241
+ }
242
+
243
+ function readSummaryFile(workdir, phase) {
244
+ const target = summaryFilePath(workdir, phase);
245
+ if (!fs.existsSync(target)) return null;
246
+ const fm = extractFrontmatter(fs.readFileSync(target, 'utf-8'));
247
+ if (!fm) return null;
248
+ const fields = parseFrontmatterFields(fm);
249
+ return {
250
+ phase: String(fields.phase != null ? fields.phase : phase),
251
+ plan: String(fields.plan != null ? fields.plan : ''),
252
+ provides: toList(fields.provides),
253
+ affects: toList(fields.affects),
254
+ keyFiles: toList(fields.key_files),
255
+ tests: toList(fields.tests),
256
+ completed: fields.completed === true,
257
+ completedAt: fields.completed_at ? String(fields.completed_at) : undefined,
258
+ notes: fields.notes ? String(fields.notes) : undefined,
259
+ };
260
+ }
261
+
262
+ function summaryFrontmatterText(summary) {
263
+ const lines = [
264
+ `phase: ${serializeScalar(summary.phase)}`,
265
+ `plan: ${serializeScalar(summary.plan)}`,
266
+ `provides: ${serializeList(summary.provides)}`,
267
+ `affects: ${serializeList(summary.affects)}`,
268
+ `key_files: ${serializeList(summary.keyFiles)}`,
269
+ ];
270
+ if (summary.tests && summary.tests.length) lines.push(`tests: ${serializeList(summary.tests)}`);
271
+ lines.push(`completed: ${summary.completed ? 'true' : 'false'}`);
272
+ if (summary.completedAt) lines.push(`completed_at: ${serializeScalar(summary.completedAt)}`);
273
+ if (summary.notes) lines.push(`notes: ${serializeScalar(summary.notes)}`);
274
+ return lines.join('\n');
275
+ }
276
+
277
+ function renderSummaryDocument(summary) {
278
+ const frontmatter = ['---', summaryFrontmatterText(summary), '---', ''].join('\n');
279
+ const body = [
280
+ `# Phase Summary: ${summary.phase}`,
281
+ '',
282
+ `**Plan**: ${summary.plan} `,
283
+ `**Status**: ${summary.completed ? 'completed' : 'in-progress'}`,
284
+ summary.completedAt ? `**Completed**: ${summary.completedAt}` : '',
285
+ '',
286
+ '## Provides',
287
+ ...(summary.provides.length ? summary.provides.map((p) => `- ${p}`) : ['_(none)_']),
288
+ '',
289
+ '## Affects',
290
+ ...(summary.affects.length ? summary.affects.map((a) => `- ${a}`) : ['_(none)_']),
291
+ '',
292
+ '## Key files',
293
+ ...(summary.keyFiles.length ? summary.keyFiles.map((f) => `- \`${f}\``) : ['_(none)_']),
294
+ '',
295
+ summary.notes ? `## Notes\n\n${summary.notes}\n` : '',
296
+ ].filter(Boolean).join('\n');
297
+ return frontmatter + body;
298
+ }
299
+
300
+ function summaryTokenEstimate(frontmatter) {
301
+ return Math.ceil(frontmatter.length / 3.5);
302
+ }
303
+
304
+ // ─────────────────────────────────────────────────────────────────
305
+ // Merge core (mirrors phase-context.ts mergeSummaryFields + budget)
306
+ // ─────────────────────────────────────────────────────────────────
307
+
308
+ function unionPreserveOrder(a, b) {
309
+ return [...new Set([...a, ...b])];
310
+ }
311
+
312
+ function mergeSummaryContent(existing, phase, plan, patch, nowIso) {
313
+ const base = existing || {
314
+ phase,
315
+ plan,
316
+ provides: [],
317
+ affects: [],
318
+ keyFiles: [],
319
+ tests: [],
320
+ completed: false,
321
+ };
322
+ const noteSegment = patch.notes
323
+ ? `[${patch.taskId}] ${String(patch.notes).replace(/\r?\n+/g, ' ').trim()}`
324
+ : undefined;
325
+ const mergedNotes = [base.notes, noteSegment].filter(Boolean).join(' | ') || undefined;
326
+ const completed = patch.completed != null ? patch.completed : base.completed;
327
+ return {
328
+ phase: base.phase || phase,
329
+ plan: base.plan || plan,
330
+ provides: unionPreserveOrder(base.provides, patch.provides || []),
331
+ affects: unionPreserveOrder(base.affects, patch.affects || []),
332
+ keyFiles: unionPreserveOrder(base.keyFiles, patch.keyFiles || []),
333
+ tests: unionPreserveOrder(base.tests || [], patch.tests || []),
334
+ completed,
335
+ completedAt: completed ? (nowIso || new Date().toISOString()) : base.completedAt,
336
+ notes: mergedNotes,
337
+ };
338
+ }
339
+
340
+ function enforceSummaryBudget(summary, budget) {
341
+ const cap = budget != null ? budget : SUMMARY_TOKEN_BUDGET;
342
+ let current = summary;
343
+ let guard = 0;
344
+ while (
345
+ current.notes &&
346
+ summaryTokenEstimate(summaryFrontmatterText(current)) > cap &&
347
+ guard < 50
348
+ ) {
349
+ const excess = summaryTokenEstimate(summaryFrontmatterText(current)) - cap;
350
+ const cutChars = Math.ceil(excess * 3.5) + 1;
351
+ const trimmed = current.notes.slice(0, Math.max(0, current.notes.length - cutChars));
352
+ current = { ...current, notes: trimmed ? `${trimmed}…` : undefined };
353
+ guard += 1;
354
+ }
355
+ return current;
356
+ }
357
+
358
+ // ─────────────────────────────────────────────────────────────────
359
+ // CLI
360
+ // ─────────────────────────────────────────────────────────────────
361
+
362
+ function parseArgs(argv) {
363
+ const out = { command: null, flags: {}, help: false, bad: false };
364
+ const args = argv.slice(2);
365
+ if (args[0] && !args[0].startsWith('-')) {
366
+ out.command = args.shift();
367
+ }
368
+ for (let i = 0; i < args.length; i++) {
369
+ const arg = args[i];
370
+ if (arg === '--help' || arg === '-h') {
371
+ out.help = true;
372
+ } else if (arg === '--workdir' || arg === '--phase' || arg === '--patch-file'
373
+ || arg === '--target' || arg === '--content-file' || arg === '--max-wait-ms') {
374
+ out.flags[arg.slice(2)] = args[++i];
375
+ } else {
376
+ process.stderr.write(`ccg-state-lock: unknown flag ${arg}\n`);
377
+ out.bad = true;
378
+ }
379
+ }
380
+ return out;
381
+ }
382
+
383
+ function usage() {
384
+ return [
385
+ 'Usage:',
386
+ ' node ccg-state-lock.cjs merge-summary --workdir <wd> --phase <p> --patch-file <json> [--max-wait-ms N]',
387
+ ' node ccg-state-lock.cjs locked-write --target <path> --content-file <file> [--max-wait-ms N]',
388
+ '',
389
+ 'merge-summary patch JSON: {taskId, plan, provides, affects, keyFiles, tests, completed, notes}',
390
+ 'Exit 0 = success; 1 = bad args / IO error / lock timeout.',
391
+ ].join('\n');
392
+ }
393
+
394
+ function lockOptsFromFlags(flags) {
395
+ const opts = {};
396
+ if (flags['max-wait-ms'] != null) {
397
+ const n = Number(flags['max-wait-ms']);
398
+ if (!Number.isFinite(n) || n < 0) throw new Error(`invalid --max-wait-ms: ${flags['max-wait-ms']}`);
399
+ opts.maxWaitMs = n;
400
+ // Keep retries responsive when the caller shrinks the budget (tests).
401
+ opts.retryDelayMs = Math.min(RETRY_DELAY_MS, Math.max(10, Math.floor(n / 4)));
402
+ }
403
+ return opts;
404
+ }
405
+
406
+ function cmdMergeSummary(flags) {
407
+ const workdir = flags.workdir;
408
+ const phase = flags.phase;
409
+ const patchFile = flags['patch-file'];
410
+ if (!workdir || !phase || !patchFile) {
411
+ process.stderr.write('ccg-state-lock: merge-summary requires --workdir, --phase, --patch-file\n');
412
+ return 1;
413
+ }
414
+ let patch;
415
+ try {
416
+ patch = JSON.parse(fs.readFileSync(patchFile, 'utf-8'));
417
+ } catch (err) {
418
+ process.stderr.write(`ccg-state-lock: cannot read patch file: ${err.message}\n`);
419
+ return 1;
420
+ }
421
+ if (!patch || typeof patch !== 'object' || typeof patch.taskId !== 'string' || !patch.taskId.trim()) {
422
+ process.stderr.write('ccg-state-lock: patch JSON must be an object with a non-empty "taskId"\n');
423
+ return 1;
424
+ }
425
+ try {
426
+ const opts = lockOptsFromFlags(flags);
427
+ const target = summaryFilePath(workdir, phase);
428
+ fs.mkdirSync(path.dirname(target), { recursive: true });
429
+ const merged = withLock(target, () => {
430
+ const existing = readSummaryFile(workdir, phase);
431
+ // Frontmatter keeps the RAW phase (TS mergeSummary parity); sanitize is
432
+ // path-only (summaryFilePath above).
433
+ const result = enforceSummaryBudget(
434
+ mergeSummaryContent(existing, phase, String(patch.plan || ''), patch)
435
+ );
436
+ atomicWriteFileSync(target, renderSummaryDocument(result));
437
+ return result;
438
+ }, opts);
439
+ process.stdout.write(JSON.stringify(merged) + '\n');
440
+ return 0;
441
+ } catch (err) {
442
+ process.stderr.write(`ccg-state-lock: ${err.message}\n`);
443
+ return 1;
444
+ }
445
+ }
446
+
447
+ function cmdLockedWrite(flags) {
448
+ const target = flags.target;
449
+ const contentFile = flags['content-file'];
450
+ if (!target || !contentFile) {
451
+ process.stderr.write('ccg-state-lock: locked-write requires --target, --content-file\n');
452
+ return 1;
453
+ }
454
+ let content;
455
+ try {
456
+ content = fs.readFileSync(contentFile, 'utf-8');
457
+ } catch (err) {
458
+ process.stderr.write(`ccg-state-lock: cannot read content file: ${err.message}\n`);
459
+ return 1;
460
+ }
461
+ try {
462
+ const opts = lockOptsFromFlags(flags);
463
+ fs.mkdirSync(path.dirname(target), { recursive: true });
464
+ withLock(target, () => {
465
+ atomicWriteFileSync(target, content);
466
+ }, opts);
467
+ process.stdout.write(JSON.stringify({ ok: true, target }) + '\n');
468
+ return 0;
469
+ } catch (err) {
470
+ process.stderr.write(`ccg-state-lock: ${err.message}\n`);
471
+ return 1;
472
+ }
473
+ }
474
+
475
+ function main(argv) {
476
+ const args = parseArgs(argv);
477
+ if (args.help) {
478
+ process.stderr.write(usage() + '\n');
479
+ return 0;
480
+ }
481
+ if (args.bad) {
482
+ process.stderr.write(usage() + '\n');
483
+ return 1;
484
+ }
485
+ if (args.command === 'merge-summary') return cmdMergeSummary(args.flags);
486
+ if (args.command === 'locked-write') return cmdLockedWrite(args.flags);
487
+ process.stderr.write(usage() + '\n');
488
+ return 1;
489
+ }
490
+
491
+ module.exports = {
492
+ acquireLock,
493
+ releaseLock,
494
+ withLock,
495
+ lockPathFor,
496
+ mergeSummaryContent,
497
+ enforceSummaryBudget,
498
+ summaryTokenEstimate,
499
+ parseArgs,
500
+ main,
501
+ RETRY_DELAY_MS,
502
+ STALE_THRESHOLD_MS,
503
+ MAX_WAIT_MS,
504
+ SUMMARY_TOKEN_BUDGET,
505
+ LOCK_RETRY_ERRNOS,
506
+ };
507
+
508
+ if (require.main === module) {
509
+ process.exit(main(process.argv));
510
+ }