@logicseek/hello-cc 1.0.0 → 1.0.2-dsh.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 (123) hide show
  1. package/CHANGELOG.md +149 -0
  2. package/README.md +36 -0
  3. package/README.zh-CN.md +35 -0
  4. package/docs/README.md +9 -0
  5. package/docs/README.zh-CN.md +9 -0
  6. package/docs/commands.md +53 -0
  7. package/docs/commands.zh-CN.md +48 -0
  8. package/docs/dsh.md +137 -0
  9. package/docs/dsh.zh-CN.md +139 -0
  10. package/docs/guide.md +58 -0
  11. package/docs/guide.zh-CN.md +55 -0
  12. package/docs/mcp.md +9 -0
  13. package/docs/mcp.zh-CN.md +23 -0
  14. package/docs/native.md +295 -0
  15. package/docs/native.zh-CN.md +253 -0
  16. package/docs/web-handoff.zh-CN.md +53 -0
  17. package/lib/cli/app.mjs +27 -6
  18. package/lib/cli/commands/coordination.mjs +3 -1
  19. package/lib/cli/commands/doctor.mjs +17 -2
  20. package/lib/cli/commands/dsh.mjs +79 -0
  21. package/lib/cli/commands/gc.mjs +182 -32
  22. package/lib/cli/commands/hook.mjs +241 -90
  23. package/lib/cli/commands/lock.mjs +34 -19
  24. package/lib/cli/commands/mcp.mjs +28 -0
  25. package/lib/cli/commands/misc.mjs +15 -0
  26. package/lib/cli/commands/native.mjs +130 -0
  27. package/lib/cli/commands/peer.mjs +21 -5
  28. package/lib/cli/commands/run.mjs +66 -21
  29. package/lib/cli/commands/task.mjs +6 -3
  30. package/lib/cli-args.mjs +4 -0
  31. package/lib/coordination-state.mjs +15 -1
  32. package/lib/core/coordination/event-retention.mjs +14 -2
  33. package/lib/core/coordination/gc-plan.mjs +3 -13
  34. package/lib/core/coordination/locks.mjs +45 -0
  35. package/lib/core/coordination/results.mjs +79 -0
  36. package/lib/core/peers/bindings.mjs +9 -3
  37. package/lib/diagnostics/codex.mjs +121 -0
  38. package/lib/integrations/codex-interactions.mjs +114 -0
  39. package/lib/integrations/dsh-collaboration.mjs +170 -0
  40. package/lib/integrations/dsh-cordis.d.ts +19 -0
  41. package/lib/integrations/dsh-cordis.mjs +177 -0
  42. package/lib/integrations/dsh.bundle.yml +7 -0
  43. package/lib/integrations/dsh.mjs +287 -0
  44. package/lib/integrations/native/claude.mjs +451 -0
  45. package/lib/integrations/native/codex.mjs +242 -0
  46. package/lib/integrations/native/dsh-acp.mjs +343 -0
  47. package/lib/integrations/native/index.mjs +19 -0
  48. package/lib/integrations/native/interactions.mjs +53 -0
  49. package/lib/integrations/native/jsonrpc.mjs +185 -0
  50. package/lib/integrations/peers/identity.mjs +36 -10
  51. package/lib/integrations/peers/processes.mjs +22 -8
  52. package/lib/integrations/providers.mjs +6 -2
  53. package/lib/mcp/scope.mjs +104 -0
  54. package/lib/mcp/stdio.mjs +80 -0
  55. package/lib/mcp/tools.mjs +197 -0
  56. package/lib/process/identity.mjs +33 -1
  57. package/lib/release/release-notes.mjs +9 -0
  58. package/lib/runtime/buffer-directory-lease.mjs +2 -1
  59. package/lib/runtime/client.mjs +3 -0
  60. package/lib/runtime/native/client.mjs +38 -0
  61. package/lib/runtime/native/service.mjs +511 -0
  62. package/lib/runtime/native/store.mjs +121 -0
  63. package/lib/shared/file-lock.mjs +11 -4
  64. package/lib/shared/socket-lock-worker.mjs +73 -24
  65. package/lib/terminal/tmux.mjs +53 -23
  66. package/lib/ui/help.mjs +5 -2
  67. package/lib/web/adoption-state.mjs +55 -0
  68. package/lib/web/auto-attach.mjs +18 -2
  69. package/lib/web/buffer-gc-runtime.mjs +2 -2
  70. package/lib/web/codex-app-server.mjs +557 -0
  71. package/lib/web/codex-sessions.mjs +287 -0
  72. package/lib/web/control-lease.mjs +204 -0
  73. package/lib/web/http-routes.mjs +197 -15
  74. package/lib/web/http.mjs +5 -4
  75. package/lib/web/native-sessions.mjs +286 -0
  76. package/lib/web/project-contexts.mjs +8 -2
  77. package/lib/web/pty-sessions.mjs +60 -33
  78. package/lib/web/runtime-main.mjs +101 -6
  79. package/lib/web/session-serialize.mjs +0 -0
  80. package/lib/web/startup.mjs +4 -2
  81. package/lib/web/task-results.mjs +76 -0
  82. package/lib/web/tmux-sessions.mjs +92 -11
  83. package/lib/web/tmux-stream.mjs +2 -2
  84. package/lib/web/ui-codex.mjs +550 -0
  85. package/lib/web/ui-command-palette.mjs +119 -0
  86. package/lib/web/ui-handoff.mjs +102 -0
  87. package/lib/web/ui-history.mjs +118 -0
  88. package/lib/web/ui-i18n.mjs +841 -0
  89. package/lib/web/ui-interactions.mjs +78 -0
  90. package/lib/web/ui-login.mjs +101 -0
  91. package/lib/web/ui-native.mjs +165 -0
  92. package/lib/web/ui-preferences.mjs +278 -0
  93. package/lib/web/ui-review.mjs +132 -0
  94. package/lib/web/ui-template.mjs +1106 -576
  95. package/lib/web/ui-terminal-find.mjs +98 -0
  96. package/lib/web/ui-workbench.mjs +94 -0
  97. package/lib/web/ui-workspace.mjs +222 -0
  98. package/package.json +12 -4
  99. package/scripts/check-free-identifiers.mjs +67 -8
  100. package/scripts/codex-diagnostics-installed-acceptance.mjs +72 -0
  101. package/scripts/dsh-cordis-acceptance.mjs +200 -0
  102. package/scripts/dsh-installed-acceptance.mjs +247 -0
  103. package/scripts/dsh-official-acceptance.mjs +180 -0
  104. package/scripts/dsh-web-startup-acceptance.mjs +87 -0
  105. package/scripts/github-release.mjs +3 -1
  106. package/scripts/native-live-acceptance.mjs +369 -0
  107. package/scripts/regression.mjs +338 -187
  108. package/scripts/web-handoff-installed-acceptance.mjs +98 -0
  109. package/scripts/web-native-interaction-acceptance.mjs +204 -0
  110. package/scripts/web-native-model-acceptance.mjs +138 -0
  111. package/docs/architecture-split-review.md +0 -217
  112. package/docs/current-state-review-2026-08.md +0 -341
  113. package/docs/defect-review-2026-07.md +0 -382
  114. package/docs/layout-split-stack-audit.md +0 -298
  115. package/docs/superpowers/plans/2026-08-03-runtime-safety-plan.md +0 -294
  116. package/docs/superpowers/plans/2026-08-03-v1-data-identity-plan.md +0 -400
  117. package/docs/superpowers/plans/2026-08-03-v1-release-verification-plan.md +0 -284
  118. package/docs/superpowers/plans/2026-08-03-web-api-hardening-plan.md +0 -303
  119. package/docs/superpowers/plans/2026-08-04-runtime-buffer-gc-two-phase.md +0 -71
  120. package/docs/superpowers/specs/2026-08-03-runtime-safety-design.md +0 -117
  121. package/docs/superpowers/specs/2026-08-03-v1-data-identity-design.md +0 -120
  122. package/docs/superpowers/specs/2026-08-03-v1-release-verification-design.md +0 -117
  123. package/docs/superpowers/specs/2026-08-03-web-api-hardening-design.md +0 -104
