@logicseek/hello-cc 1.0.5 → 1.1.0-rc.3
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 +72 -0
- package/docs/codex-account.zh-CN.md +38 -0
- package/docs/codex-protocol.md +59 -0
- package/docs/commands.md +9 -0
- package/docs/commands.zh-CN.md +7 -0
- package/docs/macos-process-identity.md +2 -0
- package/docs/native.md +179 -14
- package/docs/native.zh-CN.md +136 -6
- package/docs/web-handoff.zh-CN.md +118 -8
- package/lib/cli/app.mjs +47 -4
- package/lib/cli/commands/coordination.mjs +14 -4
- package/lib/cli/commands/dsh.mjs +1 -1
- package/lib/cli/commands/gc.mjs +22 -15
- package/lib/cli/commands/hook.mjs +19 -3
- package/lib/cli/commands/install.mjs +80 -10
- package/lib/cli/commands/misc.mjs +36 -9
- package/lib/cli/commands/native.mjs +4 -38
- package/lib/cli/commands/peer.mjs +3 -0
- package/lib/cli/commands/run.mjs +13 -6
- package/lib/cli/commands/tmux.mjs +1 -2
- package/lib/cli-runtime.mjs +19 -2
- package/lib/core/coordination/results.mjs +27 -10
- package/lib/core/sessions/hook-root-identity.mjs +47 -0
- package/lib/core/sessions/launch.mjs +7 -1
- package/lib/db/connection.mjs +140 -42
- package/lib/db/schema.mjs +59 -0
- package/lib/diagnostics/codex-protocol.mjs +105 -0
- package/lib/guidance.mjs +21 -36
- package/lib/integrations/codex-account.mjs +143 -0
- 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 +16 -11
- package/lib/integrations/mcp-elicitation.mjs +116 -0
- package/lib/integrations/mcp-url-elicitation.mjs +25 -0
- package/lib/integrations/native/claude.mjs +24 -6
- package/lib/integrations/native/codex.mjs +38 -7
- package/lib/integrations/native/dsh-acp.mjs +67 -24
- package/lib/integrations/native/index.mjs +4 -1
- package/lib/integrations/native/interactions.mjs +2 -1
- package/lib/integrations/native/jsonrpc.mjs +37 -5
- package/lib/integrations/native/telemetry.mjs +30 -0
- package/lib/process/identity.mjs +4 -0
- package/lib/process/pinned-cwd.mjs +302 -0
- package/lib/process/pinned-guidance.mjs +138 -0
- package/lib/process/pinned-project-state.mjs +105 -0
- package/lib/process/selected-cwd-identity.mjs +95 -0
- package/lib/runtime/buffer-evidence.mjs +2 -1
- package/lib/runtime/client.mjs +9 -0
- package/lib/runtime/native/client.mjs +4 -1
- package/lib/runtime/native/history.mjs +140 -0
- package/lib/runtime/native/launcher.mjs +106 -0
- package/lib/runtime/native/service.mjs +64 -8
- package/lib/runtime/native/store.mjs +83 -58
- 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 +92 -20
- package/lib/runtime/project-trust.mjs +79 -0
- package/lib/runtime/projects.mjs +9 -3
- package/lib/runtime/state-migration.mjs +697 -0
- package/lib/runtime/state.mjs +69 -27
- package/lib/shared/automatic-result-key.mjs +6 -0
- package/lib/shared/json-file.mjs +55 -6
- package/lib/shared/redact.mjs +2 -1
- package/lib/ui/help.mjs +6 -4
- package/lib/web/agent-defaults.mjs +110 -0
- package/lib/web/auto-attach.mjs +3 -2
- package/lib/web/browser/contracts.d.ts +10 -0
- package/lib/web/browser/core.mjs +1997 -0
- package/lib/web/browser/project-data.mjs +158 -0
- package/lib/web/browser/session-sync.mjs +82 -0
- package/lib/web/browser-assets.mjs +19 -0
- package/lib/web/buffer-gc-runtime.mjs +2 -2
- package/lib/web/codex-app-server.mjs +111 -37
- package/lib/web/codex-sessions.mjs +61 -12
- package/lib/web/context-files.mjs +82 -0
- package/lib/web/cookie-auth.mjs +51 -3
- package/lib/web/external-sessions.mjs +76 -9
- package/lib/web/http-routes.mjs +238 -25
- package/lib/web/http.mjs +50 -4
- package/lib/web/liveness-reaper.mjs +2 -2
- package/lib/web/native-sessions.mjs +323 -14
- package/lib/web/project-contexts.mjs +236 -44
- package/lib/web/project-file-write.mjs +249 -0
- package/lib/web/project-files.mjs +268 -0
- package/lib/web/pty-sessions.mjs +11 -3
- package/lib/web/runtime-main.mjs +132 -80
- package/lib/web/session-serialize.mjs +87 -8
- package/lib/web/session-state-sync.mjs +98 -0
- package/lib/web/shutdown-diagnostics.mjs +38 -0
- package/lib/web/shutdown-response.mjs +21 -0
- package/lib/web/shutdown-state.mjs +91 -0
- package/lib/web/startup.mjs +103 -35
- package/lib/web/task-results.mjs +19 -19
- package/lib/web/tls.mjs +84 -33
- package/lib/web/tmux-sessions.mjs +73 -9
- package/lib/web/tmux-stream.mjs +36 -8
- package/lib/web/ui-agent-defaults.mjs +149 -0
- package/lib/web/ui-agent-start.mjs +330 -0
- package/lib/web/ui-codex-account.mjs +81 -0
- package/lib/web/ui-codex.mjs +105 -85
- package/lib/web/ui-command-palette.mjs +21 -5
- package/lib/web/ui-file-writes.mjs +220 -0
- package/lib/web/ui-files.mjs +231 -0
- package/lib/web/ui-handoff.mjs +10 -2
- package/lib/web/ui-history.mjs +175 -105
- package/lib/web/ui-i18n.mjs +280 -8
- package/lib/web/ui-interactions.mjs +107 -6
- package/lib/web/ui-native-timeline.mjs +326 -0
- package/lib/web/ui-native.mjs +93 -43
- package/lib/web/ui-review.mjs +5 -3
- package/lib/web/ui-safe-markdown.mjs +79 -0
- package/lib/web/ui-session-tools.mjs +224 -0
- package/lib/web/ui-template.mjs +29 -1968
- package/lib/web/ui-terminal-find.mjs +5 -3
- package/lib/web/ui-workbench.mjs +5 -3
- package/lib/web/ui-workspace.mjs +3 -0
- package/package.json +8 -2
- package/scripts/benchmark-session-sync.mjs +53 -0
- package/scripts/codex-protocol.mjs +88 -0
- package/scripts/dsh-approval-layers-acceptance.mjs +172 -0
- package/scripts/dsh-package-provenance.mjs +118 -0
- package/scripts/mcp-form-live-fixture.mjs +81 -0
- package/scripts/native-bounded-live-acceptance.mjs +193 -0
- package/scripts/native-fixture-root.mjs +49 -0
- package/scripts/native-installed-acceptance.mjs +611 -0
- package/scripts/native-stability-checks.mjs +40 -0
- package/scripts/regression-cookie-expiry.mjs +70 -0
- package/scripts/regression-http-transport.mjs +9 -0
- package/scripts/regression-terminal-marker.mjs +95 -0
- package/scripts/regression.mjs +481 -219
- package/scripts/shim-reentry-probe.mjs +105 -0
- package/scripts/web-acceptance-diagnostics.mjs +90 -0
- package/scripts/web-codex-account-acceptance.mjs +178 -0
- package/scripts/web-file-preview-checks.mjs +302 -0
- package/scripts/web-mcp-form-acceptance.mjs +134 -0
- package/scripts/web-mcp-form-live-acceptance.mjs +368 -0
- package/scripts/web-native-handoff-checks.mjs +128 -0
- package/scripts/web-native-interaction-acceptance.mjs +219 -18
- package/scripts/web-native-interaction-diagnostics.mjs +80 -0
- package/scripts/web-native-model-acceptance.mjs +22 -7
- package/scripts/web-session-tools-checks.mjs +112 -0
- package/scripts/web-workbench-acceptance.mjs +435 -0
- package/scripts/web-workbench-installed-acceptance.mjs +122 -0
|
@@ -4,6 +4,8 @@
|
|
|
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';
|
|
8
|
+
import { HOOK_ROOT_IDENTITY_ENV, assertHookRootIdentity } from '../../core/sessions/hook-root-identity.mjs';
|
|
7
9
|
|
|
8
10
|
export function createHookCommand(deps) {
|
|
9
11
|
const {
|
|
@@ -85,21 +87,34 @@ async function cmdHook(ctx, args) {
|
|
|
85
87
|
const hccRoot = (nativeOwner || !isDsh) && process.env.HCC_ROOT
|
|
86
88
|
? path.resolve(process.env.HCC_ROOT)
|
|
87
89
|
: path.resolve(hookCwd);
|
|
90
|
+
// First-party launches carry the original directory inode. Reject a stale
|
|
91
|
+
// callback before database resolution or project activity can target a new
|
|
92
|
+
// directory installed at the old pathname. dsh service hooks are intentionally
|
|
93
|
+
// payload-scoped and do not inherit a service-wide project identity.
|
|
94
|
+
const launchRootIdentity = (!isDsh || nativeOwner)
|
|
95
|
+
? assertHookRootIdentity(hccRoot, process.env[HOOK_ROOT_IDENTITY_ENV]) : null;
|
|
88
96
|
|
|
89
97
|
const dshProject = isDsh && !nativeOwner ? resolveProjectDatabase({ root: hccRoot, createStateDir: false }) : null;
|
|
90
98
|
const nativeProject = nativeOwner ? resolveProjectDatabase({ root: hccRoot,
|
|
91
99
|
db: process.env.HCC_DB, createStateDir: false }) : null;
|
|
92
|
-
const
|
|
100
|
+
const sameHookRoot = () => {
|
|
101
|
+
if (!ctx.root) return false;
|
|
102
|
+
try { return fs.realpathSync(ctx.root) === fs.realpathSync(hccRoot); }
|
|
103
|
+
catch { return path.resolve(ctx.root) === hccRoot; }
|
|
104
|
+
};
|
|
105
|
+
const ordinaryDb = (sameHookRoot() ? ctx.dbPath : null) ||
|
|
93
106
|
process.env.HCC_DB;
|
|
94
107
|
if (nativeProject && !fs.existsSync(nativeProject.db)) {
|
|
95
108
|
throw new CliError('NATIVE_HOOK_OWNERSHIP_MISMATCH', 'Native hook database must already be owned by its runtime');
|
|
96
109
|
}
|
|
110
|
+
const ordinaryDefaultDb = !dshProject && !nativeProject && !ordinaryDb
|
|
111
|
+
? projectDbPath(hccRoot) : null;
|
|
97
112
|
const hookCtx = {
|
|
98
113
|
...ctx,
|
|
99
114
|
root: nativeProject?.root || dshProject?.root || hccRoot,
|
|
100
|
-
dbPath: nativeProject?.db || dshProject?.db ||
|
|
115
|
+
dbPath: nativeProject?.db || dshProject?.db || ordinaryDefaultDb || path.resolve(ordinaryDb),
|
|
116
|
+
...(launchRootIdentity ? { initialRootIdentity: launchRootIdentity } : {})
|
|
101
117
|
};
|
|
102
|
-
if (!nativeOwner) registerProjectActivity(hookCtx);
|
|
103
118
|
|
|
104
119
|
// dsh uses only its true session ID. Ordinary hooks retain environment,
|
|
105
120
|
// resume, session, and terminal identity precedence.
|
|
@@ -120,6 +135,7 @@ async function cmdHook(ctx, args) {
|
|
|
120
135
|
// Hook delivery must not migrate another workspace's registered database.
|
|
121
136
|
const db = isDsh || nativeOwner ? connect(hookCtx, { migrateRegistered: false }) : connect(hookCtx);
|
|
122
137
|
try {
|
|
138
|
+
if (!nativeOwner) registerProjectActivity(hookCtx);
|
|
123
139
|
if (nativeOwner) {
|
|
124
140
|
// Native delivery owns context injection and completion receipts. Hooks
|
|
125
141
|
// only renew its verified owner, never turn a native binding into a
|
|
@@ -1,17 +1,23 @@
|
|
|
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';
|
|
8
|
+
import { captureSelectedCwdIdentity } from '../../process/selected-cwd-identity.mjs';
|
|
9
|
+
import { purgePinnedLegacyProjectState } from '../../process/pinned-project-state.mjs';
|
|
4
10
|
|
|
5
11
|
export function createInstallCommands(deps) {
|
|
6
12
|
const {
|
|
7
13
|
path, fs, process, CliError, parseOpts, validateOpts, wantsHelp,
|
|
8
14
|
printResult, commandPath, connect, readRuntime, runtimeRequest,
|
|
9
|
-
|
|
15
|
+
globalRuntimePath, PRODUCT_NAME, VERSION, NPM_PACKAGE_NAME,
|
|
10
16
|
helpInstallHooks, helpShim, helpUninstall, helpUpdate,
|
|
11
17
|
loadSetup, shellCommand,
|
|
12
18
|
|
|
13
19
|
|
|
14
|
-
sameResolvedPath, writeGuidance, removeGuidanceBlocks,
|
|
20
|
+
sameResolvedPath, writeGuidance, removeGuidanceBlocks, spawnPinnedStateProcess,
|
|
15
21
|
|
|
16
22
|
} = deps;
|
|
17
23
|
|
|
@@ -236,14 +242,48 @@ async function cmdUninstall(ctx, args) {
|
|
|
236
242
|
if (opts.purge && !opts.yes) {
|
|
237
243
|
throw new CliError('CONFIRM_REQUIRED', 'Refusing to purge project data without --yes');
|
|
238
244
|
}
|
|
245
|
+
const selectedRoot = opts.purge ? captureSelectedCwdIdentity(ctx.root) : null;
|
|
246
|
+
let selectedLegacyState = null;
|
|
247
|
+
try {
|
|
248
|
+
// Resolve before changing hooks, guidance, or runtime state. An unsafe root
|
|
249
|
+
// with unmigrated legacy data must not be treated as an empty private store.
|
|
250
|
+
let purgeStateDir = null;
|
|
251
|
+
let purgeIsLegacy = false;
|
|
252
|
+
let missingPrivateState = false;
|
|
253
|
+
if (opts.purge) {
|
|
254
|
+
try {
|
|
255
|
+
const resolved = secureExistingProjectStateDirectory(ctx.root);
|
|
256
|
+
purgeStateDir = resolved.stateDir;
|
|
257
|
+
purgeIsLegacy = path.resolve(purgeStateDir) === path.join(selectedRoot.canonical, '.hello-cc');
|
|
258
|
+
if (purgeIsLegacy && resolved.exists) {
|
|
259
|
+
selectedLegacyState = captureSelectedCwdIdentity(purgeStateDir);
|
|
260
|
+
selectedRoot.assertUnchanged();
|
|
261
|
+
}
|
|
262
|
+
} catch (error) {
|
|
263
|
+
if (projectStateDir(ctx.root) !== privateProjectStateDir(ctx.root)) throw error;
|
|
264
|
+
const recoverablePending = error?.code === 'STATE_MIGRATION_REQUIRED' &&
|
|
265
|
+
inspectPrivateProjectAuthority(ctx.root) === 'pending';
|
|
266
|
+
if (!['STATE_AUTHORITY_MISSING', 'STATE_PURGE_INCOMPLETE'].includes(error?.code) &&
|
|
267
|
+
!recoverablePending) throw error;
|
|
268
|
+
purgeStateDir = privateProjectStateDir(ctx.root);
|
|
269
|
+
missingPrivateState = true;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
239
272
|
|
|
240
273
|
const lines = [];
|
|
241
274
|
|
|
242
275
|
let runtime = null;
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
276
|
+
let runtimePointer = null;
|
|
277
|
+
if (!missingPrivateState) {
|
|
278
|
+
// Keep the validated state route fixed across the asynchronous stop probe.
|
|
279
|
+
// Re-deriving it afterwards could remove another project's pointer if a
|
|
280
|
+
// user-supplied root alias is retargeted in the meantime.
|
|
281
|
+
runtimePointer = path.join(secureExistingProjectStateDirectory(ctx.root).stateDir, 'runtime.json');
|
|
282
|
+
try {
|
|
283
|
+
runtime = readRuntime(ctx);
|
|
284
|
+
} catch (err) {
|
|
285
|
+
if (!(err instanceof CliError && err.code === 'RUNTIME_NOT_RUNNING')) throw err;
|
|
286
|
+
}
|
|
247
287
|
}
|
|
248
288
|
if (runtime) {
|
|
249
289
|
try {
|
|
@@ -251,7 +291,7 @@ async function cmdUninstall(ctx, args) {
|
|
|
251
291
|
lines.push('runtime stopped');
|
|
252
292
|
} catch (err) {
|
|
253
293
|
if (!(err instanceof CliError && err.code === 'RUNTIME_UNREACHABLE')) throw err;
|
|
254
|
-
try { fs.rmSync(
|
|
294
|
+
try { fs.rmSync(runtimePointer, { force: true }); } catch {}
|
|
255
295
|
lines.push('stale runtime file removed');
|
|
256
296
|
}
|
|
257
297
|
} else {
|
|
@@ -259,6 +299,7 @@ async function cmdUninstall(ctx, args) {
|
|
|
259
299
|
}
|
|
260
300
|
|
|
261
301
|
const { uninstallClaudeHooks, uninstallCodexHooks, uninstallShims, uninstallPathEntry } = await loadSetup();
|
|
302
|
+
selectedRoot?.assertUnchanged();
|
|
262
303
|
const claude = uninstallClaudeHooks();
|
|
263
304
|
const codex = uninstallCodexHooks();
|
|
264
305
|
const shims = uninstallShims();
|
|
@@ -271,11 +312,36 @@ async function cmdUninstall(ctx, args) {
|
|
|
271
312
|
let guidance = [];
|
|
272
313
|
let purged = false;
|
|
273
314
|
if (opts.purge) {
|
|
274
|
-
guidance = removeGuidanceBlocks(ctx);
|
|
315
|
+
guidance = removeGuidanceBlocks(ctx, { expectedBinding: selectedRoot });
|
|
275
316
|
try {
|
|
276
|
-
|
|
317
|
+
selectedRoot.assertUnchanged();
|
|
318
|
+
if (!purgeIsLegacy) {
|
|
319
|
+
// Revalidate and delete under the same transition lock used by init
|
|
320
|
+
// and migration. An interrupted deletion remains marked purging.
|
|
321
|
+
purgeStateDir = purgePrivateProjectState(ctx.root);
|
|
322
|
+
} else {
|
|
323
|
+
// Async shutdown and hook removal happened after the initial lookup.
|
|
324
|
+
// Refuse a stale legacy target if project-state routing changed.
|
|
325
|
+
const current = secureExistingProjectStateDirectory(ctx.root);
|
|
326
|
+
if (path.resolve(current.stateDir) !== path.resolve(purgeStateDir)) {
|
|
327
|
+
throw new CliError('PROJECT_PATH_FORBIDDEN', 'Project state route changed during uninstall');
|
|
328
|
+
}
|
|
329
|
+
if (selectedLegacyState) {
|
|
330
|
+
purgePinnedLegacyProjectState(ctx.root, purgeStateDir, selectedRoot, selectedLegacyState,
|
|
331
|
+
{ spawnProcess: spawnPinnedStateProcess });
|
|
332
|
+
} else if (current.exists) {
|
|
333
|
+
throw new CliError('PROJECT_PATH_FORBIDDEN', 'Project state appeared during uninstall');
|
|
334
|
+
}
|
|
335
|
+
}
|
|
277
336
|
purged = true;
|
|
278
|
-
lines.push(`project data removed: ${
|
|
337
|
+
lines.push(`project data removed: ${purgeStateDir}`);
|
|
338
|
+
const legacyDir = legacyProjectStateDir(ctx.root);
|
|
339
|
+
if (path.resolve(legacyDir) !== path.resolve(purgeStateDir) && fs.existsSync(legacyDir)) {
|
|
340
|
+
lines.push(`legacy project data kept: ${legacyDir}`);
|
|
341
|
+
lines.push('private authority kept; future use requires an explicit offline migration of the legacy source');
|
|
342
|
+
} else if (path.resolve(legacyDir) !== path.resolve(purgeStateDir)) {
|
|
343
|
+
lines.push('private authority kept; a fresh init can now create new private state');
|
|
344
|
+
}
|
|
279
345
|
} catch (err) {
|
|
280
346
|
throw new CliError('PURGE_FAILED', `Could not remove .hello-cc: ${err.message}`);
|
|
281
347
|
}
|
|
@@ -285,6 +351,10 @@ async function cmdUninstall(ctx, args) {
|
|
|
285
351
|
}
|
|
286
352
|
|
|
287
353
|
printResult(ctx, { runtime: Boolean(runtime), claude, codex, shims, path_entry: pathEntry, purge: purged, guidance }, () => lines.join('\n'));
|
|
354
|
+
} finally {
|
|
355
|
+
selectedLegacyState?.release();
|
|
356
|
+
selectedRoot?.release();
|
|
357
|
+
}
|
|
288
358
|
}
|
|
289
359
|
|
|
290
360
|
// ─── hcc scan ────────────────────────────────────────────────────────────────
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
// Small utility commands extracted from bin/hcc.mjs: down, find-root, which-real.
|
|
2
|
+
import { performance } from 'node:perf_hooks';
|
|
3
|
+
import { resolveProjectDatabase } from '../../runtime/project-path.mjs';
|
|
2
4
|
|
|
3
5
|
export function createMiscCommands(deps) {
|
|
4
6
|
const {
|
|
@@ -54,7 +56,24 @@ export function createMiscCommands(deps) {
|
|
|
54
56
|
|
|
55
57
|
async function cmdDown(ctx, args) {
|
|
56
58
|
if (args[0] === '--help' || args[0] === '-h') return helpDown();
|
|
57
|
-
const
|
|
59
|
+
const stopStartedAt = performance.now();
|
|
60
|
+
const elapsedSince = (startedAt) => Math.max(0, Math.round(performance.now() - startedAt));
|
|
61
|
+
function stopFailure(error, phase, state = 'unknown', confirmationMs = null) {
|
|
62
|
+
// Retain the original error and transport context. Only fixed phase/state
|
|
63
|
+
// vocabulary and monotonic durations describe the local confirmation.
|
|
64
|
+
error.extra = {
|
|
65
|
+
...error.extra,
|
|
66
|
+
stopPhase: phase,
|
|
67
|
+
state: ['live', 'dead', 'unknown'].includes(state) ? state : 'unknown',
|
|
68
|
+
stopElapsedMs: elapsedSince(stopStartedAt),
|
|
69
|
+
...(confirmationMs === null ? {} : { confirmationMs })
|
|
70
|
+
};
|
|
71
|
+
return error;
|
|
72
|
+
}
|
|
73
|
+
const localPointer = ctx.root
|
|
74
|
+
? path.join(resolveProjectDatabase({ root: ctx.root, createStateDir: false }).stateDir, 'runtime.json')
|
|
75
|
+
: runtimePath(ctx);
|
|
76
|
+
const pointerFiles = [localPointer, globalRuntimePath()];
|
|
58
77
|
let runtime;
|
|
59
78
|
try {
|
|
60
79
|
runtime = readRuntime(ctx);
|
|
@@ -71,13 +90,16 @@ export function createMiscCommands(deps) {
|
|
|
71
90
|
await runtimeRequest(ctx, 'POST', '/api/runtime/stop', {}, runtime);
|
|
72
91
|
} catch (err) {
|
|
73
92
|
if (!(err instanceof CliError && err.code === 'RUNTIME_UNREACHABLE')) throw err;
|
|
74
|
-
if (!stopEvidence) throw err;
|
|
93
|
+
if (!stopEvidence) throw stopFailure(err, 'evidence_unavailable');
|
|
94
|
+
const confirmationStartedAt = performance.now();
|
|
75
95
|
const stopped = await waitForProcessIdentityExit(stopEvidence.owner, {
|
|
76
96
|
timeoutMs: 5_000,
|
|
77
97
|
intervalMs: 25
|
|
78
98
|
});
|
|
79
|
-
|
|
99
|
+
const confirmationMs = elapsedSince(confirmationStartedAt);
|
|
100
|
+
if (stopped.state !== 'dead') throw stopFailure(err, 'exit_unconfirmed', stopped.state, confirmationMs);
|
|
80
101
|
let cleanup;
|
|
102
|
+
let cleanupPhase = 'pointer_cleanup_failed';
|
|
81
103
|
try {
|
|
82
104
|
cleanup = withFileLock(stopEvidence.source, (file) => {
|
|
83
105
|
let current;
|
|
@@ -86,7 +108,10 @@ export function createMiscCommands(deps) {
|
|
|
86
108
|
if (error.code === 'ENOENT') return { reclaimed: 0, blocked: false };
|
|
87
109
|
throw error;
|
|
88
110
|
}
|
|
89
|
-
if (!sameStopPointer(stopEvidence.pointer, current))
|
|
111
|
+
if (!sameStopPointer(stopEvidence.pointer, current)) {
|
|
112
|
+
cleanupPhase = 'pointer_changed';
|
|
113
|
+
throw err;
|
|
114
|
+
}
|
|
90
115
|
// Hold the pointer's existing lock across comparison and reclamation.
|
|
91
116
|
// A replacement owner must never be removed by this stop request.
|
|
92
117
|
return reclaimRuntimePointerFiles([file], {
|
|
@@ -94,8 +119,8 @@ export function createMiscCommands(deps) {
|
|
|
94
119
|
withLock: (_file, operation) => operation(file)
|
|
95
120
|
});
|
|
96
121
|
}, { createParent: false });
|
|
97
|
-
} catch { throw err; }
|
|
98
|
-
if (cleanup.blocked || cleanup.reclaimed > 1) throw err;
|
|
122
|
+
} catch { throw stopFailure(err, cleanupPhase, 'dead', confirmationMs); }
|
|
123
|
+
if (cleanup.blocked || cleanup.reclaimed > 1) throw stopFailure(err, 'pointer_cleanup_failed', 'dead', confirmationMs);
|
|
99
124
|
printResult(ctx, { runtime: stopEvidence.source }, () => cleanup.reclaimed === 1
|
|
100
125
|
? `${PRODUCT_NAME} stale runtime pointer removed`
|
|
101
126
|
: `${PRODUCT_NAME} runtime stopped`);
|
|
@@ -103,16 +128,18 @@ export function createMiscCommands(deps) {
|
|
|
103
128
|
}
|
|
104
129
|
if (runtime.source !== 'env') {
|
|
105
130
|
const processIdentity = runtime.process_identity || runtime.processIdentity || runtime.pid;
|
|
131
|
+
const confirmationStartedAt = performance.now();
|
|
106
132
|
const stopped = await waitForProcessIdentityExit(processIdentity, {
|
|
107
133
|
timeoutMs: 5_000,
|
|
108
134
|
intervalMs: 25
|
|
109
135
|
});
|
|
136
|
+
const confirmationMs = elapsedSince(confirmationStartedAt);
|
|
110
137
|
if (stopped.state !== 'dead') {
|
|
111
|
-
throw new CliError(
|
|
138
|
+
throw stopFailure(new CliError(
|
|
112
139
|
'RUNTIME_STOP_TIMEOUT',
|
|
113
140
|
`Runtime accepted the stop request, but process ${runtime.pid || 'unknown'} did not exit within 5s.`,
|
|
114
|
-
{ pid: runtime.pid || null
|
|
115
|
-
);
|
|
141
|
+
{ pid: runtime.pid || null }
|
|
142
|
+
), 'exit_unconfirmed', stopped.state, confirmationMs);
|
|
116
143
|
}
|
|
117
144
|
}
|
|
118
145
|
printResult(ctx, { runtime: runtime.source || runtime.base_url }, () => `${PRODUCT_NAME} runtime stopped`);
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { spawn } from 'node:child_process';
|
|
4
|
-
import { setTimeout as delay } from 'node:timers/promises';
|
|
5
3
|
import { CliError } from '../../shared/errors.mjs';
|
|
6
4
|
import { parseOpts, validateOpts, wantsHelp, intOpt } from '../../cli-args.mjs';
|
|
7
5
|
import { printResult } from '../../format.mjs';
|
|
8
|
-
import {
|
|
9
|
-
import { nativePaths, readNativePointer } from '../../runtime/native/store.mjs';
|
|
6
|
+
import { ensureNativeRuntime } from '../../runtime/native/launcher.mjs';
|
|
10
7
|
import { nativeRequest } from '../../runtime/native/client.mjs';
|
|
11
8
|
import { startNativeService } from '../../runtime/native/service.mjs';
|
|
12
9
|
|
|
@@ -29,37 +26,6 @@ dsh workers use the ACP profile, not its Web-service terminal.`);
|
|
|
29
26
|
}
|
|
30
27
|
|
|
31
28
|
export function createNativeCommands(deps) {
|
|
32
|
-
async function up(ctx) {
|
|
33
|
-
if (readNativePointer(ctx)) {
|
|
34
|
-
try { return await nativeRequest(ctx, 'GET', '/status', null, { timeoutMs: 2000 }); }
|
|
35
|
-
catch (error) {
|
|
36
|
-
if (error.code !== 'NATIVE_RUNTIME_OFFLINE') throw error;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
const paths = nativePaths(ctx, { create: true });
|
|
40
|
-
const log = fs.openSync(paths.log, fs.constants.O_WRONLY | fs.constants.O_APPEND | fs.constants.O_CREAT | (fs.constants.O_NOFOLLOW || 0), 0o600);
|
|
41
|
-
if (!fs.fstatSync(log).isFile() || fs.fstatSync(log).nlink !== 1) {
|
|
42
|
-
fs.closeSync(log);
|
|
43
|
-
throw new CliError('NATIVE_STATE_UNSAFE', 'Native runtime log must be a private regular file');
|
|
44
|
-
}
|
|
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
|
-
let failed;
|
|
50
|
-
child.on('error', (error) => { failed = error; });
|
|
51
|
-
child.unref();
|
|
52
|
-
const deadline = Date.now() + 10000;
|
|
53
|
-
while (Date.now() < deadline) {
|
|
54
|
-
if (failed) throw new CliError('NATIVE_RUNTIME_START_FAILED', failed.message);
|
|
55
|
-
if (readNativePointer(ctx)) {
|
|
56
|
-
try { return await nativeRequest(ctx, 'GET', '/status', null, { timeoutMs: 1000 }); } catch {}
|
|
57
|
-
}
|
|
58
|
-
await delay(100);
|
|
59
|
-
}
|
|
60
|
-
throw new CliError('NATIVE_RUNTIME_START_FAILED', `Native runtime did not become ready. Inspect ${paths.log}`);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
29
|
async function cmdNative(ctx, args) {
|
|
64
30
|
const command = args[0];
|
|
65
31
|
if (!command || wantsHelp(args)) return nativeHelp();
|
|
@@ -79,7 +45,7 @@ export function createNativeCommands(deps) {
|
|
|
79
45
|
if (command === 'start' && opts.provider === 'claude' && opts.binary !== undefined) throw new CliError('BAD_ARGS', 'Claude workers use the Agent SDK; --binary is available for Codex and dsh');
|
|
80
46
|
if (command === 'send' && !opts.body?.trim()) throw new CliError('BAD_ARGS', 'Missing --body');
|
|
81
47
|
let result;
|
|
82
|
-
if (command === 'up') result = await
|
|
48
|
+
if (command === 'up') result = await ensureNativeRuntime(ctx);
|
|
83
49
|
else if (command === 'serve') {
|
|
84
50
|
const service = await startNativeService(ctx, deps);
|
|
85
51
|
const shutdown = () => { void service.shutdown().catch((error) => console.error(JSON.stringify({ code: error.code, message: error.message }))); };
|
|
@@ -87,7 +53,7 @@ export function createNativeCommands(deps) {
|
|
|
87
53
|
process.once('SIGINT', shutdown);
|
|
88
54
|
return;
|
|
89
55
|
} else if (command === 'start') {
|
|
90
|
-
await
|
|
56
|
+
await ensureNativeRuntime(ctx);
|
|
91
57
|
result = await nativeRequest(ctx, 'POST', '/workers', { peer: opts.peer, provider: opts.provider,
|
|
92
58
|
cwd: opts.cwd ? path.resolve(ctx.cwd, opts.cwd) : ctx.root, model: opts.model, binary: opts.binary, resume: opts.resume });
|
|
93
59
|
} else if (command === 'send') {
|
|
@@ -113,7 +79,7 @@ export function createNativeCommands(deps) {
|
|
|
113
79
|
const data = fs.readFileSync(path.resolve(ctx.cwd, opts['response-file']), 'utf8');
|
|
114
80
|
if (data.length > 100000) throw new CliError('BAD_ARGS', 'Response file exceeds the limit');
|
|
115
81
|
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
|
|
82
|
+
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
83
|
}
|
|
118
84
|
const request = pending[0];
|
|
119
85
|
result = await nativeRequest(ctx, 'POST', '/respond', { peer: opts.peer, generation: state.generation,
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
RUNTIME_SESSION_MUTATION_TIMEOUT_MS,
|
|
5
5
|
RUNTIME_SESSION_START_TIMEOUT_MS
|
|
6
6
|
} from '../../runtime/client.mjs';
|
|
7
|
+
import { captureSelectedCwdSnapshot } from '../../process/selected-cwd-identity.mjs';
|
|
7
8
|
|
|
8
9
|
export function createPeerCommands(deps) {
|
|
9
10
|
const {
|
|
@@ -75,6 +76,7 @@ async function peerStart(ctx, args) {
|
|
|
75
76
|
const kind = inferPeerKind(id, opts.kind, firstCommand);
|
|
76
77
|
const role = opts.role || 'peer';
|
|
77
78
|
const cwd = path.resolve(opts.cwd || ctx.root);
|
|
79
|
+
const cwdIdentity = captureSelectedCwdSnapshot(cwd);
|
|
78
80
|
const built = buildPeerCommand(id, kind, opts, cmdArgs);
|
|
79
81
|
const command = built.command;
|
|
80
82
|
const binding = { ...built.binding, transport: 'tmux', runtime_session_id: id };
|
|
@@ -139,6 +141,7 @@ async function peerStart(ctx, args) {
|
|
|
139
141
|
role,
|
|
140
142
|
command,
|
|
141
143
|
cwd,
|
|
144
|
+
cwdIdentity,
|
|
142
145
|
binding,
|
|
143
146
|
env: childSessionEnv(),
|
|
144
147
|
restartOnEnvChange: Boolean(opts['restart-env']),
|
package/lib/cli/commands/run.mjs
CHANGED
|
@@ -6,8 +6,9 @@ import path from 'node:path';
|
|
|
6
6
|
import fs from 'node:fs';
|
|
7
7
|
import process from 'node:process';
|
|
8
8
|
import { randomBytes } from 'node:crypto';
|
|
9
|
-
import { spawn } from 'node:child_process';
|
|
10
9
|
import { CliError } from '../../shared/errors.mjs';
|
|
10
|
+
import { spawnPinnedCwdProcess, spawnPinnedCwdPty } from '../../process/pinned-cwd.mjs';
|
|
11
|
+
import { captureSelectedCwdSnapshot } from '../../process/selected-cwd-identity.mjs';
|
|
11
12
|
import { tx } from '../../db/schema.mjs';
|
|
12
13
|
import { parseOpts, validateOpts } from '../../cli-args.mjs';
|
|
13
14
|
import { runtimeProcessIdentity } from '../../runtime/state.mjs';
|
|
@@ -57,6 +58,7 @@ async function cmdRun(ctx, args) {
|
|
|
57
58
|
const id = identity.id;
|
|
58
59
|
const role = opts.role || 'peer';
|
|
59
60
|
const cwd = path.resolve(opts.cwd || ctx.cwd);
|
|
61
|
+
const expectedCwdIdentity = captureSelectedCwdSnapshot(cwd);
|
|
60
62
|
const command = cmdArgs.length ? cmdArgs[0] : defaultSessionCommand(kind);
|
|
61
63
|
const commandArgs = cmdArgs.length ? cmdArgs.slice(1) : [];
|
|
62
64
|
const binding = bindingFromRun(id, kind, command, commandArgs, 'hcc-run');
|
|
@@ -67,6 +69,7 @@ async function cmdRun(ctx, args) {
|
|
|
67
69
|
kind,
|
|
68
70
|
role,
|
|
69
71
|
cwd,
|
|
72
|
+
expectedCwdIdentity,
|
|
70
73
|
command,
|
|
71
74
|
commandArgs,
|
|
72
75
|
binding,
|
|
@@ -131,10 +134,12 @@ async function cmdRun(ctx, args) {
|
|
|
131
134
|
db.close();
|
|
132
135
|
}
|
|
133
136
|
console.error(redactedLogText(`${CLI_NAME}: running ${id} (${kind}, ${role}) -> ${command} ${commandArgs.join(' ')}`.trim()));
|
|
134
|
-
const child =
|
|
137
|
+
const child = spawnPinnedCwdProcess(command, commandArgs, {
|
|
135
138
|
cwd,
|
|
139
|
+
expectedIdentity: expectedCwdIdentity,
|
|
136
140
|
stdio: 'inherit',
|
|
137
|
-
env: childSessionEnv({ HCC_PEER: id, HCC_ROOT: ctx.root, HCC_DB: ctx.dbPath }
|
|
141
|
+
env: childSessionEnv({ HCC_PEER: id, HCC_ROOT: ctx.root, HCC_DB: ctx.dbPath },
|
|
142
|
+
process.env, { rootIdentity: ctx.initialRootIdentity })
|
|
138
143
|
});
|
|
139
144
|
const exitCode = await new Promise((resolve) => {
|
|
140
145
|
child.on('exit', (code, signal) => resolve({ code, signal }));
|
|
@@ -174,7 +179,7 @@ async function cmdRun(ctx, args) {
|
|
|
174
179
|
* the local terminal and a shared buffer file so hcc web can stream it to
|
|
175
180
|
* browsers. Input from the browser is written to a .in file that we relay.
|
|
176
181
|
*/
|
|
177
|
-
async function cmdRunWebManaged(ctx, { id, kind, role, cwd, command, commandArgs, binding, force = false }) {
|
|
182
|
+
async function cmdRunWebManaged(ctx, { id, kind, role, cwd, expectedCwdIdentity, command, commandArgs, binding, force = false }) {
|
|
178
183
|
registerProjectActivity(ctx);
|
|
179
184
|
const ptyModule = await import('node-pty');
|
|
180
185
|
const pty = ptyModule.default || ptyModule;
|
|
@@ -265,9 +270,11 @@ async function cmdRunWebManaged(ctx, { id, kind, role, cwd, command, commandArgs
|
|
|
265
270
|
const cols = process.stdout.columns || 120;
|
|
266
271
|
const rows = process.stdout.rows || 40;
|
|
267
272
|
|
|
268
|
-
const child = pty
|
|
273
|
+
const child = spawnPinnedCwdPty(pty, command, commandArgs, {
|
|
269
274
|
name: 'xterm-256color', cols, rows, cwd,
|
|
270
|
-
|
|
275
|
+
expectedIdentity: expectedCwdIdentity,
|
|
276
|
+
env: childSessionEnv({ HCC_PEER: id, HCC_ROOT: ctx.root, HCC_DB: ctx.dbPath, TERM: 'xterm-256color' },
|
|
277
|
+
process.env, { rootIdentity: ctx.initialRootIdentity })
|
|
271
278
|
});
|
|
272
279
|
const childExit = trackPtyExit(child);
|
|
273
280
|
const wrapperTermination = installPtyTerminationHandlers(child);
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
// hcc scan detects provider sessions; the tmux helpers implement DB-proven,
|
|
3
3
|
// evidence-checked tmux GC (plan/validate/execute) for hcc-managed sessions.
|
|
4
4
|
|
|
5
|
-
import path from 'node:path';
|
|
6
5
|
import process from 'node:process';
|
|
7
6
|
import { spawnSync } from 'node:child_process';
|
|
8
7
|
import { CliError } from '../../shared/errors.mjs';
|
|
@@ -527,7 +526,7 @@ async function planTmuxGc(ctx, opts) {
|
|
|
527
526
|
continue;
|
|
528
527
|
}
|
|
529
528
|
const hccRoot = tmuxSessionEnvironmentValue(actualSession, 'HCC_ROOT');
|
|
530
|
-
if (hccRoot &&
|
|
529
|
+
if (hccRoot && rootEvidence(ctx.root, hccRoot).state !== 'match') {
|
|
531
530
|
skip('hcc_root_mismatch', { hcc_root: hccRoot });
|
|
532
531
|
continue;
|
|
533
532
|
}
|
package/lib/cli-runtime.mjs
CHANGED
|
@@ -2,15 +2,32 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import process from 'node:process';
|
|
4
4
|
import { projectDbPath } from './runtime/paths.mjs';
|
|
5
|
+
import { captureSelectedCwdSnapshot } from './process/selected-cwd-identity.mjs';
|
|
5
6
|
|
|
6
7
|
export function createContext(global = {}, deps = {}) {
|
|
7
8
|
const cwd = deps.cwd || process.cwd();
|
|
8
9
|
const detectRoot = deps.detectRoot;
|
|
9
10
|
if (typeof detectRoot !== 'function') throw new TypeError('createContext requires deps.detectRoot');
|
|
10
|
-
const
|
|
11
|
+
const requestedRoot = detectRoot(cwd, global.root);
|
|
12
|
+
// Freeze an existing root's canonical pathname for this command. A symlink
|
|
13
|
+
// supplied through --root (or HCC_ROOT) may be retargeted after context
|
|
14
|
+
// construction; later state and runtime path derivations must not silently
|
|
15
|
+
// switch projects. Missing roots retain their path for commands that create
|
|
16
|
+
// a project directory.
|
|
17
|
+
let root;
|
|
18
|
+
let initialRootIdentity = null;
|
|
19
|
+
try {
|
|
20
|
+
root = fs.realpathSync.native(requestedRoot);
|
|
21
|
+
initialRootIdentity = captureSelectedCwdSnapshot(root);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
25
|
+
root = requestedRoot;
|
|
26
|
+
}
|
|
11
27
|
const envDb = global.root ? null : process.env.HCC_DB;
|
|
12
28
|
const dbPath = path.resolve(global.db || envDb || projectDbPath(root));
|
|
13
|
-
return { cwd, root, dbPath, json: global.json,
|
|
29
|
+
return { cwd, root, dbPath, json: global.json,
|
|
30
|
+
explicitRoot: Boolean(global.root || process.env.HCC_ROOT), initialRootIdentity };
|
|
14
31
|
}
|
|
15
32
|
|
|
16
33
|
export function shellCommand(args, quoteArg) {
|
|
@@ -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);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import {
|
|
3
|
+
assertSelectedCwdSnapshot,
|
|
4
|
+
captureSelectedCwdSnapshot,
|
|
5
|
+
sameSelectedCwdIdentity
|
|
6
|
+
} from '../../process/selected-cwd-identity.mjs';
|
|
7
|
+
import { CliError } from '../../shared/errors.mjs';
|
|
8
|
+
|
|
9
|
+
export const HOOK_ROOT_IDENTITY_ENV = 'HCC_HOOK_ROOT_IDENTITY';
|
|
10
|
+
|
|
11
|
+
function mismatch() {
|
|
12
|
+
return new CliError('HOOK_ROOT_IDENTITY_MISMATCH',
|
|
13
|
+
'Hook project root no longer matches the provider launch identity');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Carry the launch-time directory identity into short-lived hook children.
|
|
17
|
+
// A pathname alone cannot distinguish a moved project from another directory
|
|
18
|
+
// subsequently installed at the same pathname.
|
|
19
|
+
export function hookRootIdentityValue(root, original = null) {
|
|
20
|
+
const selected = captureSelectedCwdSnapshot(root);
|
|
21
|
+
if (original) {
|
|
22
|
+
if (typeof original.assertUnchanged === 'function') original.assertUnchanged();
|
|
23
|
+
else assertSelectedCwdSnapshot(original);
|
|
24
|
+
if (!sameSelectedCwdIdentity(selected, original)) throw mismatch();
|
|
25
|
+
}
|
|
26
|
+
return JSON.stringify({ version: 1, ...selected });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function assertHookRootIdentity(root, value) {
|
|
30
|
+
if (value === undefined) return false; // Unmanaged and pre-upgrade hooks have no launch marker.
|
|
31
|
+
try {
|
|
32
|
+
if (typeof value !== 'string' || value.length > 4096) throw mismatch();
|
|
33
|
+
const snapshot = JSON.parse(value);
|
|
34
|
+
if (!snapshot || typeof snapshot !== 'object' || Array.isArray(snapshot) ||
|
|
35
|
+
snapshot.version !== 1 || snapshot.requested !== path.resolve(root) ||
|
|
36
|
+
typeof snapshot.canonical !== 'string' || !path.isAbsolute(snapshot.canonical) ||
|
|
37
|
+
!snapshot.identity || typeof snapshot.identity !== 'object' ||
|
|
38
|
+
!/^\d+$/.test(snapshot.identity.dev) || !/^\d+$/.test(snapshot.identity.ino) ||
|
|
39
|
+
(snapshot.identity.birthtimeNs !== null && !/^\d+$/.test(snapshot.identity.birthtimeNs))) {
|
|
40
|
+
throw mismatch();
|
|
41
|
+
}
|
|
42
|
+
assertSelectedCwdSnapshot(snapshot);
|
|
43
|
+
return snapshot;
|
|
44
|
+
} catch {
|
|
45
|
+
throw mismatch();
|
|
46
|
+
}
|
|
47
|
+
}
|