@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.
Files changed (146) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/docs/codex-account.zh-CN.md +38 -0
  3. package/docs/codex-protocol.md +59 -0
  4. package/docs/commands.md +9 -0
  5. package/docs/commands.zh-CN.md +7 -0
  6. package/docs/macos-process-identity.md +2 -0
  7. package/docs/native.md +179 -14
  8. package/docs/native.zh-CN.md +136 -6
  9. package/docs/web-handoff.zh-CN.md +118 -8
  10. package/lib/cli/app.mjs +47 -4
  11. package/lib/cli/commands/coordination.mjs +14 -4
  12. package/lib/cli/commands/dsh.mjs +1 -1
  13. package/lib/cli/commands/gc.mjs +22 -15
  14. package/lib/cli/commands/hook.mjs +19 -3
  15. package/lib/cli/commands/install.mjs +80 -10
  16. package/lib/cli/commands/misc.mjs +36 -9
  17. package/lib/cli/commands/native.mjs +4 -38
  18. package/lib/cli/commands/peer.mjs +3 -0
  19. package/lib/cli/commands/run.mjs +13 -6
  20. package/lib/cli/commands/tmux.mjs +1 -2
  21. package/lib/cli-runtime.mjs +19 -2
  22. package/lib/core/coordination/results.mjs +27 -10
  23. package/lib/core/sessions/hook-root-identity.mjs +47 -0
  24. package/lib/core/sessions/launch.mjs +7 -1
  25. package/lib/db/connection.mjs +140 -42
  26. package/lib/db/schema.mjs +59 -0
  27. package/lib/diagnostics/codex-protocol.mjs +105 -0
  28. package/lib/guidance.mjs +21 -36
  29. package/lib/integrations/codex-account.mjs +143 -0
  30. package/lib/integrations/codex-interactions.mjs +13 -11
  31. package/lib/integrations/dsh-collaboration.mjs +6 -12
  32. package/lib/integrations/dsh-cordis.mjs +17 -13
  33. package/lib/integrations/dsh.mjs +16 -11
  34. package/lib/integrations/mcp-elicitation.mjs +116 -0
  35. package/lib/integrations/mcp-url-elicitation.mjs +25 -0
  36. package/lib/integrations/native/claude.mjs +24 -6
  37. package/lib/integrations/native/codex.mjs +38 -7
  38. package/lib/integrations/native/dsh-acp.mjs +67 -24
  39. package/lib/integrations/native/index.mjs +4 -1
  40. package/lib/integrations/native/interactions.mjs +2 -1
  41. package/lib/integrations/native/jsonrpc.mjs +37 -5
  42. package/lib/integrations/native/telemetry.mjs +30 -0
  43. package/lib/process/identity.mjs +4 -0
  44. package/lib/process/pinned-cwd.mjs +302 -0
  45. package/lib/process/pinned-guidance.mjs +138 -0
  46. package/lib/process/pinned-project-state.mjs +105 -0
  47. package/lib/process/selected-cwd-identity.mjs +95 -0
  48. package/lib/runtime/buffer-evidence.mjs +2 -1
  49. package/lib/runtime/client.mjs +9 -0
  50. package/lib/runtime/native/client.mjs +4 -1
  51. package/lib/runtime/native/history.mjs +140 -0
  52. package/lib/runtime/native/launcher.mjs +106 -0
  53. package/lib/runtime/native/service.mjs +64 -8
  54. package/lib/runtime/native/store.mjs +83 -58
  55. package/lib/runtime/paths.mjs +17 -2
  56. package/lib/runtime/private-file.mjs +76 -0
  57. package/lib/runtime/private-state.mjs +546 -0
  58. package/lib/runtime/project-context.mjs +2 -1
  59. package/lib/runtime/project-path.mjs +92 -20
  60. package/lib/runtime/project-trust.mjs +79 -0
  61. package/lib/runtime/projects.mjs +9 -3
  62. package/lib/runtime/state-migration.mjs +697 -0
  63. package/lib/runtime/state.mjs +69 -27
  64. package/lib/shared/automatic-result-key.mjs +6 -0
  65. package/lib/shared/json-file.mjs +55 -6
  66. package/lib/shared/redact.mjs +2 -1
  67. package/lib/ui/help.mjs +6 -4
  68. package/lib/web/agent-defaults.mjs +110 -0
  69. package/lib/web/auto-attach.mjs +3 -2
  70. package/lib/web/browser/contracts.d.ts +10 -0
  71. package/lib/web/browser/core.mjs +1997 -0
  72. package/lib/web/browser/project-data.mjs +158 -0
  73. package/lib/web/browser/session-sync.mjs +82 -0
  74. package/lib/web/browser-assets.mjs +19 -0
  75. package/lib/web/buffer-gc-runtime.mjs +2 -2
  76. package/lib/web/codex-app-server.mjs +111 -37
  77. package/lib/web/codex-sessions.mjs +61 -12
  78. package/lib/web/context-files.mjs +82 -0
  79. package/lib/web/cookie-auth.mjs +51 -3
  80. package/lib/web/external-sessions.mjs +76 -9
  81. package/lib/web/http-routes.mjs +238 -25
  82. package/lib/web/http.mjs +50 -4
  83. package/lib/web/liveness-reaper.mjs +2 -2
  84. package/lib/web/native-sessions.mjs +323 -14
  85. package/lib/web/project-contexts.mjs +236 -44
  86. package/lib/web/project-file-write.mjs +249 -0
  87. package/lib/web/project-files.mjs +268 -0
  88. package/lib/web/pty-sessions.mjs +11 -3
  89. package/lib/web/runtime-main.mjs +132 -80
  90. package/lib/web/session-serialize.mjs +87 -8
  91. package/lib/web/session-state-sync.mjs +98 -0
  92. package/lib/web/shutdown-diagnostics.mjs +38 -0
  93. package/lib/web/shutdown-response.mjs +21 -0
  94. package/lib/web/shutdown-state.mjs +91 -0
  95. package/lib/web/startup.mjs +103 -35
  96. package/lib/web/task-results.mjs +19 -19
  97. package/lib/web/tls.mjs +84 -33
  98. package/lib/web/tmux-sessions.mjs +73 -9
  99. package/lib/web/tmux-stream.mjs +36 -8
  100. package/lib/web/ui-agent-defaults.mjs +149 -0
  101. package/lib/web/ui-agent-start.mjs +330 -0
  102. package/lib/web/ui-codex-account.mjs +81 -0
  103. package/lib/web/ui-codex.mjs +105 -85
  104. package/lib/web/ui-command-palette.mjs +21 -5
  105. package/lib/web/ui-file-writes.mjs +220 -0
  106. package/lib/web/ui-files.mjs +231 -0
  107. package/lib/web/ui-handoff.mjs +10 -2
  108. package/lib/web/ui-history.mjs +175 -105
  109. package/lib/web/ui-i18n.mjs +280 -8
  110. package/lib/web/ui-interactions.mjs +107 -6
  111. package/lib/web/ui-native-timeline.mjs +326 -0
  112. package/lib/web/ui-native.mjs +93 -43
  113. package/lib/web/ui-review.mjs +5 -3
  114. package/lib/web/ui-safe-markdown.mjs +79 -0
  115. package/lib/web/ui-session-tools.mjs +224 -0
  116. package/lib/web/ui-template.mjs +29 -1968
  117. package/lib/web/ui-terminal-find.mjs +5 -3
  118. package/lib/web/ui-workbench.mjs +5 -3
  119. package/lib/web/ui-workspace.mjs +3 -0
  120. package/package.json +8 -2
  121. package/scripts/benchmark-session-sync.mjs +53 -0
  122. package/scripts/codex-protocol.mjs +88 -0
  123. package/scripts/dsh-approval-layers-acceptance.mjs +172 -0
  124. package/scripts/dsh-package-provenance.mjs +118 -0
  125. package/scripts/mcp-form-live-fixture.mjs +81 -0
  126. package/scripts/native-bounded-live-acceptance.mjs +193 -0
  127. package/scripts/native-fixture-root.mjs +49 -0
  128. package/scripts/native-installed-acceptance.mjs +611 -0
  129. package/scripts/native-stability-checks.mjs +40 -0
  130. package/scripts/regression-cookie-expiry.mjs +70 -0
  131. package/scripts/regression-http-transport.mjs +9 -0
  132. package/scripts/regression-terminal-marker.mjs +95 -0
  133. package/scripts/regression.mjs +481 -219
  134. package/scripts/shim-reentry-probe.mjs +105 -0
  135. package/scripts/web-acceptance-diagnostics.mjs +90 -0
  136. package/scripts/web-codex-account-acceptance.mjs +178 -0
  137. package/scripts/web-file-preview-checks.mjs +302 -0
  138. package/scripts/web-mcp-form-acceptance.mjs +134 -0
  139. package/scripts/web-mcp-form-live-acceptance.mjs +368 -0
  140. package/scripts/web-native-handoff-checks.mjs +128 -0
  141. package/scripts/web-native-interaction-acceptance.mjs +219 -18
  142. package/scripts/web-native-interaction-diagnostics.mjs +80 -0
  143. package/scripts/web-native-model-acceptance.mjs +22 -7
  144. package/scripts/web-session-tools-checks.mjs +112 -0
  145. package/scripts/web-workbench-acceptance.mjs +435 -0
  146. package/scripts/web-workbench-installed-acceptance.mjs +122 -0