@@ -0,0 +1,197 @@
1
+ import { CliError } from '../shared/errors.mjs';
2
+ import { redactSecrets } from '../shared/redact.mjs';
3
+ import { parseOpts, intOpt, required, positiveSafeIntOpt, wantsHelp } from '../cli-args.mjs';
4
+ import { positiveIntOpt } from '../task-cli.mjs';
5
+ import { tx } from '../db/schema.mjs';
6
+ import { createMsgCommands } from '../cli/commands/msg.mjs';
7
+ import { createTaskCommands } from '../cli/commands/task.mjs';
8
+ import { createLockCommands } from '../cli/commands/lock.mjs';
9
+ import { createCoordinationCommands } from '../cli/commands/coordination.mjs';
10
+ import { createCoordinationState } from '../coordination-state.mjs';
11
+ import { createMessageStore } from '../core/coordination/messages.mjs';
12
+ import { createTaskStore } from '../core/coordination/tasks.mjs';
13
+ import { createEvidenceRuntime } from '../core/peers/evidence-runtime.mjs';
14
+ import { annotateTasksWithLiveness } from '../core/peers/liveness.mjs';
15
+ import { clockGraceSuppressed, readClockGraceUntil } from '../shared/clock-grace.mjs';
16
+ import { clockSafetyUnavailable } from '../core/coordination/clock-safety.mjs';
17
+ import { leaseDeadline } from '../core/coordination/lease-renewal.mjs';
18
+ import { scopedLockResource, lockLabel, lockScope, locksConflict } from '../core/coordination/locks.mjs';
19
+ import { captureLockAcquireSubject, sameLockAcquireSubject, observeLockOwnerEvidence } from '../core/coordination/lock-evidence.mjs';
20
+ import { runOptimisticEvidenceMutation } from '../core/coordination/optimistic-evidence.mjs';
21
+ import { normalizeListText } from '../handoff.mjs';
22
+ import { listTaskResults, recordTaskResult } from '../core/coordination/results.mjs';
23
+
24
+ const string = (maxLength = 4096) => ({ type: 'string', minLength: 1, maxLength });
25
+ const integer = (maximum = Number.MAX_SAFE_INTEGER) => ({ type: 'integer', minimum: 1, maximum });
26
+ const strings = { type: 'array', maxItems: 50, items: string() };
27
+ const definitions = [
28
+ ['hcc_state', 'Read coordination state for this project and peer.', {}, [], true],
29
+ ['hcc_task_list', 'List tasks visible to this peer, with owner evidence.',
30
+ { limit: integer(100), all: { type: 'boolean' } }, [], true],
31
+ ['hcc_inbox', 'Read this peer inbox without acknowledging messages.',
32
+ { limit: integer(100), all: { type: 'boolean' } }, [], true],
33
+ ['hcc_message_send', 'Send a coordination message as this executor peer.',
34
+ { to: string(256), body: string(16000), task_id: integer(), kind: { type: 'string', enum: ['note', 'ask', 'progress'] } }, ['to', 'body'], false],
35
+ ['hcc_task_next', 'Return the current task or claim one pending task. Never completes a task.', {}, [], false],
36
+ ['hcc_handoff', 'Record work, tests and risks for an owned task without completing it.',
37
+ { task_id: integer(), summary: string(16000), to: string(256), changed_files: strings, tests: strings, risks: strings }, ['task_id', 'summary'], false],
38
+ ['hcc_lock_acquire', 'Acquire a scoped resource lock for this peer and its task.',
39
+ { resource: string(1024), task_id: integer(), scope: string(256), ttl: integer(86400), reason: string(4096) }, ['resource', 'task_id'], false],
40
+ ['hcc_lock_release', 'Release this peer own lock; cannot force another owner lock.',
41
+ { resource: string(1024), scope: string(256) }, ['resource'], false],
42
+ ['hcc_result_list', 'Read evidence records submitted by this peer.',
43
+ { task_id: integer(), limit: integer(200) }, [], true],
44
+ ['hcc_result_record', 'Record local evidence for an owned task. Does not record publication, business acceptance, or task completion.',
45
+ { task_id: integer(), kind: { type: 'string', enum: ['verification', 'command', 'diff'] },
46
+ title: string(300), details: string(64000), evidence: { ...strings, maxItems: 20, items: string(2000) },
47
+ status: { type: 'string', enum: ['passed', 'failed', 'pending'] }, result_key: string(300) },
48
+ ['task_id', 'title', 'status'], false]
49
+ ];
50
+
51
+ // Pure catalogue shared by MCP and the Cordis tool registry.
52
+ export function scopedMcpToolDefinitions() {
53
+ return definitions.map(([name, description, properties, required, readOnly]) => ({
54
+ name, description, inputSchema: structuredClone({ type: 'object', properties, required, additionalProperties: false }),
55
+ annotations: { readOnlyHint: readOnly, destructiveHint: !readOnly, openWorldHint: false }
56
+ }));
57
+ }
58
+
59
+ function validate(value, schema, label) {
60
+ if (schema.type === 'object') {
61
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new CliError('BAD_ARGS', `${label} must be an object`);
62
+ for (const key of Object.keys(value)) {
63
+ if (!Object.hasOwn(schema.properties, key)) throw new CliError('BAD_ARGS', `Unknown argument: ${key}`);
64
+ validate(value[key], schema.properties[key], key);
65
+ }
66
+ for (const key of schema.required || []) if (!Object.hasOwn(value, key)) throw new CliError('BAD_ARGS', `Missing argument: ${key}`);
67
+ } else if (schema.type === 'string') {
68
+ if (typeof value !== 'string' || !value.trim() || value.length < (schema.minLength || 0) ||
69
+ value.length > schema.maxLength || value.includes('\0') || (schema.enum && !schema.enum.includes(value))) {
70
+ throw new CliError('BAD_ARGS', `${label} must be a valid bounded string`);
71
+ }
72
+ } else if (schema.type === 'integer') {
73
+ if (!Number.isSafeInteger(value) || value < schema.minimum || value > schema.maximum) throw new CliError('BAD_ARGS', `${label} must be a positive bounded integer`);
74
+ } else if (schema.type === 'boolean') {
75
+ if (typeof value !== 'boolean') throw new CliError('BAD_ARGS', `${label} must be boolean`);
76
+ } else if (schema.type === 'array') {
77
+ if (!Array.isArray(value) || value.length > schema.maxItems) throw new CliError('BAD_ARGS', `${label} must be a bounded array`);
78
+ for (const item of value) validate(item, schema.items, label);
79
+ }
80
+ }
81
+
82
+ export function createScopedMcpTools({ ctx, authority, connect: baseConnect, touchPeer, now, addEvent }) {
83
+ const peer = authority.scope.peer, scopedCtx = { ...ctx, cwd: authority.scope.root,
84
+ root: authority.scope.root, dbPath: authority.scope.dbPath, json: true };
85
+ let captured, databases = [], active = false, taskGuard = null;
86
+ function assertWrite(db) {
87
+ authority.assertOwnership(db);
88
+ if (taskGuard !== null) {
89
+ const row = db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskGuard);
90
+ if (!row || row.owner !== peer) throw new CliError('TASK_OWNED', 'Task ownership changed before recording work');
91
+ }
92
+ }
93
+ function connect() {
94
+ const db = baseConnect(scopedCtx, { create: false, migrateRegistered: false });
95
+ databases.push(db);
96
+ authority.assertOwnership(db);
97
+ // Existing business functions keep their transactions. Guard each write
98
+ // inside that transaction, including commands that normally autocommit.
99
+ return new Proxy(db, { get(target, key) {
100
+ if (key === 'prepare') return sql => {
101
+ const statement = target.prepare(sql);
102
+ return new Proxy(statement, { get(stmt, property) {
103
+ if (property === 'run') return (...params) => {
104
+ const run = () => { assertWrite(target); return stmt.run(...params); };
105
+ return target.isTransaction ? run() : tx(target, run);
106
+ };
107
+ const value = stmt[property]; return typeof value === 'function' ? value.bind(stmt) : value;
108
+ } });
109
+ };
110
+ const value = target[key]; return typeof value === 'function' ? value.bind(target) : value;
111
+ } });
112
+ }
113
+ const evidence = createEvidenceRuntime({ now });
114
+ const messages = createMessageStore({ now, addEvent });
115
+ const tasks = createTaskStore({ now, addEvent, sendMessage: messages.sendMessage,
116
+ activePeerTtl: 600, observeClockSafety: evidence.observeTaskTakeoverClockSafety });
117
+ const dependencies = { connect, now, tx, addEvent, CliError, parseOpts, intOpt, required,
118
+ positiveSafeIntOpt, positiveIntOpt, wantsHelp, ACTIVE_PEER_TTL: 600, DEFAULT_LOCK_TTL: 900,
119
+ printResult: (_ctx, result) => { captured = result; },
120
+ resolveCurrentPeer: () => ({ id: peer, auto: false }),
121
+ touchCurrentPeer: (db, _ctx, identity, status) => {
122
+ if (identity.id !== peer) throw new CliError('MCP_SCOPE_INVALID', 'Peer identity changed');
123
+ authority.assertOwnership(db); touchPeer(db, peer, status);
124
+ },
125
+ iso: timestamp => new Date(timestamp * 1000).toISOString(),
126
+ ...messages, ...tasks, ...evidence,
127
+ annotateTasksWithLiveness, clockGraceSuppressed, readClockGraceUntil,
128
+ leaseDeadline, scopedLockResource, lockLabel, lockScope, locksConflict,
129
+ clockSafetyUnavailable, captureLockAcquireSubject, sameLockAcquireSubject,
130
+ observeLockOwnerEvidence, runOptimisticEvidenceMutation, normalizeListText
131
+ };
132
+ const { cmdTask, notifyTaskOwnerConflict } = createTaskCommands(dependencies);
133
+ const { cmdLock } = createLockCommands({ ...dependencies, notifyTaskOwnerConflict });
134
+ const { cmdMsg } = createMsgCommands(dependencies);
135
+ const { cmdHandoff } = createCoordinationCommands(dependencies);
136
+ const { statusSnapshot } = createCoordinationState({ connect, now, ...messages, ...tasks,
137
+ observePeerEvidence: evidence.observePeerEvidence });
138
+ const tools = scopedMcpToolDefinitions();
139
+ const byName = new Map(tools.map(tool => [tool.name, tool]));
140
+ const option = (key, value) => `--${key}=${value}`;
141
+ function assertTask(taskId, action) {
142
+ const db = connect();
143
+ const row = db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId);
144
+ if (!row) throw new CliError('NOT_FOUND', 'Task does not exist');
145
+ tasks.assertTaskOwnerForMutation(db, peer, row, action);
146
+ if (row.owner !== peer) throw new CliError('TASK_OWNED', 'Claim this task before recording work or acquiring its locks');
147
+ taskGuard = taskId;
148
+ }
149
+ async function execute(name, args) {
150
+ if (name === 'hcc_state') return statusSnapshot(scopedCtx, peer);
151
+ if (name === 'hcc_inbox') return messages.queryInbox(connect(), peer, args.all === true, args.limit || 20);
152
+ if (name === 'hcc_task_list') return cmdTask(scopedCtx, ['list', option('peer', peer), option('limit', args.limit || 50), ...(args.all ? ['--all'] : [])]);
153
+ if (name === 'hcc_task_next') return cmdTask(scopedCtx, ['next', option('peer', peer), '--count=1']);
154
+ if (name === 'hcc_result_list') return listTaskResults(connect(), { peer, taskId: args.task_id || null, limit: args.limit || 100 });
155
+ if (name === 'hcc_result_record') {
156
+ assertTask(args.task_id, 'result-record');
157
+ if (args.status === 'passed' && !args.evidence?.length) throw new CliError('BAD_ARGS', 'A passed result needs an evidence reference');
158
+ return recordTaskResult(connect(), { peer, taskId: args.task_id,
159
+ executorId: authority.scope.executorId, kind: args.kind, title: args.title,
160
+ details: args.details, evidence: args.evidence, status: args.status,
161
+ resultKey: args.result_key, stage: 'local', source: 'executor' }, { now, addEvent });
162
+ }
163
+ if (name === 'hcc_message_send') return cmdMsg(scopedCtx, ['send', option('from', peer), option('to', args.to),
164
+ option('body', args.body), option('kind', args.kind || 'note'), ...(args.task_id ? [option('task', args.task_id)] : [])]);
165
+ if (name === 'hcc_handoff') {
166
+ assertTask(args.task_id, 'handoff');
167
+ return cmdHandoff(scopedCtx, ['create', option('from', peer), option('task', args.task_id), option('summary', args.summary),
168
+ option('changed-files', JSON.stringify(args.changed_files || [])), option('tests', JSON.stringify(args.tests || [])),
169
+ option('risks', JSON.stringify(args.risks || [])), ...(args.to ? [option('to', args.to)] : [])]);
170
+ }
171
+ if (name === 'hcc_lock_acquire') assertTask(args.task_id, 'lock-acquire');
172
+ return cmdLock(scopedCtx, [name === 'hcc_lock_acquire' ? 'acquire' : 'release', option('peer', peer), option('resource', args.resource),
173
+ ...(args.scope ? [option('scope', args.scope)] : []), ...(args.task_id ? [option('task', args.task_id)] : []),
174
+ ...(args.ttl ? [option('ttl', args.ttl)] : []), ...(args.reason ? [option('reason', args.reason)] : [])]);
175
+ }
176
+ return {
177
+ list: () => tools.map(tool => structuredClone(tool)), has: name => byName.has(name),
178
+ async call(name, args) {
179
+ if (active) return { isError: true, content: [{ type: 'text', text: 'A scoped tool call is already running' }] };
180
+ active = true; captured = undefined; databases = []; taskGuard = null;
181
+ try {
182
+ if (!byName.has(name)) throw new CliError('BAD_ARGS', 'Unknown scoped tool');
183
+ validate(args, byName.get(name).inputSchema, 'arguments');
184
+ authority.assertValid();
185
+ const direct = await execute(name, args);
186
+ const structuredContent = redactSecrets({ ok: true, peer, data: captured === undefined ? direct : captured });
187
+ return { content: [{ type: 'text', text: JSON.stringify(structuredContent) }], structuredContent, isError: false };
188
+ } catch (error) {
189
+ const structuredContent = redactSecrets({ ok: false, peer, error: {
190
+ code: error instanceof CliError ? error.code : 'MCP_TOOL_FAILED',
191
+ message: error instanceof CliError ? error.message : 'The scoped tool call failed'
192
+ } });
193
+ return { content: [{ type: 'text', text: JSON.stringify(structuredContent) }], structuredContent, isError: true };
194
+ } finally { for (const db of databases) { try { db.close(); } catch {} } active = false; }
195
+ }
196
+ };
197
+ }
@@ -98,7 +98,9 @@ function parseMacBootToken(text) {
98
98
  }
