@logicseek/hello-cc 0.1.4 → 0.1.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logicseek/hello-cc",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Local control plane for coordinating Claude Code, Codex, and other coding CLI sessions.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -7,6 +7,7 @@ import process from 'node:process';
7
7
  import { spawnSync } from 'node:child_process';
8
8
  import { setTimeout as sleep } from 'node:timers/promises';
9
9
  import { DatabaseSync } from 'node:sqlite';
10
+ import { Readable } from 'node:stream';
10
11
  import WebSocket from 'ws';
11
12
 
12
13
  const repoRoot = path.resolve(import.meta.dirname, '..');
@@ -458,17 +459,21 @@ function assertLegacySchemaMigration() {
458
459
  for (const column of ['parent_id', 'team_role']) {
459
460
  if (!taskColumns.has(column)) fail(`legacy migration did not add tasks.${column}`);
460
461
  }
462
+ const lockColumns = new Set(db.prepare('PRAGMA table_info(locks)').all().map((row) => row.name));
463
+ for (const column of ['base_resource', 'scope']) {
464
+ if (!lockColumns.has(column)) fail(`legacy migration did not add locks.${column}`);
465
+ }
461
466
  const message = db.prepare('SELECT id, thread_id FROM messages WHERE body = ?').get('legacy-message');
462
467
  if (!message || message.thread_id !== message.id) {
463
468
  fail(`legacy message thread_id not backfilled:\n${JSON.stringify(message, null, 2)}`);
464
469
  }
465
470
  const metaVersion = db.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get()?.value;
466
471
  const pragmaVersion = db.prepare('PRAGMA user_version').get().user_version;
467
- if (metaVersion !== '4' || pragmaVersion !== 4) {
472
+ if (metaVersion !== '5' || pragmaVersion !== 5) {
468
473
  fail(`schema version not synchronized: meta=${metaVersion} pragma=${pragmaVersion}`);
469
474
  }
470
475
  const migrations = db.prepare('SELECT version, name FROM schema_migrations ORDER BY version').all();
471
- if (migrations.length !== 4 || migrations[3].version !== 4 || migrations[3].name !== 'team task hierarchy') {
476
+ if (migrations.length !== 5 || migrations[4].version !== 5 || migrations[4].name !== 'scoped advisory locks') {
472
477
  fail(`schema migrations history wrong:\n${JSON.stringify(migrations, null, 2)}`);
473
478
  }
474
479
  } finally {
@@ -500,7 +505,7 @@ function assertRegisteredProjectDbMigration() {
500
505
  fail(`registered project DB was not migrated:\n${JSON.stringify([...taskColumns], null, 2)}`);
501
506
  }
502
507
  const version = db.prepare('PRAGMA user_version').get().user_version;
503
- if (version !== 4) fail(`registered project DB user_version wrong: ${version}`);
508
+ if (version !== 5) fail(`registered project DB user_version wrong: ${version}`);
504
509
  } finally {
505
510
  db.close();
506
511
  }
@@ -521,7 +526,7 @@ function assertFutureSchemaMigrationHistoryRejected() {
521
526
  key TEXT PRIMARY KEY,
522
527
  value TEXT NOT NULL
523
528
  );
524
- INSERT INTO meta(key, value) VALUES ('schema_version', '4');
529
+ INSERT INTO meta(key, value) VALUES ('schema_version', '5');
525
530
  CREATE TABLE schema_migrations (
526
531
  version INTEGER PRIMARY KEY,
527
532
  name TEXT NOT NULL,
@@ -534,7 +539,7 @@ function assertFutureSchemaMigrationHistoryRejected() {
534
539
  db.close();
535
540
  }
536
541
  const result = runMaybe(process.execPath, [hccBin, '--root', futureRoot, 'status', '--peer', 'future-schema-check'], { env });
537
- if (result.status === 0 || !`${result.stdout}\n${result.stderr}`.includes('Database schema version 999 is newer than this hcc (4)')) {
542
+ if (result.status === 0 || !`${result.stdout}\n${result.stderr}`.includes('Database schema version 999 is newer than this hcc (5)')) {
538
543
  fail(`future schema migration history was not rejected:\nstdout=${result.stdout}\nstderr=${result.stderr}`);
539
544
  }
540
545
  } finally {
@@ -676,6 +681,11 @@ function assertGuidanceLockPolicy(file) {
676
681
  'Do not silently treat a snapshot review as final approval',
677
682
  'Before mutating work:',
678
683
  'Before editing or mutating shared resources:',
684
+ 'module scope',
685
+ 'Locks are coordination signals',
686
+ '--scope db-schema',
687
+ '--scope web-ui',
688
+ 'narrower scoped locks',
679
689
  'Commit-readiness checks are read-only until staging begins',
680
690
  'lock `.git/index` only while staging and'
681
691
  ]) {
@@ -850,6 +860,13 @@ function tmuxStreamNodes() {
850
860
  .map((name) => path.join(dir, name));
851
861
  }
852
862
 
863
+ function libModuleFiles() {
864
+ return fs.readdirSync(path.join(repoRoot, 'lib'))
865
+ .filter((name) => name.endsWith('.mjs'))
866
+ .sort()
867
+ .map((name) => path.join('lib', name));
868
+ }
869
+
853
870
  async function expectBoundedTmuxStream(label) {
854
871
  let nodes = [];
855
872
  await waitFor(() => {
@@ -1158,6 +1175,13 @@ async function dbWorkflow() {
1158
1175
  if (stateAfterAck.automation.next_action.kind !== 'lock.acquire') {
1159
1176
  fail(`state automation did not suggest lock acquire after ack:\n${JSON.stringify(stateAfterAck.automation, null, 2)}`);
1160
1177
  }
1178
+ const readOnlyState = hccJson(['state', '--peer', 'codex-a', '--resource', 'src/read-only', '--intent', 'review']);
1179
+ if (readOnlyState.automation.next_action.kind === 'lock.acquire' || readOnlyState.automation.phase === 'acquire_lock') {
1180
+ fail(`read-only state suggested acquiring a lock:\n${JSON.stringify(readOnlyState.automation, null, 2)}`);
1181
+ }
1182
+ if (!(readOnlyState.automation.warnings || []).some((warning) => warning.includes('read-only'))) {
1183
+ fail(`read-only state did not explain no-lock behavior:\n${JSON.stringify(readOnlyState.automation, null, 2)}`);
1184
+ }
1161
1185
  const queuedTask = hcc(['task', 'create', '--from', 'human', '--to', 'codex-a', '--title', 'queued while busy']);
1162
1186
  const queuedTaskMatch = queuedTask.match(/created task #(\d+):/);
1163
1187
  if (!queuedTaskMatch) fail(`cannot parse queued task id: ${queuedTask}`);
@@ -1181,6 +1205,93 @@ async function dbWorkflow() {
1181
1205
  if (!nextAgain.includes(`current task #${taskId}`)) {
1182
1206
  fail(`task next did not preserve current task:\n${nextAgain}`);
1183
1207
  }
1208
+ const nextAgainJson = hccJson(['task', 'next', '--peer', 'codex-a']);
1209
+ if (String(nextAgainJson.id) !== String(taskId) || nextAgainJson.current !== true || nextAgainJson.tasks) {
1210
+ fail(`task next --json current task shape changed:\n${JSON.stringify(nextAgainJson, null, 2)}`);
1211
+ }
1212
+ const batchClaimIds = [];
1213
+ for (const title of ['batch claim one', 'batch claim two']) {
1214
+ const out = hcc(['task', 'create', '--from', 'human', '--title', title]);
1215
+ const match = out.match(/created task #(\d+):/);
1216
+ if (!match) fail(`cannot parse batch task id: ${out}`);
1217
+ batchClaimIds.push(match[1]);
1218
+ }
1219
+ const batchClaim = hccJson(['task', 'claim', '--peer', 'batch-a', '--ids', batchClaimIds.join(',')]);
1220
+ if (!Array.isArray(batchClaim) || batchClaim.length !== 2 || !batchClaim.every((task) => task.owner === 'batch-a')) {
1221
+ fail(`batch claim did not claim both tasks:\n${JSON.stringify(batchClaim, null, 2)}`);
1222
+ }
1223
+ for (const task of batchClaim) {
1224
+ hcc(['task', 'done', '--peer', 'batch-a', '--id', String(task.id), '--summary', 'batch claim cleanup']);
1225
+ }
1226
+ const batchNextIds = [];
1227
+ for (const title of ['batch next one', 'batch next two', 'batch next three']) {
1228
+ const out = hcc(['task', 'create', '--from', 'human', '--title', title]);
1229
+ const match = out.match(/created task #(\d+):/);
1230
+ if (!match) fail(`cannot parse batch next task id: ${out}`);
1231
+ batchNextIds.push(match[1]);
1232
+ }
1233
+ const batchNext = hccJson(['task', 'next', '--peer', 'batch-b', '--force', '--count', '2']);
1234
+ if (!batchNext?.tasks || batchNext.tasks.length !== 2 || !batchNext.tasks.every((task) => task.owner === 'batch-b')) {
1235
+ fail(`task next --count did not claim two tasks:\n${JSON.stringify(batchNext, null, 2)}`);
1236
+ }
1237
+ for (const task of batchNext.tasks) {
1238
+ hcc(['task', 'done', '--peer', 'batch-b', '--id', String(task.id), '--summary', 'batch next cleanup']);
1239
+ }
1240
+ hcc(['task', 'update', '--peer', 'human', '--id', batchNextIds[2], '--status', 'abandoned', '--summary', 'batch next leftover cleanup']);
1241
+ const takeoverOpen = hcc(['task', 'create', '--from', 'human', '--to', 'takeover-owner', '--title', 'takeover open']);
1242
+ const takeoverOpenMatch = takeoverOpen.match(/created task #(\d+):/);
1243
+ if (!takeoverOpenMatch) fail(`cannot parse takeover open id: ${takeoverOpen}`);
1244
+ const takeoverOpenId = takeoverOpenMatch[1];
1245
+ hcc(['task', 'claim', '--peer', 'takeover-owner', '--id', takeoverOpenId]);
1246
+ const blockedPolicyReject = hccMaybe(['task', 'takeover', '--peer', 'takeover-a', '--id', takeoverOpenId, '--reason', 'blocked policy smoke', '--policy', 'blocked']);
1247
+ if (blockedPolicyReject.status === 0 || !String(blockedPolicyReject.stderr || blockedPolicyReject.stdout).includes('does not match takeover policy blocked')) {
1248
+ fail(`takeover --policy blocked accepted non-blocked task:\n${blockedPolicyReject.stdout}\n${blockedPolicyReject.stderr}`);
1249
+ }
1250
+ hcc(['task', 'update', '--peer', 'takeover-owner', '--id', takeoverOpenId, '--status', 'blocked', '--summary', 'blocked for takeover smoke']);
1251
+ const blockedTakeover = hccJson(['task', 'takeover', '--peer', 'takeover-a', '--id', takeoverOpenId, '--reason', 'blocked takeover smoke', '--policy', 'blocked']);
1252
+ if (String(blockedTakeover.owner) !== 'takeover-a') {
1253
+ fail(`blocked takeover did not transfer owner:\n${JSON.stringify(blockedTakeover, null, 2)}`);
1254
+ }
1255
+ hcc(['task', 'done', '--peer', 'takeover-a', '--id', takeoverOpenId, '--summary', 'blocked takeover cleanup']);
1256
+ const staleOut = hcc(['task', 'create', '--from', 'human', '--to', 'stale-owner', '--title', 'stale takeover']);
1257
+ const staleMatch = staleOut.match(/created task #(\d+):/);
1258
+ if (!staleMatch) fail(`cannot parse stale takeover id: ${staleOut}`);
1259
+ const staleTaskId = staleMatch[1];
1260
+ hcc(['task', 'claim', '--peer', 'stale-owner', '--id', staleTaskId]);
1261
+ withMeshDb((db) => {
1262
+ const staleAt = Math.floor(Date.now() / 1000) - 7200;
1263
+ db.prepare('UPDATE peers SET last_seen_at = ? WHERE id = ?').run(staleAt, 'stale-owner');
1264
+ });
1265
+ const staleTakeover = hccJson(['task', 'takeover', '--peer', 'takeover-b', '--id', staleTaskId, '--reason', 'stale takeover smoke', '--policy', 'stale', '--stale-after', '60']);
1266
+ if (String(staleTakeover.owner) !== 'takeover-b') {
1267
+ fail(`stale takeover did not transfer owner:\n${JSON.stringify(staleTakeover, null, 2)}`);
1268
+ }
1269
+ const takeoverInbox = hcc(['msg', 'inbox', '--peer', 'stale-owner', '--all']);
1270
+ if (!takeoverInbox.includes(`Task #${staleTaskId} taken over by takeover-b`)) {
1271
+ fail(`stale takeover did not notify previous owner:\n${takeoverInbox}`);
1272
+ }
1273
+ hcc(['task', 'done', '--peer', 'takeover-b', '--id', staleTaskId, '--summary', 'stale takeover cleanup']);
1274
+ const staleLivenessOut = hcc(['task', 'create', '--from', 'human', '--to', 'stale-liveness-owner', '--title', 'stale liveness task']);
1275
+ const staleLivenessMatch = staleLivenessOut.match(/created task #(\d+):/);
1276
+ if (!staleLivenessMatch) fail(`cannot parse stale liveness task id: ${staleLivenessOut}`);
1277
+ const staleLivenessTaskId = staleLivenessMatch[1];
1278
+ hcc(['task', 'claim', '--peer', 'stale-liveness-owner', '--id', staleLivenessTaskId]);
1279
+ withMeshDb((db) => {
1280
+ const staleAt = Math.floor(Date.now() / 1000) - 7200;
1281
+ db.prepare('UPDATE peers SET last_seen_at = ? WHERE id = ?').run(staleAt, 'stale-liveness-owner');
1282
+ });
1283
+ const staleLivenessList = hcc(['task', 'list', '--status', 'claimed']);
1284
+ if (!staleLivenessList.includes(`#${staleLivenessTaskId}`) || !staleLivenessList.includes('stale/no-lock')) {
1285
+ fail(`task list did not surface stale/no-lock owner state:\n${staleLivenessList}`);
1286
+ }
1287
+ const staleLivenessState = hccJson(['state', '--peer', 'takeover-ready-peer']);
1288
+ if (staleLivenessState.automation?.phase !== 'takeover_task' ||
1289
+ staleLivenessState.automation?.next_action?.kind !== 'task.takeover' ||
1290
+ !staleLivenessState.automation.next_action.argv.includes('--policy') ||
1291
+ !staleLivenessState.automation.next_action.argv.includes('stale')) {
1292
+ fail(`state automation did not suggest stale takeover:\n${JSON.stringify(staleLivenessState.automation, null, 2)}`);
1293
+ }
1294
+ hcc(['task', 'update', '--peer', 'human', '--force', '--id', staleLivenessTaskId, '--status', 'abandoned', '--summary', 'stale liveness cleanup']);
1184
1295
  hcc(['task', 'update', '--peer', 'human', '--id', queuedTaskId, '--status', 'abandoned', '--summary', 'queued task cleanup']);
1185
1296
  hcc(['handoff', 'create', '--from', 'codex-a', '--to', 'claude-a', '--task', taskId, '--summary', 'handoff summary', '--tests', 'full script', '--risks', 'none']);
1186
1297
  if (!hcc(['handoff', 'list', '--task', taskId]).includes('handoff summary')) fail('handoff missing');
@@ -1254,6 +1365,27 @@ async function dbWorkflow() {
1254
1365
  }
1255
1366
  hcc(['task', 'done', '--peer', 'codex-team-captain', '--id', teamParentId, '--summary', 'team regression done']);
1256
1367
 
1368
+ const scopedA = hcc(['lock', 'acquire', '--peer', 'codex-a', '--resource', 'bin/hcc.mjs', '--scope', 'db-schema', '--ttl', '60', '--reason', 'db split']);
1369
+ const scopedB = hcc(['lock', 'acquire', '--peer', 'codex-b', '--resource', 'bin/hcc.mjs', '--scope', 'web-ui', '--ttl', '60', '--reason', 'ui split']);
1370
+ if (!scopedA.includes('bin/hcc.mjs [db-schema]') || !scopedB.includes('bin/hcc.mjs [web-ui]')) {
1371
+ fail(`scoped lock output missing labels:\n${scopedA}\n${scopedB}`);
1372
+ }
1373
+ const scopedRows = hccJson(['lock', 'list']);
1374
+ if (!scopedRows.some((row) => row.base_resource === 'bin/hcc.mjs' && row.scope === 'db-schema') ||
1375
+ !scopedRows.some((row) => row.base_resource === 'bin/hcc.mjs' && row.scope === 'web-ui')) {
1376
+ fail(`scoped locks missing from list:\n${JSON.stringify(scopedRows, null, 2)}`);
1377
+ }
1378
+ const duplicateScope = hccMaybe(['lock', 'acquire', '--peer', 'claude-a', '--resource', 'bin/hcc.mjs', '--scope', 'db-schema', '--ttl', '60', '--reason', 'same scope']);
1379
+ if (duplicateScope.status === 0 || !`${duplicateScope.stdout}\n${duplicateScope.stderr}`.includes('conflicts with lock bin/hcc.mjs [db-schema]')) {
1380
+ fail(`same-scope lock was not rejected:\n${duplicateScope.stdout}\n${duplicateScope.stderr}`);
1381
+ }
1382
+ const wholeResource = hccMaybe(['lock', 'acquire', '--peer', 'claude-a', '--resource', 'bin/hcc.mjs', '--ttl', '60', '--reason', 'whole file']);
1383
+ if (wholeResource.status === 0 || !`${wholeResource.stdout}\n${wholeResource.stderr}`.includes('conflicts with lock bin/hcc.mjs [db-schema]')) {
1384
+ fail(`whole-resource lock did not conflict with scoped locks:\n${wholeResource.stdout}\n${wholeResource.stderr}`);
1385
+ }
1386
+ hcc(['lock', 'release', '--peer', 'codex-a', '--resource', 'bin/hcc.mjs', '--scope', 'db-schema']);
1387
+ hcc(['lock', 'release', '--peer', 'codex-b', '--resource', 'bin/hcc.mjs', '--scope', 'web-ui']);
1388
+
1257
1389
  const conflictTask = hcc(['task', 'create', '--from', 'human', '--to', 'codex-b', '--title', 'conflict automation task']);
1258
1390
  const conflictTaskMatch = conflictTask.match(/created task #(\d+):/);
1259
1391
  if (!conflictTaskMatch) fail(`cannot parse conflict task id: ${conflictTask}`);
@@ -1270,6 +1402,23 @@ async function dbWorkflow() {
1270
1402
  hcc(['lock', 'release', '--peer', 'codex-a', '--resource', 'src/conflict']);
1271
1403
  hcc(['task', 'done', '--peer', 'codex-b', '--id', conflictTaskId, '--summary', 'conflict done']);
1272
1404
 
1405
+ const takeoverTask = hcc(['task', 'create', '--from', 'human', '--to', 'codex-owner', '--title', 'takeover regression task']);
1406
+ const takeoverTaskMatch = takeoverTask.match(/created task #(\d+):/);
1407
+ if (!takeoverTaskMatch) fail(`cannot parse takeover task id: ${takeoverTask}`);
1408
+ const takeoverTaskId = takeoverTaskMatch[1];
1409
+ hcc(['task', 'claim', '--peer', 'codex-owner', '--id', takeoverTaskId]);
1410
+ const takeoverOutput = hcc(['task', 'takeover', '--peer', 'codex-taker', '--id', takeoverTaskId, '--reason', 'owner inactive']);
1411
+ if (!takeoverOutput.includes(`took over task #${takeoverTaskId}`)) fail(`takeover output wrong:\n${takeoverOutput}`);
1412
+ const takeoverRow = hccJson(['task', 'list', '--all']).find((row) => String(row.id) === String(takeoverTaskId));
1413
+ if (!takeoverRow || takeoverRow.owner !== 'codex-taker' || takeoverRow.status !== 'claimed') {
1414
+ fail(`takeover task row wrong:\n${JSON.stringify(takeoverRow, null, 2)}`);
1415
+ }
1416
+ const takeoverOwnerInbox = hcc(['msg', 'inbox', '--peer', 'codex-owner']);
1417
+ if (!takeoverOwnerInbox.includes(`Task #${takeoverTaskId} taken over by codex-taker`)) {
1418
+ fail(`takeover did not notify previous owner:\n${takeoverOwnerInbox}`);
1419
+ }
1420
+ hcc(['task', 'done', '--peer', 'codex-taker', '--id', takeoverTaskId, '--summary', 'takeover done']);
1421
+
1273
1422
  const abandoned = hcc(['task', 'create', '--from', 'human', '--title', 'abandoned regression task']);
1274
1423
  const abandonedMatch = abandoned.match(/created task #(\d+):/);
1275
1424
  if (!abandonedMatch) fail(`cannot parse abandoned task id: ${abandoned}`);
@@ -1961,9 +2110,7 @@ async function downGcPackWorkflow() {
1961
2110
  'CHANGELOG.md',
1962
2111
  'docs/commands.md',
1963
2112
  'docs/commands.zh-CN.md',
1964
- 'lib/guidance.mjs',
1965
- 'lib/package-meta.mjs',
1966
- 'lib/release-notes.mjs',
2113
+ ...libModuleFiles(),
1967
2114
  'scripts/github-release.mjs'
1968
2115
  ]) {
1969
2116
  if (!files.has(file)) fail(`npm package missing ${file}`);
@@ -2030,15 +2177,14 @@ function oldNameScan() {
2030
2177
  if (rg.status !== 1) fail(`rg failed:\n${rg.stderr || rg.stdout}`);
2031
2178
  }
2032
2179
 
2033
- function syntaxAndHelp() {
2180
+ async function syntaxAndHelp() {
2034
2181
  log('[11/12] syntax/help');
2035
2182
  run(process.execPath, ['--check', path.join(repoRoot, 'bin', 'hcc.mjs')]);
2036
- run(process.execPath, ['--check', path.join(repoRoot, 'lib', 'setup.mjs')]);
2037
- run(process.execPath, ['--check', path.join(repoRoot, 'lib', 'discover.mjs')]);
2038
- run(process.execPath, ['--check', path.join(repoRoot, 'lib', 'guidance.mjs')]);
2039
- run(process.execPath, ['--check', path.join(repoRoot, 'lib', 'package-meta.mjs')]);
2040
- run(process.execPath, ['--check', path.join(repoRoot, 'lib', 'release-notes.mjs')]);
2183
+ for (const file of libModuleFiles()) {
2184
+ run(process.execPath, ['--check', path.join(repoRoot, file)]);
2185
+ }
2041
2186
  const hccSource = fs.readFileSync(hccBin, 'utf8');
2187
+ const webUiTemplateSource = fs.readFileSync(path.join(repoRoot, 'lib', 'web-ui-template.mjs'), 'utf8');
2042
2188
  for (const expected of [
2043
2189
  'function scheduleTmuxInputRefresh(session)',
2044
2190
  "runTmux(['pipe-pane', '-t', session.pane]);",
@@ -2050,9 +2196,25 @@ function syntaxAndHelp() {
2050
2196
  ]) {
2051
2197
  if (!hccSource.includes(expected)) fail(`web terminal input refresh support missing: ${expected}`);
2052
2198
  }
2199
+ for (const expected of [
2200
+ 'function detectedPeerCanStop(peer)',
2201
+ "if (['exited', 'detached'].includes(status)) return false;",
2202
+ 'const canStop = detectedPeerCanStop(p);',
2203
+ '${canStop ?',
2204
+ "if (e.target.closest('[data-action]')) return;",
2205
+ 'id="stopKillLabel" data-i18n="dialog.killTmux"',
2206
+ 'id="stopCancelBtn" type="button" data-i18n="dialog.cancel"',
2207
+ 'id="stopConfirmBtn" type="button" data-i18n="stop"'
2208
+ ]) {
2209
+ if (!webUiTemplateSource.includes(expected)) fail(`web display regression guard missing: ${expected}`);
2210
+ }
2211
+ if (webUiTemplateSource.includes("p.status === 'running' ?")) {
2212
+ fail('detected peer action rendering still depends on status === running instead of liveness');
2213
+ }
2053
2214
  for (const expected of [
2054
2215
  'function webPeerAction(projectCtx, peer, action, input = {})',
2055
- 'function claimNextTaskForPeer(db, peer, force = false)',
2216
+ 'function claimNextTasksForPeer(db, peer, { force = false, count = 1 } = {})',
2217
+ 'function takeOverTaskForPeer(db, peer, id, { reason, policy = ',
2056
2218
  'function statusSummary(ctx, peer = null, identity = null)',
2057
2219
  'const peerActionMatch = url.pathname.match(/^\\/api\\/peers\\/([^/]+)\\/actions\\/([^/]+)$/)',
2058
2220
  "const readOnly = ['status', 'state', 'inbox'].includes(action)",
@@ -2066,10 +2228,100 @@ function syntaxAndHelp() {
2066
2228
  fail(`CLI version ${cliVersion} does not match package.json ${packageVersion}`);
2067
2229
  }
2068
2230
  if (!hccSource.includes("import { readPackageMeta } from '../lib/package-meta.mjs'") ||
2231
+ !hccSource.includes("} from '../lib/cli-args.mjs'") ||
2232
+ !hccSource.includes("import { CliError } from '../lib/errors.mjs'") ||
2233
+ !hccSource.includes("} from '../lib/db-schema.mjs'") ||
2234
+ !hccSource.includes("} from '../lib/format.mjs'") ||
2235
+ !hccSource.includes("} from '../lib/runtime-paths.mjs'") ||
2236
+ !hccSource.includes("} from '../lib/web-runtime.mjs'") ||
2237
+ !hccSource.includes("} from '../lib/web-http.mjs'") ||
2238
+ !hccSource.includes("import { webIndexHtml } from '../lib/web-ui-template.mjs'") ||
2069
2239
  !hccSource.includes('const VERSION = PACKAGE_META.version') ||
2070
2240
  !hccSource.includes('writeGuidanceForRoot(ctx.root)')) {
2071
- fail('CLI still has duplicated package metadata or guidance wiring');
2072
- }
2241
+ fail('CLI still has duplicated package metadata, cli args, DB schema helpers, format helpers, runtime paths, web runtime/HTTP/UI helpers, or guidance wiring');
2242
+ }
2243
+ if (hccSource.includes('function createBaseSchema') ||
2244
+ hccSource.includes('function runSchemaMigrations') ||
2245
+ hccSource.includes('function readSchemaVersion')) {
2246
+ fail('CLI still embeds DB schema or migration helpers');
2247
+ }
2248
+ for (const helper of [
2249
+ 'function runtimeBaseUrl',
2250
+ 'function runtimeApiUrl',
2251
+ 'function requestUrl',
2252
+ 'function runtimeUrlQuery',
2253
+ 'function makeWebToken',
2254
+ 'function validateWebTokenOpts',
2255
+ 'function expectedWebHost',
2256
+ 'function webRuntimeMatchesRequest',
2257
+ 'function publicRuntimeUrl',
2258
+ 'function localRuntimeUrl'
2259
+ ]) {
2260
+ if (hccSource.includes(helper)) fail(`CLI still embeds web runtime helper: ${helper}`);
2261
+ }
2262
+ for (const helper of [
2263
+ 'function readRequestBody',
2264
+ 'function readJsonRequest',
2265
+ 'function sendHttp',
2266
+ 'function sendJson',
2267
+ 'function sendFile',
2268
+ 'function authOk'
2269
+ ]) {
2270
+ if (hccSource.includes(helper)) fail(`CLI still embeds web HTTP helper: ${helper}`);
2271
+ }
2272
+ if (hccSource.includes('new URL(req.url')) fail('CLI still embeds raw server request URL parsing');
2273
+ if (hccSource.includes('function webIndexHtml()')) fail('CLI still embeds the web UI template');
2274
+ const webRuntime = await import(path.join(repoRoot, 'lib', 'web-runtime.mjs'));
2275
+ const webHttp = await import(path.join(repoRoot, 'lib', 'web-http.mjs'));
2276
+ const webUiTemplate = await import(path.join(repoRoot, 'lib', 'web-ui-template.mjs'));
2277
+ const expectEqual = (actual, expected, label) => {
2278
+ if (actual !== expected) fail(`${label}: expected ${expected}, got ${actual}`);
2279
+ };
2280
+ const html = webUiTemplate.webIndexHtml();
2281
+ if (!html.includes('<!doctype html>') ||
2282
+ !html.includes('<div class="app">') ||
2283
+ !html.includes('<script src="/assets/xterm.js"></script>')) {
2284
+ fail('web UI template module did not render the expected shell HTML');
2285
+ }
2286
+ const jsonReq = Readable.from(['{"ok":true}']);
2287
+ jsonReq.headers = {};
2288
+ const parsedJson = await webHttp.readJsonRequest(jsonReq);
2289
+ if (parsedJson.ok !== true) fail(`web HTTP helper failed to parse JSON request: ${JSON.stringify(parsedJson)}`);
2290
+ const emptyReq = Readable.from(['']);
2291
+ emptyReq.headers = {};
2292
+ const parsedEmpty = await webHttp.readJsonRequest(emptyReq);
2293
+ if (Object.keys(parsedEmpty).length !== 0) fail(`web HTTP helper failed empty request fallback: ${JSON.stringify(parsedEmpty)}`);
2294
+ const mockRes = {
2295
+ status: null,
2296
+ headers: null,
2297
+ body: '',
2298
+ writeHead(status, headers) { this.status = status; this.headers = headers; },
2299
+ end(body) { this.body = body; }
2300
+ };
2301
+ webHttp.sendJson(mockRes, 202, { ok: true });
2302
+ if (mockRes.status !== 202 ||
2303
+ mockRes.headers?.['Content-Type'] !== 'application/json; charset=utf-8' ||
2304
+ mockRes.headers?.['Cache-Control'] !== 'no-store' ||
2305
+ !mockRes.body.includes('"ok": true')) {
2306
+ fail(`web HTTP helper failed sendJson response: ${JSON.stringify(mockRes)}`);
2307
+ }
2308
+ if (!webHttp.authOk(new URL('http://example.test/?token=tok'), { headers: {} }, 'tok') ||
2309
+ !webHttp.authOk(new URL('http://example.test/'), { headers: { authorization: 'Bearer tok' } }, 'tok') ||
2310
+ webHttp.authOk(new URL('http://example.test/?token=bad'), { headers: {} }, 'tok')) {
2311
+ fail('web HTTP helper authOk token checks failed');
2312
+ }
2313
+ const wildcardRuntime = { host: '0.0.0.0', port: 8787, token: 'tok' };
2314
+ const ipv6WildcardRuntime = { host: '::', port: 8788, token: 'tok' };
2315
+ const localRuntime = { host: '127.0.0.1', port: 8789, token: 'tok' };
2316
+ expectEqual(webRuntime.runtimeBaseUrl('0.0.0.0', 8787), 'http://127.0.0.1:8787', 'runtimeBaseUrl 0.0.0.0');
2317
+ expectEqual(webRuntime.runtimeBaseUrl('::', 8788), 'http://127.0.0.1:8788', 'runtimeBaseUrl ::');
2318
+ expectEqual(String(webRuntime.runtimeApiUrl({ base_url: 'http://127.0.0.1:8787/base' }, '/api/state?peer=a b')), 'http://127.0.0.1:8787/api/state?peer=a%20b', 'runtimeApiUrl route');
2319
+ expectEqual(String(webRuntime.requestUrl({ url: '/api/state?peer=a b', headers: { host: 'example.test:8787' } })), 'http://example.test:8787/api/state?peer=a%20b', 'requestUrl host and query');
2320
+ expectEqual(String(webRuntime.requestUrl({ url: '', headers: {} })), 'http://localhost/', 'requestUrl fallback');
2321
+ expectEqual(webRuntime.publicRuntimeUrl(wildcardRuntime, '/tmp/hcc project'), 'http://<machine-ip>:8787/?token=tok&project=%2Ftmp%2Fhcc%20project', 'publicRuntimeUrl wildcard');
2322
+ expectEqual(webRuntime.localRuntimeUrl(wildcardRuntime, '/tmp/hcc project'), 'http://127.0.0.1:8787/?token=tok&project=%2Ftmp%2Fhcc%20project', 'localRuntimeUrl wildcard');
2323
+ expectEqual(webRuntime.publicRuntimeUrl(ipv6WildcardRuntime, '/tmp/hcc project'), 'http://<machine-ip>:8788/?token=tok&project=%2Ftmp%2Fhcc%20project', 'publicRuntimeUrl ipv6 wildcard');
2324
+ expectEqual(webRuntime.localRuntimeUrl(localRuntime, null), 'http://127.0.0.1:8789/?token=tok', 'localRuntimeUrl no project');
2073
2325
  const mainHelp = run(process.execPath, [hccBin, '--help']);
2074
2326
  if (mainHelp.includes('setup') || mainHelp.includes('--web-managed')) {
2075
2327
  fail(`public help exposes maintenance or removed commands:\n${mainHelp}`);
@@ -2092,7 +2344,11 @@ function syntaxAndHelp() {
2092
2344
  }
2093
2345
  const taskHelp = run(process.execPath, [hccBin, 'task', '--help']);
2094
2346
  if (!taskHelp.includes('task next [--peer ID] [--force]') ||
2347
+ !taskHelp.includes('[--count N]') ||
2095
2348
  !taskHelp.includes('existing claimed/running/review/blocked task') ||
2349
+ !taskHelp.includes('task takeover [--peer ID] --id N --reason TEXT') ||
2350
+ !taskHelp.includes('[--policy any|blocked|stale|blocked-or-stale]') ||
2351
+ !taskHelp.includes('task claim [--peer ID] --id N[,N]') ||
2096
2352
  !taskHelp.includes('task create --title TEXT --parent N')) {
2097
2353
  fail(`task help missing current-task task next semantics:\n${taskHelp}`);
2098
2354
  }
@@ -2101,9 +2357,18 @@ function syntaxAndHelp() {
2101
2357
  fail(`team help missing expected content:\n${teamHelp}`);
2102
2358
  }
2103
2359
  const stateHelp = run(process.execPath, [hccBin, 'state', '--help']);
2104
- if (!stateHelp.includes('hcc state') || !stateHelp.includes('automation.next_action.argv') || !stateHelp.includes('automation.current_task') || !stateHelp.includes('hcc team plan')) {
2360
+ if (!stateHelp.includes('hcc state') ||
2361
+ !stateHelp.includes('--scope SCOPE') ||
2362
+ !stateHelp.includes('--intent read|review|work|write|stop|finish') ||
2363
+ !stateHelp.includes('automation.next_action.argv') ||
2364
+ !stateHelp.includes('automation.current_task') ||
2365
+ !stateHelp.includes('hcc team plan')) {
2105
2366
  fail(`state help missing expected content:\n${stateHelp}`);
2106
2367
  }
2368
+ const lockHelp = run(process.execPath, [hccBin, 'lock', '--help']);
2369
+ if (!lockHelp.includes('--scope SCOPE') || !lockHelp.toLowerCase().includes('different scopes on the same resource')) {
2370
+ fail(`lock help missing scoped lock content:\n${lockHelp}`);
2371
+ }
2107
2372
  const updateHelp = run(process.execPath, [hccBin, 'update', '--help']);
2108
2373
  if (!updateHelp.includes('hcc update') || !updateHelp.includes('npm install -g @logicseek/hello-cc@TAG')) {
2109
2374
  fail(`update help missing expected content:\n${updateHelp}`);
@@ -2214,7 +2479,7 @@ async function main() {
2214
2479
  await askBroadcastWorkflow();
2215
2480
  await downGcPackWorkflow();
2216
2481
  oldNameScan();
2217
- syntaxAndHelp();
2482
+ await syntaxAndHelp();
2218
2483
  uninstallWorkflow();
2219
2484
  log('FULL_REGRESSION_OK');
2220
2485
  }