@@ -2,6 +2,7 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import process from 'node:process';
4
4
  import { shortHash } from '../peers/format.mjs';
5
+ import { HOOK_ROOT_IDENTITY_ENV, hookRootIdentityValue } from './hook-root-identity.mjs';
5
6
 
6
7
  export const WEB_CHILD_ENV = 'HCC_WEB_CHILD';
7
8
  export const LAUNCH_FINGERPRINT_ENV = 'HCC_LAUNCH_FINGERPRINT';
@@ -11,6 +12,7 @@ const LAUNCH_ENV_IGNORED_KEYS = new Set([
11
12
  '_',
12
13
  'COLUMNS',
13
14
  'HCC_DB',
15
+ HOOK_ROOT_IDENTITY_ENV,
14
16
  'HCC_NO_AUTO_INSTALL_TMUX',
15
17
  'HCC_PEER',
16
18
  'HCC_ROOT',
@@ -41,8 +43,12 @@ const LAUNCH_ENV_IGNORED_KEYS = new Set([
41
43
  'SSH_AUTH_SOCK'
42
44
  ]);
43
45
 
44
- export function childSessionEnv(extra = {}, baseEnv = process.env) {
46
+ export function childSessionEnv(extra = {}, baseEnv = process.env, { rootIdentity = null } = {}) {
45
47
  const env = { ...(baseEnv || {}), ...extra };
48
+ if (Object.hasOwn(extra, 'HCC_ROOT')) {
49
+ if (extra.HCC_ROOT) env[HOOK_ROOT_IDENTITY_ENV] = hookRootIdentityValue(extra.HCC_ROOT, rootIdentity);
50
+ else delete env[HOOK_ROOT_IDENTITY_ENV];
51
+ }
46
52
  delete env[WEB_CHILD_ENV];
47
53
  delete env.HCC_RUNTIME_LOCAL_ONLY;
48
54
  return env;
@@ -10,11 +10,16 @@ import { DatabaseSync } from 'node:sqlite';
10
10
  import { CliError } from '../shared/errors.mjs';
11
11
  import {
12
12
  DB_SCHEMA_VERSION,
13
- execWithBusyRetry
13
+ execWithBusyRetry,
14
+ readSchemaVersion
14
15
  } from './schema.mjs';
15
16
  import { initSchemaWithBackup } from './migration-backup.mjs';
16
17
  import { readProjectRegistry } from '../runtime/projects.mjs';
17
18
  import { resolveProjectDatabase, secureExistingProjectStateDirectory } from '../runtime/project-path.mjs';
19
+ import { projectStateDir, legacyProjectStateDir } from '../runtime/paths.mjs';
20
+ import { privateProjectStateDir, ensurePrivateProjectStateDir } from '../runtime/private-state.mjs';
21
+ import { unsafeDirectoryAcl } from '../runtime/project-trust.mjs';
22
+ import { assertSelectedCwdSnapshot } from '../process/selected-cwd-identity.mjs';
18
23
 
19
24
  const MIGRATION_FAILURE_COOLDOWN_MS = 5 * 60 * 1000;
20
25
 
@@ -25,48 +30,110 @@ function managedDatabase(ctx) {
25
30
  try { canonicalRoot = fs.realpathSync.native(requestedRoot); } catch {}
26
31
  const requestedDb = path.resolve(ctx.dbPath);
27
32
  let canonicalCandidate = requestedDb;
28
- try {
29
- canonicalCandidate = path.join(fs.realpathSync.native(path.dirname(requestedDb)), path.basename(requestedDb));
30
- } catch {}
31
- return [requestedRoot, canonicalRoot].some(root =>
32
- [requestedDb, canonicalCandidate].some(db => {
33
- const relative = path.relative(path.join(root, '.hello-cc'), db);
34
- return relative !== '' && relative !== '..' &&
35
- !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
36
- }));
33
+ let parent = path.dirname(requestedDb);
34
+ const missingParents = [];
35
+ for (;;) {
36
+ try {
37
+ canonicalCandidate = path.join(fs.realpathSync.native(parent),
38
+ ...missingParents, path.basename(requestedDb));
39
+ break;
40
+ } catch (error) {
41
+ if (error?.code !== 'ENOENT' || path.dirname(parent) === parent) break;
42
+ missingParents.unshift(path.basename(parent));
43
+ parent = path.dirname(parent);
44
+ }
45
+ }
46
+ const candidates = new Set([requestedDb, canonicalCandidate]);
47
+ try { candidates.add(fs.realpathSync.native(requestedDb)); } catch {}
48
+ const within = (parent, candidate) => {
49
+ const relative = path.relative(parent, candidate);
50
+ return relative !== '' && relative !== '..' &&
51
+ !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
52
+ };
53
+ const managed = [requestedRoot, canonicalRoot].some(root =>
54
+ [legacyProjectStateDir(root), privateProjectStateDir(root)].some(stateDir =>
55
+ [...candidates].some(db => within(stateDir, db))));
56
+ if (managed) return true;
57
+ // A context may have captured A's local or private .hello-cc DB before its
58
+ // root alias was repointed to B. A reserved state path is never an explicit
59
+ // external DB just because it no longer belongs to the current root.
60
+ const reservedStatePath = db => path.dirname(db).split(path.sep).some(segment =>
61
+ segment.toLowerCase() === '.hello-cc');
62
+ if ([...candidates].some(reservedStatePath)) {
63
+ throw new CliError('PROJECT_PATH_FORBIDDEN',
64
+ `Private database path is not bound to the current project root: ${requestedDb}`);
65
+ }
66
+ return false;
37
67
  }
38
68
 
39
- function secureManagedDatabaseFile(dbPath, create) {
69
+ function secureOwnedSqliteFile(file, { create = false, readOnly = false, optional = false } = {}) {
70
+ const changed = () => new CliError('PROJECT_PATH_FORBIDDEN', `SQLite file changed during validation: ${file}`);
71
+ const safeStat = stat => stat && !stat.isSymbolicLink() && stat.isFile() && stat.nlink === 1 &&
72
+ (typeof process.getuid !== 'function' || stat.uid === process.getuid()) &&
73
+ (process.platform === 'win32' || (stat.mode & 0o022) === 0) &&
74
+ !unsafeDirectoryAcl(file, stat);
75
+ const stillSame = (current, original) => safeStat(current) &&
76
+ current.dev === original.dev && current.ino === original.ino;
77
+ const recheck = () => {
78
+ try { return fs.lstatSync(file); }
79
+ catch (error) {
80
+ if (!optional || error?.code !== 'ENOENT') throw error;
81
+ // An optional WAL/SHM/journal may disappear as SQLite closes its last
82
+ // connection. Do not accept a different file rebuilt at the same path.
83
+ try { fs.lstatSync(file); }
84
+ catch (again) {
85
+ if (again?.code === 'ENOENT') return null;
86
+ throw again;
87
+ }
88
+ throw changed();
89
+ }
90
+ };
40
91
  let stat;
41
- try { stat = fs.lstatSync(dbPath); }
92
+ try { stat = fs.lstatSync(file); }
42
93
  catch (error) {
43
94
  if (error?.code !== 'ENOENT') throw error;
44
- if (!create) throw new CliError('NOT_FOUND', `Project database does not exist: ${dbPath}`);
95
+ if (!create) return false;
45
96
  try {
46
- const fd = fs.openSync(dbPath, fs.constants.O_RDWR | fs.constants.O_CREAT |
97
+ const fd = fs.openSync(file, fs.constants.O_RDWR | fs.constants.O_CREAT |
47
98
  fs.constants.O_EXCL | (fs.constants.O_NOFOLLOW || 0), 0o600);
48
99
  fs.closeSync(fd);
49
100
  } catch (creationError) {
50
101
  if (creationError?.code !== 'EEXIST') throw creationError;
51
102
  }
52
- stat = fs.lstatSync(dbPath);
103
+ stat = fs.lstatSync(file);
53
104
  }
54
- if (stat.isSymbolicLink() || !stat.isFile() || stat.nlink !== 1 ||
55
- (typeof process.getuid === 'function' && stat.uid !== process.getuid())) {
56
- throw new CliError('PROJECT_PATH_FORBIDDEN', `Project database must be an owned regular file: ${dbPath}`);
105
+ if (!safeStat(stat)) {
106
+ // ACL inspection invokes ls and may outlive a retiring optional sidecar.
107
+ // Missing is safe to skip; a rebuilt or otherwise unsafe file is not.
108
+ if (optional && !recheck()) return false;
109
+ throw new CliError('PROJECT_PATH_FORBIDDEN', `SQLite file must be an owned non-writable regular file: ${file}`);
57
110
  }
58
- const fd = fs.openSync(dbPath, fs.constants.O_RDWR | (fs.constants.O_NOFOLLOW || 0));
59
- try {
60
- const opened = fs.fstatSync(fd);
61
- if (!opened.isFile() || opened.dev !== stat.dev || opened.ino !== stat.ino) {
62
- throw new CliError('PROJECT_PATH_FORBIDDEN', `Project database changed during validation: ${dbPath}`);
63
- }
64
- if (process.platform !== 'win32' && (opened.mode & 0o777) !== 0o600) fs.fchmodSync(fd, 0o600);
65
- const current = fs.lstatSync(dbPath);
66
- if (current.isSymbolicLink() || current.dev !== stat.dev || current.ino !== stat.ino) {
67
- throw new CliError('PROJECT_PATH_FORBIDDEN', `Project database changed during validation: ${dbPath}`);
111
+ // Never open/close an existing SQLite file outside SQLite: on POSIX, close()
112
+ // can release locks held by another SQLite connection in this process.
113
+ const current = recheck();
114
+ if (!current) return false;
115
+ if (!stillSame(current, stat)) throw changed();
116
+ if (!readOnly && process.platform !== 'win32' && (current.mode & 0o777) !== 0o600) {
117
+ try { fs.chmodSync(file, 0o600); }
118
+ catch (error) {
119
+ if (!optional || error?.code !== 'ENOENT') throw error;
120
+ if (!recheck()) return false;
121
+ throw changed();
68
122
  }
69
- } finally { fs.closeSync(fd); }
123
+ const tightened = recheck();
124
+ if (!tightened) return false;
125
+ if (!stillSame(tightened, stat) || (tightened.mode & 0o777) !== 0o600) throw changed();
126
+ }
127
+ return true;
128
+ }
129
+
130
+ function secureManagedDatabaseFile(dbPath, create, readOnly = false) {
131
+ if (!secureOwnedSqliteFile(dbPath, { create, readOnly })) {
132
+ throw new CliError('NOT_FOUND', `Project database does not exist: ${dbPath}`);
133
+ }
134
+ for (const suffix of ['-wal', '-shm', '-journal']) {
135
+ secureOwnedSqliteFile(`${dbPath}${suffix}`, { readOnly, optional: true });
136
+ }
70
137
  }
71
138
 
72
139
  export function createConnectionHelpers({ now, dedupePeerBindings, redactedLogText }) {
@@ -74,11 +141,25 @@ export function createConnectionHelpers({ now, dedupePeerBindings, redactedLogTe
74
141
  const migratedRegisteredProjectDbs = new Set();
75
142
 
76
143
  function connect(ctx, options = {}) {
144
+ const existingSchema = options.existingSchema === true;
145
+ if (existingSchema && options.create !== false) {
146
+ throw new TypeError('existingSchema requires create: false');
147
+ }
148
+ if (ctx.initialRootIdentity) assertSelectedCwdSnapshot(ctx.initialRootIdentity);
77
149
  let dbPath = ctx.dbPath;
78
150
  if (managedDatabase(ctx)) {
151
+ const rootWasMissing = options.create !== false && !fs.existsSync(ctx.root);
152
+ const pendingDefault = rootWasMissing ? path.join(privateProjectStateDir(ctx.root), 'mesh.db') : null;
79
153
  if (options.create !== false && !fs.existsSync(ctx.root)) {
80
154
  fs.mkdirSync(ctx.root, { recursive: true });
81
155
  }
156
+ if (rootWasMissing && path.resolve(dbPath) === pendingDefault) {
157
+ dbPath = path.join(projectStateDir(ctx.root), 'mesh.db');
158
+ }
159
+ if (rootWasMissing && path.resolve(ctx.dbPath) === pendingDefault &&
160
+ projectStateDir(ctx.root) === privateProjectStateDir(ctx.root)) {
161
+ ensurePrivateProjectStateDir(ctx.root, { create: true });
162
+ }
82
163
  const resolved = resolveProjectDatabase({
83
164
  root: ctx.root,
84
165
  db: dbPath,
@@ -94,26 +175,43 @@ export function createConnectionHelpers({ now, dedupePeerBindings, redactedLogTe
94
175
  }
95
176
  if (options.create !== false) fs.mkdirSync(path.dirname(dbPath), { recursive: true });
96
177
  }
97
- const db = new DatabaseSync(dbPath, { timeout: 5000 });
98
- db.exec('PRAGMA busy_timeout = 5000;');
99
- execWithBusyRetry(db, 'PRAGMA journal_mode = WAL;', { ignoreBusy: true });
100
- db.exec('PRAGMA synchronous = NORMAL;');
101
- db.exec('PRAGMA wal_autocheckpoint = 1000;');
102
- db.exec('PRAGMA foreign_keys = ON;');
103
- initSchemaWithBackup(db, dbPath, {
104
- beforePostMigrationIndexes: dedupePeerBindings
105
- });
106
- if (options.migrateRegistered !== false) migrateRegisteredProjectDbs(ctx);
107
- return db;
178
+ const db = new DatabaseSync(dbPath, { timeout: existingSchema ? 0 : 5000 });
179
+ try {
180
+ if (existingSchema) {
181
+ // Shutdown may abandon advisory detachment when another writer owns
182
+ // the DB. Never migrate, change journal mode, or retry while exiting.
183
+ db.exec('PRAGMA busy_timeout = 0; PRAGMA foreign_keys = ON;');
184
+ if (readSchemaVersion(db) !== DB_SCHEMA_VERSION) {
185
+ throw new CliError('DB_SCHEMA_NOT_CURRENT', 'Shutdown requires an existing current database schema');
186
+ }
187
+ return db;
188
+ }
189
+ db.exec('PRAGMA busy_timeout = 5000;');
190
+ execWithBusyRetry(db, 'PRAGMA journal_mode = WAL;', { ignoreBusy: true });
191
+ db.exec('PRAGMA synchronous = NORMAL;');
192
+ db.exec('PRAGMA wal_autocheckpoint = 1000;');
193
+ db.exec('PRAGMA foreign_keys = ON;');
194
+ initSchemaWithBackup(db, dbPath, {
195
+ beforePostMigrationIndexes: dedupePeerBindings
196
+ });
197
+ if (options.migrateRegistered !== false) migrateRegisteredProjectDbs(ctx);
198
+ return db;
199
+ } catch (error) {
200
+ try { db.close(); } catch {}
201
+ throw error;
202
+ }
108
203
  }
109
204
 
110
205
  function connectReadOnly(ctx) {
111
- const dbPath = managedDatabase(ctx)
206
+ if (ctx.initialRootIdentity) assertSelectedCwdSnapshot(ctx.initialRootIdentity);
207
+ const managed = managedDatabase(ctx);
208
+ const dbPath = managed
112
209
  ? resolveProjectDatabase({ root: ctx.root, db: ctx.dbPath, createStateDir: false }).db
113
210
  : ctx.dbPath;
114
211
  if (!fs.existsSync(dbPath) || !fs.statSync(dbPath).isFile()) {
115
212
  throw new CliError('NOT_FOUND', `Project database does not exist: ${dbPath}`);
116
213
  }
214
+ if (managed) secureManagedDatabaseFile(dbPath, false, true);
117
215
  return new DatabaseSync(dbPath, { timeout: 5000, readOnly: true });
118
216
  }
119
217
 
@@ -128,7 +226,7 @@ export function createConnectionHelpers({ now, dedupePeerBindings, redactedLogTe
128
226
  try {
129
227
  resolved = resolveProjectDatabase({
130
228
  root: project.root,
131
- db: project.db || path.join(project.root, '.hello-cc', 'mesh.db'),
229
+ db: project.db || path.join(projectStateDir(project.root), 'mesh.db'),
132
230
  createStateDir: false
133
231
  });
134
232
  } catch (err) {
package/lib/db/schema.mjs CHANGED
@@ -1,7 +1,9 @@
1
1
  import { aggregateCleanupFailure } from '../shared/cleanup-error.mjs';
2
+ import { automaticResultKey } from '../shared/automatic-result-key.mjs';
2
3
  import { CliError } from '../shared/errors.mjs';
3
4
 
4
5
  export const DB_SCHEMA_VERSION = 7;
6
+ const AUTOMATIC_RESULT_SCRUB_KEY = 'automatic_result_content_scrub_v1';
5
7
 
6
8
  const sleepBuffer = new SharedArrayBuffer(4);
7
9
  const sleepView = new Int32Array(sleepBuffer);
@@ -125,6 +127,63 @@ export function initSchema(db, {
125
127
  runSchemaMigrations(db);
126
128
  if (typeof beforePostMigrationIndexes === 'function') beforePostMigrationIndexes(db);
127
129
  createPostMigrationIndexes(db);
130
+ scrubAutomaticTaskResults(db);
131
+ }
132
+
133
+ function scrubAutomaticTaskResults(db) {
134
+ const marker = db.prepare('SELECT 1 FROM meta WHERE key = ?');
135
+ if (marker.get(AUTOMATIC_RESULT_SCRUB_KEY)) return;
136
+ tx(db, () => {
137
+ if (marker.get(AUTOMATIC_RESULT_SCRUB_KEY)) return;
138
+ const read = db.prepare("SELECT id,payload,created_at FROM events WHERE type = 'task.result.recorded' AND id > ? ORDER BY id LIMIT 100");
139
+ const update = db.prepare('UPDATE events SET payload = ? WHERE id = ?');
140
+ let lastId = 0;
141
+ while (true) {
142
+ const rows = read.all(lastId);
143
+ if (!rows.length) break;
144
+ for (const row of rows) {
145
+ let record;
146
+ try { record = JSON.parse(row.payload); } catch { continue; }
147
+ if (record?.source !== 'executor' || record.stage !== 'local' ||
148
+ !['command', 'diff'].includes(record.kind) ||
149
+ typeof record.executor_id !== 'string' || !record.executor_id ||
150
+ typeof record.thread_id !== 'string' || !record.thread_id ||
151
+ typeof record.turn_id !== 'string' || !record.turn_id ||
152
+ typeof record.result_key !== 'string') continue;
153
+ // Legacy Web observer keys identify these records more precisely than
154
+ // source/kind/turn metadata, which an explicit result may also carry.
155
+ const commandKey = `${record.executor_id}:${record.turn_id}:${record.item_id}:command`;
156
+ const diffPrefix = `${record.executor_id}:${record.turn_id}:diff:`;
157
+ const legacyCommand = record.kind === 'command' && typeof record.item_id === 'string' &&
158
+ record.item_id && record.result_key === commandKey;
159
+ const legacyDiff = record.kind === 'diff' && record.title === 'Turn changes awaiting review' &&
160
+ record.result_key.startsWith(diffPrefix) &&
161
+ /^[a-f0-9]{64}$/.test(record.result_key.slice(diffPrefix.length));
162
+ if (!legacyCommand && !legacyDiff) continue;
163
+ const executorId = typeof record.executor_id === 'string' ? record.executor_id : null;
164
+ const turnId = typeof record.turn_id === 'string' ? record.turn_id : null;
165
+ const itemId = typeof record.item_id === 'string' ? record.item_id : null;
166
+ // Distinct old diff hashes may become the same structural key. Keep the
167
+ // historical rows; result-key lookup already chooses the latest ID.
168
+ const resultKey = automaticResultKey({ executorId,
169
+ threadId: typeof record.thread_id === 'string' ? record.thread_id : null,
170
+ turnId, kind: record.kind, itemId: record.kind === 'command' ? itemId : null });
171
+ update.run(JSON.stringify({
172
+ result_id: record.result_id, result_key: resultKey,
173
+ peer: record.peer, task_id: record.task_id, stage: 'local', status: record.status,
174
+ kind: record.kind, source: 'executor',
175
+ title: record.kind === 'command' ? 'Command execution' : 'Turn changes awaiting review',
176
+ details: '', evidence: [], command: '', diff: '',
177
+ executor_id: executorId, thread_id: record.thread_id, turn_id: turnId,
178
+ item_id: record.kind === 'command' ? itemId : null,
179
+ exit_code: Number.isSafeInteger(record.exit_code) ? record.exit_code : null,
180
+ created_at: row.created_at
181
+ }), row.id);
182
+ }
183
+ lastId = rows.at(-1).id;
184
+ }
185
+ db.prepare('INSERT INTO meta(key,value) VALUES (?,?)').run(AUTOMATIC_RESULT_SCRUB_KEY, '1');
186
+ });
128
187
  }
129
188
 
130
189
  function createBaseSchema(db) {
@@ -0,0 +1,105 @@
1
+ import { CODEX_INTERACTION_METHODS } from '../integrations/codex-interactions.mjs';
2
+
3
+ // A bounded upgrade gate, not a JSON Schema validator or a runtime permission
4
+ // check. Generated types remain the authoritative version-specific artifacts.
5
+ export const CODEX_CLIENT_METHODS = Object.freeze([
6
+ 'initialize', 'thread/start', 'thread/resume', 'thread/read', 'thread/list',
7
+ 'thread/fork', 'turn/start', 'turn/steer', 'turn/interrupt',
8
+ 'mcpServerStatus/list', 'mcpServer/tool/call', 'account/read', 'account/rateLimits/read'
9
+ ]);
10
+ export const CODEX_NOTIFICATION_METHODS = Object.freeze([
11
+ 'thread/started', 'thread/status/changed', 'turn/started', 'turn/completed',
12
+ 'turn/plan/updated', 'turn/diff/updated', 'item/started', 'item/completed',
13
+ 'item/agentMessage/delta', 'serverRequest/resolved', 'error',
14
+ 'account/updated', 'account/login/completed', 'account/rateLimits/updated'
15
+ ]);
16
+ const ACCOUNT_SCHEMA_FILES = ['GetAccountParams', 'GetAccountResponse', 'GetAccountRateLimitsResponse',
17
+ 'AccountUpdatedNotification', 'AccountLoginCompletedNotification', 'AccountRateLimitsUpdatedNotification'];
18
+ export const CODEX_PROTOCOL_SCHEMA_FILES = Object.freeze([
19
+ 'ClientRequest', 'ClientNotification', 'ServerRequest', 'ServerNotification',
20
+ 'CommandExecutionRequestApprovalResponse', 'FileChangeRequestApprovalResponse',
21
+ 'PermissionsRequestApprovalResponse', 'ToolRequestUserInputResponse',
22
+ 'McpServerElicitationRequestParams', 'McpServerElicitationRequestResponse',
23
+ ...ACCOUNT_SCHEMA_FILES
24
+ ]);
25
+ // Official account schemas are generated only in the versioned v2 directory.
26
+ export const CODEX_PROTOCOL_SCHEMA_PATHS = Object.freeze(Object.fromEntries(CODEX_PROTOCOL_SCHEMA_FILES
27
+ .map(name => [name, (ACCOUNT_SCHEMA_FILES.includes(name) ? 'v2/' : '') + name + '.json'])));
28
+
29
+ function methods(schema) {
30
+ return (schema?.oneOf || []).flatMap(row => row.properties?.method?.enum || []);
31
+ }
32
+ function enumValues(schema, document, seen = new Set()) {
33
+ if (!schema || typeof schema !== 'object') return [];
34
+ if (schema.$ref) {
35
+ const ref = schema.$ref;
36
+ if (seen.has(ref) || !ref.startsWith('#/definitions/')) return [];
37
+ const next = new Set(seen); next.add(ref);
38
+ return enumValues(document.definitions?.[ref.slice('#/definitions/'.length)], document, next);
39
+ }
40
+ return [...(schema.enum || []), ...(Object.hasOwn(schema, 'const') ? [schema.const] : []),
41
+ ...['oneOf', 'anyOf', 'allOf'].flatMap(key => (schema[key] || []).flatMap(row => enumValues(row, document, seen)))];
42
+ }
43
+
44
+ export function inspectCodexProtocol(schemas) {
45
+ const checks = [];
46
+ const check = (contract, ok) => checks.push({ contract, status: ok ? 'pass' : 'fail' });
47
+ for (const [file, expected] of [
48
+ ['ClientRequest', CODEX_CLIENT_METHODS], ['ClientNotification', ['initialized']],
49
+ ['ServerRequest', [...CODEX_INTERACTION_METHODS]], ['ServerNotification', CODEX_NOTIFICATION_METHODS]
50
+ ]) {
51
+ const available = methods(schemas[file]);
52
+ for (const method of expected) check(`${file}: ${method}`, available.includes(method));
53
+ }
54
+ for (const [file, fields] of [
55
+ ['CommandExecutionRequestApprovalResponse', ['decision']],
56
+ ['FileChangeRequestApprovalResponse', ['decision']],
57
+ ['PermissionsRequestApprovalResponse', ['permissions', 'scope']],
58
+ ['ToolRequestUserInputResponse', ['answers']],
59
+ ['McpServerElicitationRequestResponse', ['action', 'content']]
60
+ ]) {
61
+ const schema = schemas[file];
62
+ check(`${file}: response fields`, fields.every(field => Object.hasOwn(schema?.properties || {}, field)));
63
+ // HCC never supplies unknown required response fields. Optional extensions
64
+ // are compatible with this gate; new required fields demand adapter review.
65
+ check(`${file}: no new required response fields`, Boolean(schema) && (schema.required || []).every(field => fields.includes(field)));
66
+ }
67
+ for (const [file, field, expected] of [
68
+ ['CommandExecutionRequestApprovalResponse', 'decision', ['accept', 'decline', 'cancel']],
69
+ ['FileChangeRequestApprovalResponse', 'decision', ['accept', 'decline', 'cancel']],
70
+ ['PermissionsRequestApprovalResponse', 'scope', ['turn', 'session']],
71
+ ['McpServerElicitationRequestResponse', 'action', ['accept', 'decline', 'cancel']]
72
+ ]) {
73
+ const values = enumValues(schemas[file]?.properties?.[field], schemas[file] || {});
74
+ check(`${file}: ${field} choices`, expected.every(value => values.includes(value)));
75
+ }
76
+ const elicitation = schemas.McpServerElicitationRequestParams;
77
+ check('MCP elicitation: thread and server identity',
78
+ ['threadId', 'serverName'].every(field => elicitation?.required?.includes(field)) &&
79
+ Object.hasOwn(elicitation?.properties || {}, 'turnId'));
80
+ for (const [mode, fields] of [['form', ['message', 'mode', 'requestedSchema']], ['url', ['message', 'mode', 'elicitationId', 'url']]]) {
81
+ check(`MCP elicitation: ${mode} identity and payload`, Boolean(elicitation?.oneOf?.some(row =>
82
+ row.properties?.mode?.enum?.includes(mode) && fields.every(field => row.required?.includes(field) && Object.hasOwn(row.properties || {}, field)))));
83
+ }
84
+ check('Account read: explicit nonrefreshing option', schemas.GetAccountParams?.properties?.refreshToken?.type === 'boolean' &&
85
+ (schemas.GetAccountParams?.required || []).every(field => field === 'refreshToken'));
86
+ check('Account read: auth requirement is explicit', schemas.GetAccountResponse?.required?.includes('requiresOpenaiAuth') &&
87
+ schemas.GetAccountResponse?.properties?.requiresOpenaiAuth?.type === 'boolean');
88
+ check('Account read: recognized account types', ['apiKey', 'chatgpt', 'amazonBedrock'].every(type =>
89
+ schemas.GetAccountResponse?.definitions?.Account?.oneOf?.some(row => row.properties?.type?.enum?.includes(type))));
90
+ check('Account limits: historical and multi-bucket fields', ['rateLimits', 'rateLimitsByLimitId'].every(field =>
91
+ Object.hasOwn(schemas.GetAccountRateLimitsResponse?.properties || {}, field)));
92
+ check('Account limits: window percentage', schemas.GetAccountRateLimitsResponse?.definitions?.RateLimitWindow?.required?.includes('usedPercent') &&
93
+ ['integer', 'number'].includes(schemas.GetAccountRateLimitsResponse?.definitions?.RateLimitWindow?.properties?.usedPercent?.type));
94
+ check('Account notifications: status and quota fields', Object.hasOwn(schemas.AccountUpdatedNotification?.properties || {}, 'authMode') &&
95
+ Object.hasOwn(schemas.AccountUpdatedNotification?.properties || {}, 'planType') && schemas.AccountRateLimitsUpdatedNotification?.required?.includes('rateLimits'));
96
+ check('Account notification: correlated login completion', schemas.AccountLoginCompletedNotification?.required?.includes('success') &&
97
+ Object.hasOwn(schemas.AccountLoginCompletedNotification?.properties || {}, 'loginId'));
98
+ const unsupportedServerRequests = methods(schemas.ServerRequest).filter(method => !CODEX_INTERACTION_METHODS.has(method)).sort();
99
+ return {
100
+ status: checks.every(entry => entry.status === 'pass') ? 'compatible' : 'incompatible',
101
+ checks, unsupportedServerRequests,
102
+ scope: 'Method presence, response fields and choices, MCP form/URL identity, and read-only account/quota fields; not full schema or runtime validation.',
103
+ inferenceCalled: false, accountAccessed: false
104
+ };
105
+ }
package/lib/guidance.mjs CHANGED
@@ -1,5 +1,7 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
1
+ import { resolveProjectDatabase } from './runtime/project-path.mjs';
2
+ import { writeProjectGuidanceFiles, removeProjectGuidanceFiles } from './process/pinned-guidance.mjs';
3
+ import { writePinnedProjectGuidance } from './process/pinned-project-state.mjs';
4
+ import { captureSelectedCwdIdentity } from './process/selected-cwd-identity.mjs';
3
5
 
4
6
  export function guidanceMarkdown() {
5
7
  return `# hello-cc Coordination Rules
@@ -83,41 +85,24 @@ Before stopping:
83
85
  `;
84
86
  }
85
87
 
86
- export function writeGuidance(root) {
87
- const guidePath = path.join(root, '.hello-cc', 'HCC.md');
88
- fs.mkdirSync(path.dirname(guidePath), { recursive: true });
89
- const content = guidanceMarkdown();
90
- fs.writeFileSync(guidePath, content);
91
- const clause = `\n<!-- hello-cc:start -->\n\n${content}\n<!-- hello-cc:end -->\n`;
92
- for (const file of ['CLAUDE.md', 'AGENTS.md']) {
93
- const target = path.join(root, file);
94
- if (!fs.existsSync(target)) {
95
- fs.writeFileSync(target, clause.trimStart());
96
- continue;
97
- }
98
- const existing = fs.readFileSync(target, 'utf8');
99
- const updated = existing.includes('<!-- hello-cc:start -->')
100
- ? existing.replace(/<!-- hello-cc:start -->[\s\S]*?<!-- hello-cc:end -->/, clause.trim())
101
- : `${existing.trimEnd()}\n${clause}`;
102
- fs.writeFileSync(target, updated);
88
+ export function writeGuidance(root, options = {}) {
89
+ const selectedRoot = captureSelectedCwdIdentity(root, options.expectedSnapshot);
90
+ let selectedState;
91
+ try {
92
+ const stateDir = resolveProjectDatabase({ root, createStateDir: true }).stateDir;
93
+ selectedState = captureSelectedCwdIdentity(stateDir);
94
+ selectedRoot.assertUnchanged();
95
+ const content = guidanceMarkdown();
96
+ writeProjectGuidanceFiles(root, content, { ...options, expectedBinding: selectedRoot });
97
+ selectedRoot.assertUnchanged();
98
+ return writePinnedProjectGuidance(stateDir, content, selectedState,
99
+ { spawnProcess: options.stateSpawnProcess });
100
+ } finally {
101
+ selectedState?.release();
102
+ selectedRoot.release();
103
103
  }
104
- return guidePath;
105
104
  }
106
105
 
107
- export function removeGuidanceBlocks(root) {
108
- const changed = [];
109
- for (const file of ['CLAUDE.md', 'AGENTS.md']) {
110
- const target = path.join(root, file);
111
- if (!fs.existsSync(target)) continue;
112
- const existing = fs.readFileSync(target, 'utf8');
113
- const updated = existing
114
- .replace(/\n?<!-- hello-cc:start -->[\s\S]*?<!-- hello-cc:end -->\n?/g, '\n')
115
- .replace(/\n{3,}/g, '\n\n')
116
- .trimEnd();
117
- if (updated !== existing) {
118
- fs.writeFileSync(target, updated ? `${updated}\n` : '');
119
- changed.push(target);
120
- }
121
- }
122
- return changed;
106
+ export function removeGuidanceBlocks(root, options = {}) {
107
+ return removeProjectGuidanceFiles(root, options);
123
108
  }