@logicseek/hello-cc 1.0.2-dsh.3 → 1.1.0-rc.1
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 +121 -0
- package/README.md +10 -1
- package/README.zh-CN.md +8 -1
- package/docs/commands.md +28 -4
- package/docs/commands.zh-CN.md +20 -4
- package/docs/native.md +84 -10
- package/docs/native.zh-CN.md +66 -5
- package/docs/web-handoff.zh-CN.md +11 -3
- package/lib/cli/app.mjs +42 -1
- package/lib/cli/commands/coordination.mjs +7 -2
- package/lib/cli/commands/dsh.mjs +1 -1
- package/lib/cli/commands/gc.mjs +9 -4
- package/lib/cli/commands/hook.mjs +6 -1
- package/lib/cli/commands/install.mjs +48 -6
- package/lib/cli/commands/lifecycle.mjs +14 -3
- package/lib/cli/commands/native.mjs +16 -9
- package/lib/cli/commands/query.mjs +15 -10
- package/lib/cli/commands/run.mjs +7 -6
- package/lib/core/coordination/results.mjs +27 -10
- package/lib/db/connection.mjs +135 -10
- package/lib/db/schema.mjs +59 -0
- package/lib/discover.mjs +4 -4
- package/lib/guidance.mjs +3 -2
- package/lib/integrations/codex-interactions.mjs +13 -11
- package/lib/integrations/dsh-collaboration.mjs +6 -12
- package/lib/integrations/dsh-cordis.mjs +17 -13
- package/lib/integrations/dsh.mjs +12 -9
- package/lib/integrations/hooks.mjs +132 -40
- package/lib/integrations/mcp-elicitation.mjs +116 -0
- package/lib/integrations/mcp-url-elicitation.mjs +25 -0
- package/lib/integrations/native/interactions.mjs +2 -1
- package/lib/integrations/peers/identity.mjs +42 -7
- package/lib/process/identity.mjs +15 -14
- package/lib/runtime/buffer-evidence.mjs +2 -1
- package/lib/runtime/native/service.mjs +11 -5
- package/lib/runtime/native/store.mjs +70 -18
- package/lib/runtime/paths.mjs +17 -2
- package/lib/runtime/private-file.mjs +76 -0
- package/lib/runtime/private-state.mjs +546 -0
- package/lib/runtime/project-context.mjs +2 -1
- package/lib/runtime/project-path.mjs +148 -12
- package/lib/runtime/project-trust.mjs +79 -0
- package/lib/runtime/projects.mjs +6 -3
- package/lib/runtime/state-migration.mjs +697 -0
- package/lib/runtime/state.mjs +22 -11
- package/lib/shared/automatic-result-key.mjs +6 -0
- package/lib/shared/json-file.mjs +55 -6
- package/lib/ui/help.mjs +6 -4
- package/lib/web/codex-app-server.mjs +26 -5
- package/lib/web/codex-sessions.mjs +5 -3
- package/lib/web/cookie-auth.mjs +1 -1
- package/lib/web/external-sessions.mjs +54 -5
- package/lib/web/http-routes.mjs +13 -9
- package/lib/web/http.mjs +20 -2
- package/lib/web/native-sessions.mjs +14 -7
- package/lib/web/peer-actions.mjs +27 -27
- package/lib/web/project-contexts.mjs +2 -3
- package/lib/web/runtime-main.mjs +22 -5
- package/lib/web/runtime.mjs +18 -4
- package/lib/web/session-serialize.mjs +1 -1
- package/lib/web/startup.mjs +115 -22
- package/lib/web/task-results.mjs +20 -17
- package/lib/web/terminal-assets.mjs +18 -0
- package/lib/web/tls.mjs +84 -33
- package/lib/web/tmux-sessions.mjs +2 -2
- package/lib/web/tmux-stream.mjs +3 -4
- package/lib/web/ui-codex.mjs +161 -32
- package/lib/web/ui-interactions.mjs +137 -4
- package/lib/web/ui-native.mjs +31 -13
- package/lib/web/ui-template.mjs +10 -3
- package/package.json +1 -1
- package/scripts/mcp-form-live-fixture.mjs +81 -0
- package/scripts/native-installed-acceptance.mjs +413 -0
- package/scripts/regression.mjs +46 -29
- package/scripts/web-mcp-form-acceptance.mjs +119 -0
- package/scripts/web-mcp-form-live-acceptance.mjs +368 -0
- package/scripts/web-native-interaction-acceptance.mjs +77 -10
package/lib/cli/app.mjs
CHANGED
|
@@ -89,6 +89,9 @@ import { assignTeamWorkers, expandTeamWorkers, inferTeamItems } from '../core/co
|
|
|
89
89
|
import { autoPeerBasis, autoPeerKind, autoPeerProviderSession, autoPeerResumeId, autoPeerSessionId, readAncestorCliInfo, resolveCurrentPeer, resumeIdFromArgs, shortHash } from '../integrations/peers/identity.mjs';
|
|
90
90
|
import { inspectProviderProcess } from '../integrations/peers/processes.mjs';
|
|
91
91
|
import { registerProjectActivity } from '../runtime/projects.mjs';
|
|
92
|
+
import { migrateLegacyProjectState } from '../runtime/state-migration.mjs';
|
|
93
|
+
import { projectDbPath } from '../runtime/paths.mjs';
|
|
94
|
+
import { ensureDshIntegration } from '../integrations/dsh.mjs';
|
|
92
95
|
|
|
93
96
|
// Lazy-load lib modules (they may import node-pty which needs to be optional)
|
|
94
97
|
const _libDir = path.resolve(fileURLToPath(import.meta.url), '..', '..');
|
|
@@ -303,7 +306,7 @@ const { cmdInit, cmdRegister, cmdEnv, cmdJoin } = createLifecycleCommands({
|
|
|
303
306
|
connect, now, tx, addEvent, printResult, CliError, parseOpts, intOpt,
|
|
304
307
|
registerProjectActivity, resolveCurrentPeer, resolveTargetPeer,
|
|
305
308
|
upsertPeer, upsertCanonicalPeerBinding, storedPeerIdentity, detectBranch,
|
|
306
|
-
writeGuidance, helpEnv, helpJoin, shellExports, path, process
|
|
309
|
+
readAncestorCliInfo, writeGuidance, helpEnv, helpJoin, shellExports, path, process
|
|
307
310
|
});
|
|
308
311
|
|
|
309
312
|
const { cmdTeam } = createTeamCommands({
|
|
@@ -580,6 +583,43 @@ function removeGuidanceBlocks(ctx) {
|
|
|
580
583
|
return removeGuidanceBlocksForRoot(ctx.root);
|
|
581
584
|
}
|
|
582
585
|
|
|
586
|
+
function cmdMigrateState(ctx, args) {
|
|
587
|
+
if (wantsHelp(args)) {
|
|
588
|
+
console.log(`${CLI_NAME} migrate-state --offline --yes\n\n` +
|
|
589
|
+
'Stop all old hello-cc Web, Native, PTY, tmux, hook and external database writers first. ' +
|
|
590
|
+
'This command verifies known process evidence, snapshots SQLite including WAL, and leaves the old directory untouched.');
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
const opts = parseOpts(args, { booleans: ['offline', 'yes'] });
|
|
594
|
+
validateOpts('migrate-state', opts, ['offline', 'yes']);
|
|
595
|
+
if (opts.offline !== true || opts.yes !== true) {
|
|
596
|
+
throw new CliError('STATE_MIGRATION_OFFLINE_REQUIRED',
|
|
597
|
+
'Stop every old project-state writer, then acknowledge downtime with migrate-state --offline --yes');
|
|
598
|
+
}
|
|
599
|
+
const result = migrateLegacyProjectState(ctx.root, {
|
|
600
|
+
confirmedOffline: true,
|
|
601
|
+
// The operator's explicit --offline assertion covers external writers
|
|
602
|
+
// that cannot be discovered reliably across supported platforms. The
|
|
603
|
+
// migrator still checks known runtime, PTY and SQLite peer evidence.
|
|
604
|
+
assertOffline: () => true
|
|
605
|
+
});
|
|
606
|
+
let dshRebuilt = false;
|
|
607
|
+
if (result.dsh) {
|
|
608
|
+
try {
|
|
609
|
+
ensureDshIntegration({ ...ctx, dbPath: projectDbPath(ctx.root) }, { mode: result.dsh.mode });
|
|
610
|
+
dshRebuilt = true;
|
|
611
|
+
} catch (error) {
|
|
612
|
+
throw new CliError('STATE_MIGRATION_DSH_REBUILD_REQUIRED',
|
|
613
|
+
`Project state migrated to ${result.stateDir}, but DSH configuration needs rebuilding. ` +
|
|
614
|
+
`Run ${result.dsh.rebuildCommand}. Original data remains at ${result.sourceDir}.`,
|
|
615
|
+
{ cause: error });
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
printResult(ctx, { ...result, dsh_rebuilt: dshRebuilt },
|
|
619
|
+
() => `project state migrated: ${result.stateDir}\nlegacy source kept: ${result.sourceDir}` +
|
|
620
|
+
(dshRebuilt ? '\nDSH integration rebuilt for the new private path' : ''));
|
|
621
|
+
}
|
|
622
|
+
|
|
583
623
|
// ─── hcc doctor ─────────────────────────────────────────────────────────────
|
|
584
624
|
// Health self-check: SQLite integrity, schema version, WAL/DB size, row counts.
|
|
585
625
|
async function dispatch(ctx, rest) {
|
|
@@ -592,6 +632,7 @@ async function dispatch(ctx, rest) {
|
|
|
592
632
|
if (command === 'update') return cmdUpdate(ctx, args);
|
|
593
633
|
if (command === 'uninstall') return cmdUninstall(ctx, args);
|
|
594
634
|
if (command === 'init') return cmdInit(ctx, args);
|
|
635
|
+
if (command === 'migrate-state') return cmdMigrateState(ctx, args);
|
|
595
636
|
if (command === 'register') return cmdRegister(ctx, args);
|
|
596
637
|
if (command === 'join') return cmdJoin(ctx, args);
|
|
597
638
|
if (command === 'env') return cmdEnv(ctx, args);
|
|
@@ -184,8 +184,13 @@ export function createCoordinationCommands(deps) {
|
|
|
184
184
|
const kind = opts.kind || 'broadcast';
|
|
185
185
|
const runtime = opts.inject ? readRuntime(ctx) : null;
|
|
186
186
|
const db = connect(ctx);
|
|
187
|
-
|
|
188
|
-
|
|
187
|
+
let id;
|
|
188
|
+
try {
|
|
189
|
+
touchCurrentPeer(db, ctx, identity, null, 'shell');
|
|
190
|
+
id = sendMessage(db, sender, 'all', taskId, kind, body);
|
|
191
|
+
} finally {
|
|
192
|
+
db.close();
|
|
193
|
+
}
|
|
189
194
|
let injected = 0;
|
|
190
195
|
let skipped = 0;
|
|
191
196
|
if (opts.inject) {
|
package/lib/cli/commands/dsh.mjs
CHANGED
|
@@ -14,7 +14,7 @@ function defaultHelp() {
|
|
|
14
14
|
' hcc dsh status [--dsh-bin PATH]',
|
|
15
15
|
' hcc dsh web [--mode hooks|cordis|off] [--dsh-bin PATH] [--dsh-home PATH] -- [dsh web arguments]',
|
|
16
16
|
'',
|
|
17
|
-
'Setup writes
|
|
17
|
+
'Setup writes managed configuration under the selected private state directory. Modes: hooks (default), cordis (native tools), off.',
|
|
18
18
|
'Web starts the official dsh web runtime; its real sessions join hello-cc through the selected integration.',
|
|
19
19
|
'Pass runtime arguments after --, for example: hcc dsh web -- --port 8080',
|
|
20
20
|
'Show dsh web help: hcc dsh web -- --help',
|
package/lib/cli/commands/gc.mjs
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
import { runOptimisticEvidenceMutation } from '../../core/coordination/optimistic-evidence.mjs';
|
|
27
27
|
import { peerEvidenceAllowsReap } from '../../core/peers/evidence.mjs';
|
|
28
28
|
import { runtimePath, globalRuntimePath } from '../../runtime/paths.mjs';
|
|
29
|
+
import { resolveProjectDatabase } from '../../runtime/project-path.mjs';
|
|
29
30
|
import { readRuntime, reclaimRuntimePointerFiles } from '../../runtime/state.mjs';
|
|
30
31
|
import { runtimeRequest, runtimeBufferGcUnavailable } from '../../runtime/client.mjs';
|
|
31
32
|
import { collectBufferEvidence } from '../../runtime/buffer-evidence.mjs';
|
|
@@ -263,12 +264,16 @@ function captureRequiredBufferDirectoryComponent(component, description) {
|
|
|
263
264
|
function captureOptionalBufferDirectory(root, directoryName) {
|
|
264
265
|
const requestedRoot = path.resolve(root);
|
|
265
266
|
const canonicalRoot = fs.realpathSync.native(requestedRoot);
|
|
266
|
-
const stateDirectory =
|
|
267
|
+
const stateDirectory = resolveProjectDatabase({ root: canonicalRoot, createStateDir: false }).stateDir;
|
|
267
268
|
const directory = path.join(stateDirectory, directoryName);
|
|
268
269
|
const components = [
|
|
269
|
-
captureRequiredBufferDirectoryComponent(canonicalRoot, 'Project root')
|
|
270
|
-
captureRequiredBufferDirectoryComponent(stateDirectory, 'Project state directory')
|
|
270
|
+
captureRequiredBufferDirectoryComponent(canonicalRoot, 'Project root')
|
|
271
271
|
];
|
|
272
|
+
try {
|
|
273
|
+
components.push(captureRequiredBufferDirectoryComponent(stateDirectory, 'Project state directory'));
|
|
274
|
+
} catch (error) {
|
|
275
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
276
|
+
}
|
|
272
277
|
|
|
273
278
|
let stat;
|
|
274
279
|
try {
|
|
@@ -624,7 +629,7 @@ function runGcWithHistorySnapshot(ctx, db, {
|
|
|
624
629
|
}
|
|
625
630
|
|
|
626
631
|
function bufferDirectory(ctx) {
|
|
627
|
-
return path.
|
|
632
|
+
return path.join(resolveProjectDatabase({ root: ctx.root, createStateDir: false }).stateDir, BUFS_DIR_NAME);
|
|
628
633
|
}
|
|
629
634
|
|
|
630
635
|
function bufferDirectoryPaths(directory) {
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { parseOpts, validateOpts } from '../../cli-args.mjs';
|
|
5
5
|
import { tx } from '../../db/schema.mjs';
|
|
6
6
|
import { resolveProjectDatabase } from '../../runtime/project-path.mjs';
|
|
7
|
+
import { projectDbPath } from '../../runtime/paths.mjs';
|
|
7
8
|
|
|
8
9
|
export function createHookCommand(deps) {
|
|
9
10
|
const {
|
|
@@ -89,13 +90,17 @@ async function cmdHook(ctx, args) {
|
|
|
89
90
|
const dshProject = isDsh && !nativeOwner ? resolveProjectDatabase({ root: hccRoot, createStateDir: false }) : null;
|
|
90
91
|
const nativeProject = nativeOwner ? resolveProjectDatabase({ root: hccRoot,
|
|
91
92
|
db: process.env.HCC_DB, createStateDir: false }) : null;
|
|
93
|
+
const ordinaryDb = (ctx.root && path.resolve(ctx.root) === hccRoot ? ctx.dbPath : null) ||
|
|
94
|
+
process.env.HCC_DB;
|
|
92
95
|
if (nativeProject && !fs.existsSync(nativeProject.db)) {
|
|
93
96
|
throw new CliError('NATIVE_HOOK_OWNERSHIP_MISMATCH', 'Native hook database must already be owned by its runtime');
|
|
94
97
|
}
|
|
98
|
+
const ordinaryDefaultDb = !dshProject && !nativeProject && !ordinaryDb
|
|
99
|
+
? projectDbPath(hccRoot) : null;
|
|
95
100
|
const hookCtx = {
|
|
96
101
|
...ctx,
|
|
97
102
|
root: nativeProject?.root || dshProject?.root || hccRoot,
|
|
98
|
-
dbPath: nativeProject?.db || dshProject?.db || path.
|
|
103
|
+
dbPath: nativeProject?.db || dshProject?.db || ordinaryDefaultDb || path.resolve(ordinaryDb)
|
|
99
104
|
};
|
|
100
105
|
if (!nativeOwner) registerProjectActivity(hookCtx);
|
|
101
106
|
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
// Install/setup/update/uninstall/shim commands extracted from bin/hcc.mjs.
|
|
2
2
|
|
|
3
3
|
import { execFileSync, spawnSync } from 'node:child_process';
|
|
4
|
+
import { secureExistingProjectStateDirectory } from '../../runtime/project-path.mjs';
|
|
5
|
+
import { legacyProjectStateDir, projectStateDir } from '../../runtime/paths.mjs';
|
|
6
|
+
import { privateProjectStateDir, inspectPrivateProjectAuthority,
|
|
7
|
+
purgePrivateProjectState } from '../../runtime/private-state.mjs';
|
|
4
8
|
|
|
5
9
|
export function createInstallCommands(deps) {
|
|
6
10
|
const {
|
|
@@ -236,14 +240,33 @@ async function cmdUninstall(ctx, args) {
|
|
|
236
240
|
if (opts.purge && !opts.yes) {
|
|
237
241
|
throw new CliError('CONFIRM_REQUIRED', 'Refusing to purge project data without --yes');
|
|
238
242
|
}
|
|
243
|
+
// Resolve before changing hooks, guidance, or runtime state. An unsafe root
|
|
244
|
+
// with unmigrated legacy data must not be treated as an empty private store.
|
|
245
|
+
let purgeStateDir = null;
|
|
246
|
+
let missingPrivateState = false;
|
|
247
|
+
if (opts.purge) {
|
|
248
|
+
try {
|
|
249
|
+
purgeStateDir = secureExistingProjectStateDirectory(ctx.root).stateDir;
|
|
250
|
+
} catch (error) {
|
|
251
|
+
if (projectStateDir(ctx.root) !== privateProjectStateDir(ctx.root)) throw error;
|
|
252
|
+
const recoverablePending = error?.code === 'STATE_MIGRATION_REQUIRED' &&
|
|
253
|
+
inspectPrivateProjectAuthority(ctx.root) === 'pending';
|
|
254
|
+
if (!['STATE_AUTHORITY_MISSING', 'STATE_PURGE_INCOMPLETE'].includes(error?.code) &&
|
|
255
|
+
!recoverablePending) throw error;
|
|
256
|
+
purgeStateDir = privateProjectStateDir(ctx.root);
|
|
257
|
+
missingPrivateState = true;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
239
260
|
|
|
240
261
|
const lines = [];
|
|
241
262
|
|
|
242
263
|
let runtime = null;
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
264
|
+
if (!missingPrivateState) {
|
|
265
|
+
try {
|
|
266
|
+
runtime = readRuntime(ctx);
|
|
267
|
+
} catch (err) {
|
|
268
|
+
if (!(err instanceof CliError && err.code === 'RUNTIME_NOT_RUNNING')) throw err;
|
|
269
|
+
}
|
|
247
270
|
}
|
|
248
271
|
if (runtime) {
|
|
249
272
|
try {
|
|
@@ -273,9 +296,28 @@ async function cmdUninstall(ctx, args) {
|
|
|
273
296
|
if (opts.purge) {
|
|
274
297
|
guidance = removeGuidanceBlocks(ctx);
|
|
275
298
|
try {
|
|
276
|
-
|
|
299
|
+
if (path.resolve(purgeStateDir) === path.resolve(privateProjectStateDir(ctx.root))) {
|
|
300
|
+
// Revalidate and delete under the same transition lock used by init
|
|
301
|
+
// and migration. An interrupted deletion remains marked purging.
|
|
302
|
+
purgeStateDir = purgePrivateProjectState(ctx.root);
|
|
303
|
+
} else {
|
|
304
|
+
// Async shutdown and hook removal happened after the initial lookup.
|
|
305
|
+
// Refuse a stale legacy target if project-state routing changed.
|
|
306
|
+
const current = secureExistingProjectStateDirectory(ctx.root).stateDir;
|
|
307
|
+
if (path.resolve(current) !== path.resolve(purgeStateDir)) {
|
|
308
|
+
throw new CliError('PROJECT_PATH_FORBIDDEN', 'Project state route changed during uninstall');
|
|
309
|
+
}
|
|
310
|
+
fs.rmSync(purgeStateDir, { recursive: true, force: true });
|
|
311
|
+
}
|
|
277
312
|
purged = true;
|
|
278
|
-
lines.push(`project data removed: ${
|
|
313
|
+
lines.push(`project data removed: ${purgeStateDir}`);
|
|
314
|
+
const legacyDir = legacyProjectStateDir(ctx.root);
|
|
315
|
+
if (path.resolve(legacyDir) !== path.resolve(purgeStateDir) && fs.existsSync(legacyDir)) {
|
|
316
|
+
lines.push(`legacy project data kept: ${legacyDir}`);
|
|
317
|
+
lines.push('private authority kept; future use requires an explicit offline migration of the legacy source');
|
|
318
|
+
} else if (path.resolve(legacyDir) !== path.resolve(purgeStateDir)) {
|
|
319
|
+
lines.push('private authority kept; a fresh init can now create new private state');
|
|
320
|
+
}
|
|
279
321
|
} catch (err) {
|
|
280
322
|
throw new CliError('PURGE_FAILED', `Could not remove .hello-cc: ${err.message}`);
|
|
281
323
|
}
|
|
@@ -4,10 +4,21 @@ export function createLifecycleCommands(deps) {
|
|
|
4
4
|
const {
|
|
5
5
|
connect, now, addEvent, printResult, CliError, parseOpts, intOpt,
|
|
6
6
|
registerProjectActivity, resolveCurrentPeer, resolveTargetPeer,
|
|
7
|
-
upsertPeer, upsertCanonicalPeerBinding, detectBranch,
|
|
7
|
+
upsertPeer, upsertCanonicalPeerBinding, detectBranch, readAncestorCliInfo,
|
|
8
8
|
writeGuidance, helpEnv, helpJoin, shellExports, path, process
|
|
9
9
|
} = deps;
|
|
10
10
|
|
|
11
|
+
function registrationPid(opts) {
|
|
12
|
+
if (opts.pid !== undefined) return intOpt(opts, 'pid', process.ppid);
|
|
13
|
+
const ancestor = readAncestorCliInfo();
|
|
14
|
+
if (['claude', 'codex'].includes(ancestor?.kind) &&
|
|
15
|
+
(!opts.kind || opts.kind === ancestor.kind) &&
|
|
16
|
+
Number.isSafeInteger(ancestor.pid) && ancestor.pid > 1) {
|
|
17
|
+
return ancestor.pid;
|
|
18
|
+
}
|
|
19
|
+
return process.ppid;
|
|
20
|
+
}
|
|
21
|
+
|
|
11
22
|
async function cmdInit(ctx, args) {
|
|
12
23
|
registerProjectActivity(ctx);
|
|
13
24
|
const opts = parseOpts(args, { booleans: ['no-guidance'] });
|
|
@@ -34,7 +45,7 @@ export function createLifecycleCommands(deps) {
|
|
|
34
45
|
role: opts.role || '',
|
|
35
46
|
worktree: path.resolve(opts.worktree || ctx.cwd),
|
|
36
47
|
branch: opts.branch || detectBranch(ctx.cwd),
|
|
37
|
-
pid:
|
|
48
|
+
pid: registrationPid(opts),
|
|
38
49
|
status: opts.status || 'idle',
|
|
39
50
|
capabilities: Array.isArray(opts.cap) ? opts.cap.join(',') : (opts.cap || opts.capabilities || '')
|
|
40
51
|
};
|
|
@@ -67,7 +78,7 @@ export function createLifecycleCommands(deps) {
|
|
|
67
78
|
role: opts.role || 'peer',
|
|
68
79
|
worktree: path.resolve(opts.worktree || ctx.cwd),
|
|
69
80
|
branch: opts.branch || detectBranch(ctx.cwd),
|
|
70
|
-
pid:
|
|
81
|
+
pid: registrationPid(opts),
|
|
71
82
|
status: opts.status || 'working',
|
|
72
83
|
capabilities: Array.isArray(opts.cap) ? opts.cap.join(',') : (opts.cap || opts.capabilities || 'manual-shell')
|
|
73
84
|
};
|
|
@@ -7,6 +7,7 @@ import { parseOpts, validateOpts, wantsHelp, intOpt } from '../../cli-args.mjs';
|
|
|
7
7
|
import { printResult } from '../../format.mjs';
|
|
8
8
|
import { commandPath } from '../../cli-runtime.mjs';
|
|
9
9
|
import { nativePaths, readNativePointer } from '../../runtime/native/store.mjs';
|
|
10
|
+
import { openPrivateAppendFile } from '../../runtime/private-file.mjs';
|
|
10
11
|
import { nativeRequest } from '../../runtime/native/client.mjs';
|
|
11
12
|
import { startNativeService } from '../../runtime/native/service.mjs';
|
|
12
13
|
|
|
@@ -37,15 +38,21 @@ export function createNativeCommands(deps) {
|
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
const paths = nativePaths(ctx, { create: true });
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
let log;
|
|
42
|
+
let child;
|
|
43
|
+
try {
|
|
44
|
+
log = openPrivateAppendFile(paths.log);
|
|
45
|
+
child = spawn(process.execPath, [commandPath(), '--root', fs.realpathSync(ctx.root), '--db', path.resolve(ctx.dbPath), 'native', 'serve'], {
|
|
46
|
+
cwd: ctx.root, env: process.env, detached: true, stdio: ['ignore', log, log]
|
|
47
|
+
});
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (error?.code === 'PROJECT_PATH_FORBIDDEN') {
|
|
50
|
+
throw new CliError('NATIVE_STATE_UNSAFE', error.message);
|
|
51
|
+
}
|
|
52
|
+
throw error;
|
|
53
|
+
} finally {
|
|
54
|
+
if (log !== undefined) fs.closeSync(log);
|
|
44
55
|
}
|
|
45
|
-
const child = spawn(process.execPath, [commandPath(), '--root', fs.realpathSync(ctx.root), '--db', path.resolve(ctx.dbPath), 'native', 'serve'], {
|
|
46
|
-
cwd: ctx.root, env: process.env, detached: true, stdio: ['ignore', log, log]
|
|
47
|
-
});
|
|
48
|
-
fs.closeSync(log);
|
|
49
56
|
let failed;
|
|
50
57
|
child.on('error', (error) => { failed = error; });
|
|
51
58
|
child.unref();
|
|
@@ -113,7 +120,7 @@ export function createNativeCommands(deps) {
|
|
|
113
120
|
const data = fs.readFileSync(path.resolve(ctx.cwd, opts['response-file']), 'utf8');
|
|
114
121
|
if (data.length > 100000) throw new CliError('BAD_ARGS', 'Response file exceeds the limit');
|
|
115
122
|
try { response = JSON.parse(data); } catch { throw new CliError('BAD_ARGS', 'Response file must contain JSON'); }
|
|
116
|
-
if (!response || typeof response !== 'object' || Array.isArray(response) || Object.keys(response).some(key => !['permissions','scope','answers'].includes(key))) throw new CliError('BAD_ARGS', 'Response file accepts permissions, scope and
|
|
123
|
+
if (!response || typeof response !== 'object' || Array.isArray(response) || Object.keys(response).some(key => !['permissions','scope','answers','content'].includes(key))) throw new CliError('BAD_ARGS', 'Response file accepts permissions, scope, answers and MCP content only');
|
|
117
124
|
}
|
|
118
125
|
const request = pending[0];
|
|
119
126
|
result = await nativeRequest(ctx, 'POST', '/respond', { peer: opts.peer, generation: state.generation,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Coordination query commands extracted from bin/hcc.mjs: peers, status, state, prompt.
|
|
2
2
|
|
|
3
|
+
import { shellQuoteArg } from '../../format.mjs';
|
|
4
|
+
|
|
3
5
|
export function createQueryCommands(deps) {
|
|
4
6
|
const {
|
|
5
7
|
connect, now, printResult, table, CliError, ACTIVE_PEER_TTL,
|
|
@@ -63,7 +65,10 @@ export function createQueryCommands(deps) {
|
|
|
63
65
|
const peer = required(opts, 'peer', 'HCC_PEER');
|
|
64
66
|
const kind = opts.kind || 'codex';
|
|
65
67
|
const role = opts.role || 'peer';
|
|
66
|
-
const cmd =
|
|
68
|
+
const cmd = ['node', commandPath(), '--root', ctx.root, '--db', ctx.dbPath].map(shellQuoteArg).join(' ');
|
|
69
|
+
const quotedPeer = shellQuoteArg(peer);
|
|
70
|
+
const quotedKind = shellQuoteArg(kind);
|
|
71
|
+
const quotedRole = shellQuoteArg(role);
|
|
67
72
|
const text = `You are ${peer}, a ${kind} ${role} session working in this project.
|
|
68
73
|
|
|
69
74
|
Use hcc as the shared coordination bus for this terminal session. This
|
|
@@ -71,23 +76,23 @@ project uses a flat peer mesh: there is no required main/worker hierarchy.
|
|
|
71
76
|
|
|
72
77
|
Run these commands before changing files:
|
|
73
78
|
|
|
74
|
-
${cmd} register --peer ${
|
|
75
|
-
${cmd} state --peer ${
|
|
76
|
-
${cmd} msg inbox --peer ${
|
|
77
|
-
${cmd} task next --peer ${
|
|
79
|
+
${cmd} register --peer ${quotedPeer} --kind ${quotedKind} --role ${quotedRole}
|
|
80
|
+
${cmd} state --peer ${quotedPeer}
|
|
81
|
+
${cmd} msg inbox --peer ${quotedPeer}
|
|
82
|
+
${cmd} task next --peer ${quotedPeer}
|
|
78
83
|
|
|
79
84
|
Coordination rules:
|
|
80
85
|
- Claim exactly one task before editing.
|
|
81
86
|
- If state shows a current task for ${peer}, continue that task before claiming another pending task.
|
|
82
87
|
- Before editing a file, directory, module, or shared test resource, run:
|
|
83
|
-
${cmd} lock acquire --peer ${
|
|
88
|
+
${cmd} lock acquire --peer ${quotedPeer} --resource <path-or-module> [--scope <scope>] --task <task-id>
|
|
84
89
|
- If a lock is held by another peer, message that peer instead of editing:
|
|
85
|
-
${cmd} msg send --from ${
|
|
90
|
+
${cmd} msg send --from ${quotedPeer} --to <peer-id> --body "<question>"
|
|
86
91
|
- Report progress or requests through msg send.
|
|
87
92
|
- Before stopping, run tests, create a handoff, and release locks:
|
|
88
|
-
${cmd} handoff create --from ${
|
|
89
|
-
${cmd} task done --peer ${
|
|
90
|
-
${cmd} lock release --peer ${
|
|
93
|
+
${cmd} handoff create --from ${quotedPeer} --task <task-id> --summary "<what changed>" --tests "<commands/results>" --risks "<known risks>"
|
|
94
|
+
${cmd} task done --peer ${quotedPeer} --id <task-id> --summary "<done summary>"
|
|
95
|
+
${cmd} lock release --peer ${quotedPeer} --resource <path-or-module> [--scope <scope>]
|
|
91
96
|
`;
|
|
92
97
|
printResult(ctx, { prompt: text }, () => text);
|
|
93
98
|
}
|
package/lib/cli/commands/run.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { CliError } from '../../shared/errors.mjs';
|
|
|
11
11
|
import { tx } from '../../db/schema.mjs';
|
|
12
12
|
import { parseOpts, validateOpts } from '../../cli-args.mjs';
|
|
13
13
|
import { runtimeProcessIdentity } from '../../runtime/state.mjs';
|
|
14
|
+
import { ensurePrivateProjectBufferDirectory } from '../../runtime/project-path.mjs';
|
|
14
15
|
import { withBufferDirectoryLease } from '../../runtime/buffer-directory-lease.mjs';
|
|
15
16
|
import {
|
|
16
17
|
externalBufferEvidence,
|
|
@@ -178,8 +179,7 @@ async function cmdRunWebManaged(ctx, { id, kind, role, cwd, command, commandArgs
|
|
|
178
179
|
const ptyModule = await import('node-pty');
|
|
179
180
|
const pty = ptyModule.default || ptyModule;
|
|
180
181
|
|
|
181
|
-
const bufsDir =
|
|
182
|
-
fs.mkdirSync(bufsDir, { recursive: true });
|
|
182
|
+
const bufsDir = ensurePrivateProjectBufferDirectory(ctx.root, BUFS_DIR_NAME);
|
|
183
183
|
const outFile = path.join(bufsDir, `${id}.out`);
|
|
184
184
|
const inFile = path.join(bufsDir, `${id}.in`);
|
|
185
185
|
const resizeFile = path.join(bufsDir, `${id}.resize`);
|
|
@@ -219,8 +219,8 @@ async function cmdRunWebManaged(ctx, { id, kind, role, cwd, command, commandArgs
|
|
|
219
219
|
}
|
|
220
220
|
for (const file of bufferFiles) fs.rmSync(file, { force: true });
|
|
221
221
|
}
|
|
222
|
-
fs.writeFileSync(inFile, '');
|
|
223
|
-
fs.writeFileSync(resizeFile, '');
|
|
222
|
+
fs.writeFileSync(inFile, '', { flag: 'wx', mode: 0o600 });
|
|
223
|
+
fs.writeFileSync(resizeFile, '', { flag: 'wx', mode: 0o600 });
|
|
224
224
|
fs.writeFileSync(metaFile, JSON.stringify({
|
|
225
225
|
id,
|
|
226
226
|
generation: bufferGeneration,
|
|
@@ -231,7 +231,7 @@ async function cmdRunWebManaged(ctx, { id, kind, role, cwd, command, commandArgs
|
|
|
231
231
|
wrapper_pid: process.pid,
|
|
232
232
|
...(publishingWrapperIdentity ? { wrapper_identity: publishingWrapperIdentity } : {}),
|
|
233
233
|
publishing: true
|
|
234
|
-
}));
|
|
234
|
+
}), { flag: 'wx', mode: 0o600 });
|
|
235
235
|
|
|
236
236
|
const db = connect(ctx);
|
|
237
237
|
try {
|
|
@@ -359,7 +359,8 @@ async function cmdRunWebManaged(ctx, { id, kind, role, cwd, command, commandArgs
|
|
|
359
359
|
if (!ownsExternalBufferGroup()) {
|
|
360
360
|
throw new CliError('EXTERNAL_SESSION_SUPERSEDED', `External session ${id} was replaced during startup`);
|
|
361
361
|
}
|
|
362
|
-
outFd = fs.openSync(outFile,
|
|
362
|
+
outFd = fs.openSync(outFile, fs.constants.O_WRONLY | fs.constants.O_CREAT |
|
|
363
|
+
fs.constants.O_EXCL | (fs.constants.O_NOFOLLOW || 0), 0o600);
|
|
363
364
|
});
|
|
364
365
|
} catch (error) {
|
|
365
366
|
await failStartup(error?.code || 'external_buffer_open_failed');
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { tx } from '../../db/schema.mjs';
|
|
3
|
+
import { automaticResultKey } from '../../shared/automatic-result-key.mjs';
|
|
3
4
|
import { CliError } from '../../shared/errors.mjs';
|
|
4
5
|
|
|
5
6
|
export const RESULT_EVENT = 'task.result.recorded';
|
|
@@ -18,7 +19,7 @@ function text(value, field, max, required = false) {
|
|
|
18
19
|
|
|
19
20
|
// Evidence records do not mutate task status. The owner is checked in the same
|
|
20
21
|
// transaction as the insert, including after a concurrent task takeover.
|
|
21
|
-
export function recordTaskResult(db, input, { now = () => Math.floor(Date.now() / 1000), addEvent } = {}) {
|
|
22
|
+
export function recordTaskResult(db, input, { now = () => Math.floor(Date.now() / 1000), addEvent, automatic = false } = {}) {
|
|
22
23
|
if (!input || typeof input !== 'object' || Array.isArray(input)) throw new CliError('BAD_REQUEST', 'A result object is required');
|
|
23
24
|
const peer = text(input.peer, 'peer', 200, true);
|
|
24
25
|
const taskId = input.taskId;
|
|
@@ -29,21 +30,35 @@ export function recordTaskResult(db, input, { now = () => Math.floor(Date.now()
|
|
|
29
30
|
throw new CliError('BAD_REQUEST', 'Unsupported result stage, status, kind or source');
|
|
30
31
|
}
|
|
31
32
|
if (source === 'executor' && stage !== 'local') throw new CliError('BAD_REQUEST', 'Executor records can only report local evidence');
|
|
33
|
+
if (automatic && (source !== 'executor' || !['command', 'diff'].includes(kind))) {
|
|
34
|
+
throw new CliError('BAD_REQUEST', 'Automatic results must be local executor commands or diffs');
|
|
35
|
+
}
|
|
32
36
|
const evidence = input.evidence || [];
|
|
33
37
|
if (!Array.isArray(evidence) || evidence.length > 20) throw new CliError('BAD_REQUEST', 'evidence must contain up to 20 text references');
|
|
34
38
|
const references = evidence.map((entry) => text(entry, 'evidence', 2000, true));
|
|
35
39
|
if (source === 'user' && status === 'passed' && !references.length) {
|
|
36
40
|
throw new CliError('BAD_REQUEST', 'A passed verification needs an evidence reference');
|
|
37
41
|
}
|
|
42
|
+
const executorId = text(input.executorId, 'executorId', 512) || null;
|
|
43
|
+
const threadId = text(input.threadId, 'threadId', 512) || null;
|
|
44
|
+
const turnId = text(input.turnId, 'turnId', 512) || null;
|
|
45
|
+
const itemId = text(input.itemId, 'itemId', 512) || null;
|
|
46
|
+
if (automatic && (!executorId || !threadId || !turnId || (kind === 'command' && !itemId))) {
|
|
47
|
+
throw new CliError('BAD_REQUEST', 'Automatic results require executor, thread, turn and command item identities');
|
|
48
|
+
}
|
|
49
|
+
const resultKey = automatic
|
|
50
|
+
? automaticResultKey({ executorId, threadId, turnId, kind, itemId: kind === 'command' ? itemId : null })
|
|
51
|
+
: input.resultKey;
|
|
38
52
|
const record = {
|
|
39
|
-
result_id: randomUUID(), result_key: text(
|
|
53
|
+
result_id: randomUUID(), result_key: text(resultKey, 'resultKey', 700) || randomUUID(),
|
|
40
54
|
peer, task_id: taskId, stage, status, kind, source,
|
|
41
|
-
title:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
55
|
+
title: automatic ? (kind === 'command' ? 'Command execution' : 'Turn changes awaiting review')
|
|
56
|
+
: text(input.title, 'title', 300, true),
|
|
57
|
+
details: automatic ? '' : text(input.details, 'details', 64000),
|
|
58
|
+
evidence: automatic ? [] : references,
|
|
59
|
+
command: automatic ? '' : text(input.command, 'command', 64000),
|
|
60
|
+
diff: automatic ? '' : text(input.diff, 'diff', 64000),
|
|
61
|
+
executor_id: executorId, thread_id: threadId, turn_id: turnId, item_id: itemId,
|
|
47
62
|
exit_code: Number.isSafeInteger(input.exitCode) ? input.exitCode : null,
|
|
48
63
|
created_at: now()
|
|
49
64
|
};
|
|
@@ -55,8 +70,10 @@ export function recordTaskResult(db, input, { now = () => Math.floor(Date.now()
|
|
|
55
70
|
AND task_id = ? AND actor = ? AND json_valid(payload) AND json_extract(payload, '$.result_key') = ?
|
|
56
71
|
ORDER BY id DESC LIMIT 1`).get(RESULT_EVENT, taskId, peer, record.result_key);
|
|
57
72
|
if (previous) return { ...JSON.parse(previous.payload), id: previous.id, created_at: previous.created_at };
|
|
58
|
-
|
|
59
|
-
|
|
73
|
+
if (!automatic) {
|
|
74
|
+
record.task_title = task.title;
|
|
75
|
+
record.task_status = task.status;
|
|
76
|
+
}
|
|
60
77
|
if (addEvent) addEvent(db, RESULT_EVENT, peer, taskId, record);
|
|
61
78
|
else db.prepare('INSERT INTO events(type,actor,task_id,payload,created_at) VALUES(?,?,?,?,?)')
|
|
62
79
|
.run(RESULT_EVENT, peer, taskId, JSON.stringify(record), record.created_at);
|