99
99
 
100
100
  function macIdentity(pid) {
101
- const boot = run('sysctl', ['-n', 'kern.boottime']);
101
+ let boot = run('sysctl', ['-n', 'kern.boottime']);
102
+ // Launchers often omit /usr/sbin from PATH; macOS still provides sysctl here.
103
+ if (boot.error?.code === 'ENOENT') boot = run('/usr/sbin/sysctl', ['-n', 'kern.boottime']);
102
104
  if (boot.error || boot.status !== 0) return null;
103
105
  const bootToken = parseMacBootToken(boot.stdout);
104
106
  if (!bootToken) return null;
@@ -152,6 +154,36 @@ export async function waitForLiveProcessIdentity(value, options = {}) {
152
154
  }
153
155
  }
154
156
 
157
+ export async function waitForProcessIdentityExit(value, options = {}) {
158
+ const inspect = options.inspect || inspectProcessIdentity;
159
+ const monotonicNow = options.monotonicNow || (() => performance.now());
160
+ const wait = options.sleep || ((delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)));
161
+ const timeoutMs = Math.max(0, Number(options.timeoutMs ?? 5_000));
162
+ const intervalMs = Math.max(1, Number(options.intervalMs ?? 25));
163
+ const storedIdentity = isCompleteIdentity(value) ? value : null;
164
+ const pid = Number(storedIdentity?.pid ?? value);
165
+ if (!Number.isInteger(pid) || pid <= 0) return { state: 'unknown', identity: null };
166
+
167
+ const deadline = monotonicNow() + timeoutMs;
168
+ let lastObservation = { state: 'unknown', identity: null };
169
+ while (true) {
170
+ const observed = inspect(pid);
171
+ if (observed?.state === 'dead') return { state: 'dead', identity: null };
172
+ if (observed?.state === 'live' && isCompleteIdentity(observed.identity)) {
173
+ if (storedIdentity && compareProcessIdentity(storedIdentity, observed.identity) === 'dead') {
174
+ return { state: 'dead', identity: null };
175
+ }
176
+ lastObservation = observed;
177
+ } else {
178
+ lastObservation = { state: 'unknown', identity: null };
179
+ }
180
+
181
+ const remainingMs = deadline - monotonicNow();
182
+ if (remainingMs <= 0) return lastObservation;
183
+ await wait(Math.min(intervalMs, remainingMs));
184
+ }
185
+ }
186
+
155
187
  export function inspectProcessIdentity(value) {
156
188
  let pid;
157
189
  try {
@@ -2,6 +2,15 @@ export function normalizeVersion(version) {
2
2
  return String(version || '').replace(/^v/, '');
3
3
  }
4
4
 
5
+ export function isPrereleaseVersion(version) {
6
+ const numeric = '(?:0|[1-9][0-9]*)';
7
+ const identifier = `(?:${numeric}|[0-9]*[A-Za-z-][0-9A-Za-z-]*)`;
8
+ const semver = new RegExp(`^${numeric}\\.${numeric}\\.${numeric}(?:-(${identifier}(?:\\.${identifier})*))?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$`);
9
+ const match = normalizeVersion(version).match(semver);
10
+ if (!match) throw new Error(`Invalid release version: ${version}`);
11
+ return match[1] !== undefined;
12
+ }
13
+
5
14
  export function releaseSection(markdown, version) {
6
15
  const lines = markdown.split(/\r?\n/);
7
16
  const heading = `## ${version}`;
@@ -26,7 +26,8 @@ export function withBufferDirectoryLease(directory, fn, options = {}) {
26
26
  const lockOptions = {
27
27
  ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
28
28
  ...(options.retryMs === undefined ? {} : { retryMs: options.retryMs }),
29
- ...(options.nonblocking === undefined ? {} : { nonblocking: options.nonblocking })
29
+ ...(options.nonblocking === undefined ? {} : { nonblocking: options.nonblocking }),
30
+ ...(options.createParent === undefined ? {} : { createParent: options.createParent })
30
31
  };
31
32
  return withFileLock(target, () => fn(path.dirname(target)), lockOptions);
32
33
  }
@@ -4,6 +4,8 @@ import { runtimeApiUrl, runtimeHttpRequest } from '../web/runtime.mjs';
4
4
  import { withRuntimeApiVersionHeader } from '../web/api-version.mjs';
5
5
 
6
6
  const DEFAULT_CLI_NAME = 'hcc';
7
+ export const RUNTIME_SESSION_START_TIMEOUT_MS = 60_000;
8
+ export const RUNTIME_SESSION_MUTATION_TIMEOUT_MS = 30_000;
7
9
 
8
10
  function requestAbortScope(signal, timeoutMs) {
9
11
  const controller = new AbortController();
@@ -59,6 +61,7 @@ export async function runtimeRequest(ctx, method, route, body = null, runtime =
59
61
  method,
60
62
  headers,
61
63
  body: body === null ? null : JSON.stringify(body),
64
+ timeoutMs: opts.timeoutMs,
62
65
  signal: abortScope.signal
63
66
  }), abortScope.signal);
64
67
  text = res.text;
@@ -0,0 +1,38 @@
1
+ import http from 'node:http';
2
+ import { CliError } from '../../shared/errors.mjs';
3
+ import { readNativePointer } from './store.mjs';
4
+
5
+ export function nativeRequest(ctx, method, route, body = null, { timeoutMs = 35000 } = {}) {
6
+ const pointer = readNativePointer(ctx);
7
+ if (!pointer) return Promise.reject(new CliError('NATIVE_RUNTIME_OFFLINE', 'Native runtime is offline. Run hcc native up.'));
8
+ return new Promise((resolve, reject) => {
9
+ const payload = body === null ? null : JSON.stringify(body);
10
+ let timer;
11
+ const fail = (error) => { clearTimeout(timer); reject(error); };
12
+ const invalid = (message) => new CliError('NATIVE_RESPONSE_INVALID', message, { uncertain: method !== 'GET' });
13
+ const request = http.request({ host: '127.0.0.1', port: pointer.port, method, path: route,
14
+ headers: { authorization: `Bearer ${pointer.token}`, ...(payload ? { 'content-type': 'application/json', 'content-length': Buffer.byteLength(payload) } : {}) }
15
+ }, (response) => {
16
+ let data = '';
17
+ response.setEncoding('utf8');
18
+ response.on('data', (chunk) => {
19
+ data += chunk;
20
+ if (Buffer.byteLength(data) > 8 * 1024 * 1024) response.destroy(invalid('Native response is too large'));
21
+ });
22
+ response.on('error', (error) => fail(error instanceof CliError ? error : invalid('Native response was interrupted; inspect status before retrying.')));
23
+ response.on('end', () => {
24
+ clearTimeout(timer);
25
+ try {
26
+ const result = JSON.parse(data);
27
+ if (!result || typeof result !== 'object' || Array.isArray(result) || typeof result.ok !== 'boolean') throw invalid('Invalid native response; inspect status before retrying.');
28
+ if (!result.ok) fail(new CliError(result.error?.code || 'NATIVE_REQUEST_FAILED', result.error?.message || 'Native request failed', result.error?.extra || {}));
29
+ else if (response.statusCode < 200 || response.statusCode >= 300) fail(invalid('Native runtime returned an unsuccessful HTTP response'));
30
+ else resolve(result.data);
31
+ } catch (error) { fail(error instanceof CliError ? error : invalid('Invalid native JSON response; inspect status before retrying.')); }
32
+ });
33
+ });
34
+ timer = setTimeout(() => request.destroy(new CliError('NATIVE_CLIENT_TIMEOUT', 'Native runtime request timed out; inspect status before retrying.', { uncertain: method !== 'GET' })), timeoutMs);
35
+ request.on('error', (error) => fail(error instanceof CliError ? error : new CliError('NATIVE_RUNTIME_OFFLINE', 'Cannot reach the native runtime', { uncertain: method !== 'GET' })));
36
+ request.end(payload);
37
+ });
38
+ }