@logicseek/hello-cc 1.0.2 → 1.1.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +111 -23
- package/README.md +10 -1
- package/README.zh-CN.md +8 -1
- package/docs/commands.md +28 -4
- package/docs/commands.zh-CN.md +20 -4
- package/docs/dsh.md +2 -2
- package/docs/dsh.zh-CN.md +3 -3
- package/docs/native.md +84 -10
- package/docs/native.zh-CN.md +66 -5
- package/docs/web-handoff.zh-CN.md +11 -3
- package/lib/cli/app.mjs +42 -1
- package/lib/cli/commands/coordination.mjs +7 -2
- package/lib/cli/commands/dsh.mjs +1 -1
- package/lib/cli/commands/gc.mjs +9 -4
- package/lib/cli/commands/hook.mjs +6 -1
- package/lib/cli/commands/install.mjs +48 -6
- package/lib/cli/commands/lifecycle.mjs +14 -3
- package/lib/cli/commands/native.mjs +16 -9
- package/lib/cli/commands/query.mjs +15 -10
- package/lib/cli/commands/run.mjs +7 -6
- package/lib/core/coordination/results.mjs +27 -10
- package/lib/db/connection.mjs +135 -10
- package/lib/db/schema.mjs +59 -0
- package/lib/discover.mjs +4 -4
- package/lib/guidance.mjs +3 -2
- package/lib/integrations/codex-interactions.mjs +13 -11
- package/lib/integrations/dsh-collaboration.mjs +6 -12
- package/lib/integrations/dsh-cordis.mjs +17 -13
- package/lib/integrations/dsh.mjs +12 -9
- package/lib/integrations/hooks.mjs +132 -40
- package/lib/integrations/mcp-elicitation.mjs +116 -0
- package/lib/integrations/mcp-url-elicitation.mjs +25 -0
- package/lib/integrations/native/interactions.mjs +2 -1
- package/lib/integrations/peers/identity.mjs +42 -7
- package/lib/process/identity.mjs +15 -14
- package/lib/runtime/buffer-evidence.mjs +2 -1
- package/lib/runtime/native/service.mjs +11 -5
- package/lib/runtime/native/store.mjs +70 -18
- package/lib/runtime/paths.mjs +17 -2
- package/lib/runtime/private-file.mjs +76 -0
- package/lib/runtime/private-state.mjs +546 -0
- package/lib/runtime/project-context.mjs +2 -1
- package/lib/runtime/project-path.mjs +148 -12
- package/lib/runtime/project-trust.mjs +79 -0
- package/lib/runtime/projects.mjs +6 -3
- package/lib/runtime/state-migration.mjs +697 -0
- package/lib/runtime/state.mjs +22 -11
- package/lib/shared/automatic-result-key.mjs +6 -0
- package/lib/shared/json-file.mjs +55 -6
- package/lib/ui/help.mjs +6 -4
- package/lib/web/codex-app-server.mjs +26 -5
- package/lib/web/codex-sessions.mjs +5 -3
- package/lib/web/cookie-auth.mjs +1 -1
- package/lib/web/external-sessions.mjs +54 -5
- package/lib/web/http-routes.mjs +13 -9
- package/lib/web/http.mjs +20 -2
- package/lib/web/native-sessions.mjs +14 -7
- package/lib/web/peer-actions.mjs +27 -27
- package/lib/web/project-contexts.mjs +2 -3
- package/lib/web/runtime-main.mjs +22 -5
- package/lib/web/runtime.mjs +18 -4
- package/lib/web/session-serialize.mjs +1 -1
- package/lib/web/startup.mjs +115 -22
- package/lib/web/task-results.mjs +20 -17
- package/lib/web/terminal-assets.mjs +18 -0
- package/lib/web/tls.mjs +84 -33
- package/lib/web/tmux-sessions.mjs +2 -2
- package/lib/web/tmux-stream.mjs +3 -4
- package/lib/web/ui-codex.mjs +161 -32
- package/lib/web/ui-interactions.mjs +137 -4
- package/lib/web/ui-native.mjs +31 -13
- package/lib/web/ui-template.mjs +10 -3
- package/package.json +1 -1
- package/scripts/mcp-form-live-fixture.mjs +81 -0
- package/scripts/native-installed-acceptance.mjs +413 -0
- package/scripts/regression.mjs +46 -29
- package/scripts/web-mcp-form-acceptance.mjs +119 -0
- package/scripts/web-mcp-form-live-acceptance.mjs +368 -0
- package/scripts/web-native-interaction-acceptance.mjs +77 -10
|
@@ -16,7 +16,11 @@
|
|
|
16
16
|
|
|
17
17
|
结构化界面提供消息、当前 turn 状态、追加指令、轮次中断、计划、差异和人工交互。命令/文件审批以及空表单 MCP 工具确认由当前控制窗口明确批准本次或拒绝。文件与网络权限可逐项勾选,并明确选择本轮或会话有效期;只能授予原请求的子集。问题支持选项、其他/自由输入和敏感答案,需逐题明确填写。接管控制本身不会批准工具。
|
|
18
18
|
|
|
19
|
-
表单输入与焦点会跨状态刷新保留,直接答案不写入 HCC 状态、事件、回执或浏览器存储。原 turn 完成、中断成功、provider 解决请求或执行器关闭后,请求卡片失效;旧控制窗口不能用过期 epoch
|
|
19
|
+
表单输入与焦点会跨状态刷新保留,直接答案不写入 HCC 状态、事件、回执或浏览器存储。原 turn 完成、中断成功、provider 解决请求或执行器关闭后,请求卡片失效;旧控制窗口不能用过期 epoch 应答。参数被截断时只能拒绝或取消。MCP 支持下述标准表单和带原 thread/turn 标识的 URL 请求;嵌套对象、任意数组等复杂结构、动态工具及账号 token 刷新尚未接入。
|
|
20
|
+
|
|
21
|
+
MCP URL 请求显示授权目标,当前控制窗口点击“打开授权页面”后,原执行器接受请求才跳转。弹窗被拦截时不提交请求;过期请求不会打开授权地址。仅允许 HTTPS 或 loopback HTTP,链接带用户名/密码、其他协议、请求标识缺失或参数截断时保留拒绝/取消入口。loopback 地址需在执行器所在电脑打开,其他设备无法通过 HCC 代理本地授权页。
|
|
22
|
+
|
|
23
|
+
打开页面只表示同意发起外部流程,不表示认证成功。请在外部页面完成操作,再返回关注原任务结果;新页面不保留 opener/referrer。URL、设备码消息和元数据不进入 HCC 交互事件或浏览器存储,实时待处理请求仍可供控制窗口查看。账号登录、token 刷新和 OpenAI 用户验证扩展不包含在此能力内。
|
|
20
24
|
|
|
21
25
|
运行中发消息使用 steer;空闲时开始新 turn。中断当前轮与终止执行器是不同操作。turn 完成不自动把 hcc task 标记为验收完成。
|
|
22
26
|
|
|
@@ -26,6 +30,10 @@
|
|
|
26
30
|
|
|
27
31
|
关闭浏览器后 App Server 仍由 hello-cc runtime 管理;停止 runtime 会关闭它。新 runtime 不会自动重启 App Server 或重放未确认消息,可以从已保存 thread 显式恢复。
|
|
28
32
|
|
|
33
|
+
MCP 标准表单可以直接在 Web 填写文本、数值、是/否和单选/多选;两种执行器共用校验,答复仍回到同一请求。状态刷新保留页面内的填写内容,可选项不自动发送默认值;页面重载会清空答复。未支持的模式或结构会说明原因,保留拒绝/取消入口。实现和验收范围见 `docs/verification/2026-10-02-mcp-form-web-validation.md`。
|
|
34
|
+
|
|
35
|
+
真实模型复现脚本为 `scripts/web-mcp-form-live-acceptance.mjs --run-live`:先由本地 Codex 实际发起 MCP 交互,再从桌面/手机 Web 提交,核对同一 native 执行器继续完成和交回本地;另覆盖 Web-owned App Server。环境变量与隔离/账号范围见 `docs/native.zh-CN.md` 的真实模型与浏览器验收说明。成功以该次回执为准,默认运行只打印帮助,不调用模型。
|
|
36
|
+
|
|
29
37
|
## 本地 native worker 接手
|
|
30
38
|
|
|
31
39
|
先用 `hcc native start --peer codex-worker --provider codex` 启动后台任务,再打开 Web 的同一项目。Web 自动发现实际仍由 native runtime 持有的 worker,显示其原 peer/session、事件和投递回执;发送、中断和明确关闭都通过原 runtime 执行,不新建 provider 子进程。Claude/dsh worker 也走这条路径,按实际能力显示中断操作。
|
|
@@ -42,12 +50,12 @@
|
|
|
42
50
|
|
|
43
51
|
Web 专用 Codex App Server,以及由 HCC 托管的 Codex/Claude/dsh native worker,自动接入 [项目与 peer 受限的 MCP 协作工具](mcp.zh-CN.md)。配置只传给对应执行器或 worker,不写全局 provider 配置;关闭 worker 前撤销其临时权限。模型可查询任务与 inbox、领取任务、发送消息、写 handoff、获取/释放自己的资源锁和记录本地证据;不替用户执行发布或业务验收。
|
|
44
52
|
|
|
45
|
-
会话栏“结果审阅”汇总结构化 command
|
|
53
|
+
会话栏“结果审阅”汇总结构化 command 的执行状态与退出码、是否有 turn 差异、模型通过 MCP 明确提交的本地证据,以及人工添加的验收记录。自动结果仅在实时观察到新 turn 开始后归属当前任务;缺失该事件时,不会从历史快照补认任务证据。自动结果在项目数据库 events 中只保存中性标题、状态、退出码和来源 ID,不保存命令、输出或差异正文;正文仍可在可用的执行器会话或 provider 历史中查看。命令退出成功仅说明该命令成功;人工填写“通过”必须附证据引用。界面分别展示本地验证、发布部署、业务验收,并注明记录来源。turn 完成、测试记录或人工验收均不自动改变 hcc task 状态。`gc --history` 保留结果审计记录和最小提交去重凭据。
|
|
46
54
|
|
|
47
55
|
## 验收边界
|
|
48
56
|
|
|
49
57
|
本地验证包含真实 SQLite/HTTP、模拟交互请求、隔离 tmux 与桌面/手机浏览器。安装版 Codex 0.144.6 的协议验收另行覆盖 initialize、thread/start/list、10 个 MCP 工具发现和实际 `hcc_state` 读回;此协议检查不调用模型,可用 `node scripts/web-handoff-installed-acceptance.mjs` 重现。
|
|
50
58
|
|
|
51
|
-
真实模型验收已在隔离项目和 Codex home 中完成“本地 → Web → 本地”三段文件任务,全程保持原 Codex 进程和 thread;关闭 Web runtime 后仍可继续。可用 `scripts/web-native-model-acceptance.mjs` 在明确需要真实模型调用时重现。三种 provider 的真实生命周期、模型 MCP 发信、回复/ACK 和无回复循环另有独立回执。交互表单另有 11 项模拟 provider 浏览器检查。真实 Codex 已经完成“提问 → Web 答复 → 指定文件权限 → 本轮授权 → 写入所选答案 → 后续请求拒绝 → 本地继续”的验收;默认加载的 Claude SDK 已完成真实 Write 工具批准、拒绝和本地续接。dsh
|
|
59
|
+
真实模型验收已在隔离项目和 Codex home 中完成“本地 → Web → 本地”三段文件任务,全程保持原 Codex 进程和 thread;关闭 Web runtime 后仍可继续。可用 `scripts/web-native-model-acceptance.mjs` 在明确需要真实模型调用时重现。三种 provider 的真实生命周期、模型 MCP 发信、回复/ACK 和无回复循环另有独立回执。交互表单另有 11 项模拟 provider 浏览器检查。真实 Codex 已经完成“提问 → Web 答复 → 指定文件权限 → 本轮授权 → 写入所选答案 → 后续请求拒绝 → 本地继续”的验收;默认加载的 Claude SDK 已完成真实 Write 工具批准、拒绝和本地续接。dsh 已完成真实 ACP 写入的 Web 批准、拒绝和原 session 本地续接,审批卡片另通过桌面与 390px 手机检查;仅在隔离 profile 中对指定验收文件设置公开审批策略。完整结果与运行条件见 本地验收记录 (源码目录: `docs/verification/2026-10-02-web-local-task-handoff.md`)。
|
|
52
60
|
|
|
53
61
|
任意未托管 TUI/普通终端的活动执行器迁移、私有 daemon endpoint、任意协议版本的完整兼容矩阵,以及发布安装和真实业务验收仍在本次证据范围之外。
|
package/lib/cli/app.mjs
CHANGED
|
@@ -89,6 +89,9 @@ import { assignTeamWorkers, expandTeamWorkers, inferTeamItems } from '../core/co
|
|
|
89
89
|
import { autoPeerBasis, autoPeerKind, autoPeerProviderSession, autoPeerResumeId, autoPeerSessionId, readAncestorCliInfo, resolveCurrentPeer, resumeIdFromArgs, shortHash } from '../integrations/peers/identity.mjs';
|
|
90
90
|
import { inspectProviderProcess } from '../integrations/peers/processes.mjs';
|
|
91
91
|
import { registerProjectActivity } from '../runtime/projects.mjs';
|
|
92
|
+
import { migrateLegacyProjectState } from '../runtime/state-migration.mjs';
|
|
93
|
+
import { projectDbPath } from '../runtime/paths.mjs';
|
|
94
|
+
import { ensureDshIntegration } from '../integrations/dsh.mjs';
|
|
92
95
|
|
|
93
96
|
// Lazy-load lib modules (they may import node-pty which needs to be optional)
|
|
94
97
|
const _libDir = path.resolve(fileURLToPath(import.meta.url), '..', '..');
|
|
@@ -303,7 +306,7 @@ const { cmdInit, cmdRegister, cmdEnv, cmdJoin } = createLifecycleCommands({
|
|
|
303
306
|
connect, now, tx, addEvent, printResult, CliError, parseOpts, intOpt,
|
|
304
307
|
registerProjectActivity, resolveCurrentPeer, resolveTargetPeer,
|
|
305
308
|
upsertPeer, upsertCanonicalPeerBinding, storedPeerIdentity, detectBranch,
|
|
306
|
-
writeGuidance, helpEnv, helpJoin, shellExports, path, process
|
|
309
|
+
readAncestorCliInfo, writeGuidance, helpEnv, helpJoin, shellExports, path, process
|
|
307
310
|
});
|
|
308
311
|
|
|
309
312
|
const { cmdTeam } = createTeamCommands({
|
|
@@ -580,6 +583,43 @@ function removeGuidanceBlocks(ctx) {
|
|
|
580
583
|
return removeGuidanceBlocksForRoot(ctx.root);
|
|
581
584
|
}
|
|
582
585
|
|
|
586
|
+
function cmdMigrateState(ctx, args) {
|
|
587
|
+
if (wantsHelp(args)) {
|
|
588
|
+
console.log(`${CLI_NAME} migrate-state --offline --yes\n\n` +
|
|
589
|
+
'Stop all old hello-cc Web, Native, PTY, tmux, hook and external database writers first. ' +
|
|
590
|
+
'This command verifies known process evidence, snapshots SQLite including WAL, and leaves the old directory untouched.');
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
const opts = parseOpts(args, { booleans: ['offline', 'yes'] });
|
|
594
|
+
validateOpts('migrate-state', opts, ['offline', 'yes']);
|
|
595
|
+
if (opts.offline !== true || opts.yes !== true) {
|
|
596
|
+
throw new CliError('STATE_MIGRATION_OFFLINE_REQUIRED',
|
|
597
|
+
'Stop every old project-state writer, then acknowledge downtime with migrate-state --offline --yes');
|
|
598
|
+
}
|
|
599
|
+
const result = migrateLegacyProjectState(ctx.root, {
|
|
600
|
+
confirmedOffline: true,
|
|
601
|
+
// The operator's explicit --offline assertion covers external writers
|
|
602
|
+
// that cannot be discovered reliably across supported platforms. The
|
|
603
|
+
// migrator still checks known runtime, PTY and SQLite peer evidence.
|
|
604
|
+
assertOffline: () => true
|
|
605
|
+
});
|
|
606
|
+
let dshRebuilt = false;
|
|
607
|
+
if (result.dsh) {
|
|
608
|
+
try {
|
|
609
|
+
ensureDshIntegration({ ...ctx, dbPath: projectDbPath(ctx.root) }, { mode: result.dsh.mode });
|
|
610
|
+
dshRebuilt = true;
|
|
611
|
+
} catch (error) {
|
|
612
|
+
throw new CliError('STATE_MIGRATION_DSH_REBUILD_REQUIRED',
|
|
613
|
+
`Project state migrated to ${result.stateDir}, but DSH configuration needs rebuilding. ` +
|
|
614
|
+
`Run ${result.dsh.rebuildCommand}. Original data remains at ${result.sourceDir}.`,
|
|
615
|
+
{ cause: error });
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
printResult(ctx, { ...result, dsh_rebuilt: dshRebuilt },
|
|
619
|
+
() => `project state migrated: ${result.stateDir}\nlegacy source kept: ${result.sourceDir}` +
|
|
620
|
+
(dshRebuilt ? '\nDSH integration rebuilt for the new private path' : ''));
|
|
621
|
+
}
|
|
622
|
+
|
|
583
623
|
// ─── hcc doctor ─────────────────────────────────────────────────────────────
|
|
584
624
|
// Health self-check: SQLite integrity, schema version, WAL/DB size, row counts.
|
|
585
625
|
async function dispatch(ctx, rest) {
|
|
@@ -592,6 +632,7 @@ async function dispatch(ctx, rest) {
|
|
|
592
632
|
if (command === 'update') return cmdUpdate(ctx, args);
|
|
593
633
|
if (command === 'uninstall') return cmdUninstall(ctx, args);
|
|
594
634
|
if (command === 'init') return cmdInit(ctx, args);
|
|
635
|
+
if (command === 'migrate-state') return cmdMigrateState(ctx, args);
|
|
595
636
|
if (command === 'register') return cmdRegister(ctx, args);
|
|
596
637
|
if (command === 'join') return cmdJoin(ctx, args);
|
|
597
638
|
if (command === 'env') return cmdEnv(ctx, args);
|
|
@@ -184,8 +184,13 @@ export function createCoordinationCommands(deps) {
|
|
|
184
184
|
const kind = opts.kind || 'broadcast';
|
|
185
185
|
const runtime = opts.inject ? readRuntime(ctx) : null;
|
|
186
186
|
const db = connect(ctx);
|
|
187
|
-
|
|
188
|
-
|
|
187
|
+
let id;
|
|
188
|
+
try {
|
|
189
|
+
touchCurrentPeer(db, ctx, identity, null, 'shell');
|
|
190
|
+
id = sendMessage(db, sender, 'all', taskId, kind, body);
|
|
191
|
+
} finally {
|
|
192
|
+
db.close();
|
|
193
|
+
}
|
|
189
194
|
let injected = 0;
|
|
190
195
|
let skipped = 0;
|
|
191
196
|
if (opts.inject) {
|
package/lib/cli/commands/dsh.mjs
CHANGED
|
@@ -14,7 +14,7 @@ function defaultHelp() {
|
|
|
14
14
|
' hcc dsh status [--dsh-bin PATH]',
|
|
15
15
|
' hcc dsh web [--mode hooks|cordis|off] [--dsh-bin PATH] [--dsh-home PATH] -- [dsh web arguments]',
|
|
16
16
|
'',
|
|
17
|
-
'Setup writes
|
|
17
|
+
'Setup writes managed configuration under the selected private state directory. Modes: hooks (default), cordis (native tools), off.',
|
|
18
18
|
'Web starts the official dsh web runtime; its real sessions join hello-cc through the selected integration.',
|
|
19
19
|
'Pass runtime arguments after --, for example: hcc dsh web -- --port 8080',
|
|
20
20
|
'Show dsh web help: hcc dsh web -- --help',
|
package/lib/cli/commands/gc.mjs
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
import { runOptimisticEvidenceMutation } from '../../core/coordination/optimistic-evidence.mjs';
|
|
27
27
|
import { peerEvidenceAllowsReap } from '../../core/peers/evidence.mjs';
|
|
28
28
|
import { runtimePath, globalRuntimePath } from '../../runtime/paths.mjs';
|
|
29
|
+
import { resolveProjectDatabase } from '../../runtime/project-path.mjs';
|
|
29
30
|
import { readRuntime, reclaimRuntimePointerFiles } from '../../runtime/state.mjs';
|
|
30
31
|
import { runtimeRequest, runtimeBufferGcUnavailable } from '../../runtime/client.mjs';
|
|
31
32
|
import { collectBufferEvidence } from '../../runtime/buffer-evidence.mjs';
|
|
@@ -263,12 +264,16 @@ function captureRequiredBufferDirectoryComponent(component, description) {
|
|
|
263
264
|
function captureOptionalBufferDirectory(root, directoryName) {
|
|
264
265
|
const requestedRoot = path.resolve(root);
|
|
265
266
|
const canonicalRoot = fs.realpathSync.native(requestedRoot);
|
|
266
|
-
const stateDirectory =
|
|
267
|
+
const stateDirectory = resolveProjectDatabase({ root: canonicalRoot, createStateDir: false }).stateDir;
|
|
267
268
|
const directory = path.join(stateDirectory, directoryName);
|
|
268
269
|
const components = [
|
|
269
|
-
captureRequiredBufferDirectoryComponent(canonicalRoot, 'Project root')
|
|
270
|
-
captureRequiredBufferDirectoryComponent(stateDirectory, 'Project state directory')
|
|
270
|
+
captureRequiredBufferDirectoryComponent(canonicalRoot, 'Project root')
|
|
271
271
|
];
|
|
272
|
+
try {
|
|
273
|
+
components.push(captureRequiredBufferDirectoryComponent(stateDirectory, 'Project state directory'));
|
|
274
|
+
} catch (error) {
|
|
275
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
276
|
+
}
|
|
272
277
|
|
|
273
278
|
let stat;
|
|
274
279
|
try {
|
|
@@ -624,7 +629,7 @@ function runGcWithHistorySnapshot(ctx, db, {
|
|
|
624
629
|
}
|
|
625
630
|
|
|
626
631
|
function bufferDirectory(ctx) {
|
|
627
|
-
return path.
|
|
632
|
+
return path.join(resolveProjectDatabase({ root: ctx.root, createStateDir: false }).stateDir, BUFS_DIR_NAME);
|
|
628
633
|
}
|
|
629
634
|
|
|
630
635
|
function bufferDirectoryPaths(directory) {
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { parseOpts, validateOpts } from '../../cli-args.mjs';
|
|
5
5
|
import { tx } from '../../db/schema.mjs';
|
|
6
6
|
import { resolveProjectDatabase } from '../../runtime/project-path.mjs';
|
|
7
|
+
import { projectDbPath } from '../../runtime/paths.mjs';
|
|
7
8
|
|
|
8
9
|
export function createHookCommand(deps) {
|
|
9
10
|
const {
|
|
@@ -89,13 +90,17 @@ async function cmdHook(ctx, args) {
|
|
|
89
90
|
const dshProject = isDsh && !nativeOwner ? resolveProjectDatabase({ root: hccRoot, createStateDir: false }) : null;
|
|
90
91
|
const nativeProject = nativeOwner ? resolveProjectDatabase({ root: hccRoot,
|
|
91
92
|
db: process.env.HCC_DB, createStateDir: false }) : null;
|
|
93
|
+
const ordinaryDb = (ctx.root && path.resolve(ctx.root) === hccRoot ? ctx.dbPath : null) ||
|
|
94
|
+
process.env.HCC_DB;
|
|
92
95
|
if (nativeProject && !fs.existsSync(nativeProject.db)) {
|
|
93
96
|
throw new CliError('NATIVE_HOOK_OWNERSHIP_MISMATCH', 'Native hook database must already be owned by its runtime');
|
|
94
97
|
}
|
|
98
|
+
const ordinaryDefaultDb = !dshProject && !nativeProject && !ordinaryDb
|
|
99
|
+
? projectDbPath(hccRoot) : null;
|
|
95
100
|
const hookCtx = {
|
|
96
101
|
...ctx,
|
|
97
102
|
root: nativeProject?.root || dshProject?.root || hccRoot,
|
|
98
|
-
dbPath: nativeProject?.db || dshProject?.db || path.
|
|
103
|
+
dbPath: nativeProject?.db || dshProject?.db || ordinaryDefaultDb || path.resolve(ordinaryDb)
|
|
99
104
|
};
|
|
100
105
|
if (!nativeOwner) registerProjectActivity(hookCtx);
|
|
101
106
|
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
// Install/setup/update/uninstall/shim commands extracted from bin/hcc.mjs.
|
|
2
2
|
|
|
3
3
|
import { execFileSync, spawnSync } from 'node:child_process';
|
|
4
|
+
import { secureExistingProjectStateDirectory } from '../../runtime/project-path.mjs';
|
|
5
|
+
import { legacyProjectStateDir, projectStateDir } from '../../runtime/paths.mjs';
|
|
6
|
+
import { privateProjectStateDir, inspectPrivateProjectAuthority,
|
|
7
|
+
purgePrivateProjectState } from '../../runtime/private-state.mjs';
|
|
4
8
|
|
|
5
9
|
export function createInstallCommands(deps) {
|
|
6
10
|
const {
|
|
@@ -236,14 +240,33 @@ async function cmdUninstall(ctx, args) {
|
|
|
236
240
|
if (opts.purge && !opts.yes) {
|
|
237
241
|
throw new CliError('CONFIRM_REQUIRED', 'Refusing to purge project data without --yes');
|
|
238
242
|
}
|
|
243
|
+
// Resolve before changing hooks, guidance, or runtime state. An unsafe root
|
|
244
|
+
// with unmigrated legacy data must not be treated as an empty private store.
|
|
245
|
+
let purgeStateDir = null;
|
|
246
|
+
let missingPrivateState = false;
|
|
247
|
+
if (opts.purge) {
|
|
248
|
+
try {
|
|
249
|
+
purgeStateDir = secureExistingProjectStateDirectory(ctx.root).stateDir;
|
|
250
|
+
} catch (error) {
|
|
251
|
+
if (projectStateDir(ctx.root) !== privateProjectStateDir(ctx.root)) throw error;
|
|
252
|
+
const recoverablePending = error?.code === 'STATE_MIGRATION_REQUIRED' &&
|
|
253
|
+
inspectPrivateProjectAuthority(ctx.root) === 'pending';
|
|
254
|
+
if (!['STATE_AUTHORITY_MISSING', 'STATE_PURGE_INCOMPLETE'].includes(error?.code) &&
|
|
255
|
+
!recoverablePending) throw error;
|
|
256
|
+
purgeStateDir = privateProjectStateDir(ctx.root);
|
|
257
|
+
missingPrivateState = true;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
239
260
|
|
|
240
261
|
const lines = [];
|
|
241
262
|
|
|
242
263
|
let runtime = null;
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
264
|
+
if (!missingPrivateState) {
|
|
265
|
+
try {
|
|
266
|
+
runtime = readRuntime(ctx);
|
|
267
|
+
} catch (err) {
|
|
268
|
+
if (!(err instanceof CliError && err.code === 'RUNTIME_NOT_RUNNING')) throw err;
|
|
269
|
+
}
|
|
247
270
|
}
|
|
248
271
|
if (runtime) {
|
|
249
272
|
try {
|
|
@@ -273,9 +296,28 @@ async function cmdUninstall(ctx, args) {
|
|
|
273
296
|
if (opts.purge) {
|
|
274
297
|
guidance = removeGuidanceBlocks(ctx);
|
|
275
298
|
try {
|
|
276
|
-
|
|
299
|
+
if (path.resolve(purgeStateDir) === path.resolve(privateProjectStateDir(ctx.root))) {
|
|
300
|
+
// Revalidate and delete under the same transition lock used by init
|
|
301
|
+
// and migration. An interrupted deletion remains marked purging.
|
|
302
|
+
purgeStateDir = purgePrivateProjectState(ctx.root);
|
|
303
|
+
} else {
|
|
304
|
+
// Async shutdown and hook removal happened after the initial lookup.
|
|
305
|
+
// Refuse a stale legacy target if project-state routing changed.
|
|
306
|
+
const current = secureExistingProjectStateDirectory(ctx.root).stateDir;
|
|
307
|
+
if (path.resolve(current) !== path.resolve(purgeStateDir)) {
|
|
308
|
+
throw new CliError('PROJECT_PATH_FORBIDDEN', 'Project state route changed during uninstall');
|
|
309
|
+
}
|
|
310
|
+
fs.rmSync(purgeStateDir, { recursive: true, force: true });
|
|
311
|
+
}
|
|
277
312
|
purged = true;
|
|
278
|
-
lines.push(`project data removed: ${
|
|
313
|
+
lines.push(`project data removed: ${purgeStateDir}`);
|
|
314
|
+
const legacyDir = legacyProjectStateDir(ctx.root);
|
|
315
|
+
if (path.resolve(legacyDir) !== path.resolve(purgeStateDir) && fs.existsSync(legacyDir)) {
|
|
316
|
+
lines.push(`legacy project data kept: ${legacyDir}`);
|
|
317
|
+
lines.push('private authority kept; future use requires an explicit offline migration of the legacy source');
|
|
318
|
+
} else if (path.resolve(legacyDir) !== path.resolve(purgeStateDir)) {
|
|
319
|
+
lines.push('private authority kept; a fresh init can now create new private state');
|
|
320
|
+
}
|
|
279
321
|
} catch (err) {
|
|
280
322
|
throw new CliError('PURGE_FAILED', `Could not remove .hello-cc: ${err.message}`);
|
|
281
323
|
}
|
|
@@ -4,10 +4,21 @@ export function createLifecycleCommands(deps) {
|
|
|
4
4
|
const {
|
|
5
5
|
connect, now, addEvent, printResult, CliError, parseOpts, intOpt,
|
|
6
6
|
registerProjectActivity, resolveCurrentPeer, resolveTargetPeer,
|
|
7
|
-
upsertPeer, upsertCanonicalPeerBinding, detectBranch,
|
|
7
|
+
upsertPeer, upsertCanonicalPeerBinding, detectBranch, readAncestorCliInfo,
|
|
8
8
|
writeGuidance, helpEnv, helpJoin, shellExports, path, process
|
|
9
9
|
} = deps;
|
|
10
10
|
|
|
11
|
+
function registrationPid(opts) {
|
|
12
|
+
if (opts.pid !== undefined) return intOpt(opts, 'pid', process.ppid);
|
|
13
|
+
const ancestor = readAncestorCliInfo();
|
|
14
|
+
if (['claude', 'codex'].includes(ancestor?.kind) &&
|
|
15
|
+
(!opts.kind || opts.kind === ancestor.kind) &&
|
|
16
|
+
Number.isSafeInteger(ancestor.pid) && ancestor.pid > 1) {
|
|
17
|
+
return ancestor.pid;
|
|
18
|
+
}
|
|
19
|
+
return process.ppid;
|
|
20
|
+
}
|
|
21
|
+
|
|
11
22
|
async function cmdInit(ctx, args) {
|
|
12
23
|
registerProjectActivity(ctx);
|
|
13
24
|
const opts = parseOpts(args, { booleans: ['no-guidance'] });
|
|
@@ -34,7 +45,7 @@ export function createLifecycleCommands(deps) {
|
|
|
34
45
|
role: opts.role || '',
|
|
35
46
|
worktree: path.resolve(opts.worktree || ctx.cwd),
|
|
36
47
|
branch: opts.branch || detectBranch(ctx.cwd),
|
|
37
|
-
pid:
|
|
48
|
+
pid: registrationPid(opts),
|
|
38
49
|
status: opts.status || 'idle',
|
|
39
50
|
capabilities: Array.isArray(opts.cap) ? opts.cap.join(',') : (opts.cap || opts.capabilities || '')
|
|
40
51
|
};
|
|
@@ -67,7 +78,7 @@ export function createLifecycleCommands(deps) {
|
|
|
67
78
|
role: opts.role || 'peer',
|
|
68
79
|
worktree: path.resolve(opts.worktree || ctx.cwd),
|
|
69
80
|
branch: opts.branch || detectBranch(ctx.cwd),
|
|
70
|
-
pid:
|
|
81
|
+
pid: registrationPid(opts),
|
|
71
82
|
status: opts.status || 'working',
|
|
72
83
|
capabilities: Array.isArray(opts.cap) ? opts.cap.join(',') : (opts.cap || opts.capabilities || 'manual-shell')
|
|
73
84
|
};
|
|
@@ -7,6 +7,7 @@ import { parseOpts, validateOpts, wantsHelp, intOpt } from '../../cli-args.mjs';
|
|
|
7
7
|
import { printResult } from '../../format.mjs';
|
|
8
8
|
import { commandPath } from '../../cli-runtime.mjs';
|
|
9
9
|
import { nativePaths, readNativePointer } from '../../runtime/native/store.mjs';
|
|
10
|
+
import { openPrivateAppendFile } from '../../runtime/private-file.mjs';
|
|
10
11
|
import { nativeRequest } from '../../runtime/native/client.mjs';
|
|
11
12
|
import { startNativeService } from '../../runtime/native/service.mjs';
|
|
12
13
|
|
|
@@ -37,15 +38,21 @@ export function createNativeCommands(deps) {
|
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
const paths = nativePaths(ctx, { create: true });
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
let log;
|
|
42
|
+
let child;
|
|
43
|
+
try {
|
|
44
|
+
log = openPrivateAppendFile(paths.log);
|
|
45
|
+
child = spawn(process.execPath, [commandPath(), '--root', fs.realpathSync(ctx.root), '--db', path.resolve(ctx.dbPath), 'native', 'serve'], {
|
|
46
|
+
cwd: ctx.root, env: process.env, detached: true, stdio: ['ignore', log, log]
|
|
47
|
+
});
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (error?.code === 'PROJECT_PATH_FORBIDDEN') {
|
|
50
|
+
throw new CliError('NATIVE_STATE_UNSAFE', error.message);
|
|
51
|
+
}
|
|
52
|
+
throw error;
|
|
53
|
+
} finally {
|
|
54
|
+
if (log !== undefined) fs.closeSync(log);
|
|
44
55
|
}
|
|
45
|
-
const child = spawn(process.execPath, [commandPath(), '--root', fs.realpathSync(ctx.root), '--db', path.resolve(ctx.dbPath), 'native', 'serve'], {
|
|
46
|
-
cwd: ctx.root, env: process.env, detached: true, stdio: ['ignore', log, log]
|
|
47
|
-
});
|
|
48
|
-
fs.closeSync(log);
|
|
49
56
|
let failed;
|
|
50
57
|
child.on('error', (error) => { failed = error; });
|
|
51
58
|
child.unref();
|
|
@@ -113,7 +120,7 @@ export function createNativeCommands(deps) {
|
|
|
113
120
|
const data = fs.readFileSync(path.resolve(ctx.cwd, opts['response-file']), 'utf8');
|
|
114
121
|
if (data.length > 100000) throw new CliError('BAD_ARGS', 'Response file exceeds the limit');
|
|
115
122
|
try { response = JSON.parse(data); } catch { throw new CliError('BAD_ARGS', 'Response file must contain JSON'); }
|
|
116
|
-
if (!response || typeof response !== 'object' || Array.isArray(response) || Object.keys(response).some(key => !['permissions','scope','answers'].includes(key))) throw new CliError('BAD_ARGS', 'Response file accepts permissions, scope and
|
|
123
|
+
if (!response || typeof response !== 'object' || Array.isArray(response) || Object.keys(response).some(key => !['permissions','scope','answers','content'].includes(key))) throw new CliError('BAD_ARGS', 'Response file accepts permissions, scope, answers and MCP content only');
|
|
117
124
|
}
|
|
118
125
|
const request = pending[0];
|
|
119
126
|
result = await nativeRequest(ctx, 'POST', '/respond', { peer: opts.peer, generation: state.generation,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Coordination query commands extracted from bin/hcc.mjs: peers, status, state, prompt.
|
|
2
2
|
|
|
3
|
+
import { shellQuoteArg } from '../../format.mjs';
|
|
4
|
+
|
|
3
5
|
export function createQueryCommands(deps) {
|
|
4
6
|
const {
|
|
5
7
|
connect, now, printResult, table, CliError, ACTIVE_PEER_TTL,
|
|
@@ -63,7 +65,10 @@ export function createQueryCommands(deps) {
|
|
|
63
65
|
const peer = required(opts, 'peer', 'HCC_PEER');
|
|
64
66
|
const kind = opts.kind || 'codex';
|
|
65
67
|
const role = opts.role || 'peer';
|
|
66
|
-
const cmd =
|
|
68
|
+
const cmd = ['node', commandPath(), '--root', ctx.root, '--db', ctx.dbPath].map(shellQuoteArg).join(' ');
|
|
69
|
+
const quotedPeer = shellQuoteArg(peer);
|
|
70
|
+
const quotedKind = shellQuoteArg(kind);
|
|
71
|
+
const quotedRole = shellQuoteArg(role);
|
|
67
72
|
const text = `You are ${peer}, a ${kind} ${role} session working in this project.
|
|
68
73
|
|
|
69
74
|
Use hcc as the shared coordination bus for this terminal session. This
|
|
@@ -71,23 +76,23 @@ project uses a flat peer mesh: there is no required main/worker hierarchy.
|
|
|
71
76
|
|
|
72
77
|
Run these commands before changing files:
|
|
73
78
|
|
|
74
|
-
${cmd} register --peer ${
|
|
75
|
-
${cmd} state --peer ${
|
|
76
|
-
${cmd} msg inbox --peer ${
|
|
77
|
-
${cmd} task next --peer ${
|
|
79
|
+
${cmd} register --peer ${quotedPeer} --kind ${quotedKind} --role ${quotedRole}
|
|
80
|
+
${cmd} state --peer ${quotedPeer}
|
|
81
|
+
${cmd} msg inbox --peer ${quotedPeer}
|
|
82
|
+
${cmd} task next --peer ${quotedPeer}
|
|
78
83
|
|
|
79
84
|
Coordination rules:
|
|
80
85
|
- Claim exactly one task before editing.
|
|
81
86
|
- If state shows a current task for ${peer}, continue that task before claiming another pending task.
|
|
82
87
|
- Before editing a file, directory, module, or shared test resource, run:
|
|
83
|
-
${cmd} lock acquire --peer ${
|
|
88
|
+
${cmd} lock acquire --peer ${quotedPeer} --resource <path-or-module> [--scope <scope>] --task <task-id>
|
|
84
89
|
- If a lock is held by another peer, message that peer instead of editing:
|
|
85
|
-
${cmd} msg send --from ${
|
|
90
|
+
${cmd} msg send --from ${quotedPeer} --to <peer-id> --body "<question>"
|
|
86
91
|
- Report progress or requests through msg send.
|
|
87
92
|
- Before stopping, run tests, create a handoff, and release locks:
|
|
88
|
-
${cmd} handoff create --from ${
|
|
89
|
-
${cmd} task done --peer ${
|
|
90
|
-
${cmd} lock release --peer ${
|
|
93
|
+
${cmd} handoff create --from ${quotedPeer} --task <task-id> --summary "<what changed>" --tests "<commands/results>" --risks "<known risks>"
|
|
94
|
+
${cmd} task done --peer ${quotedPeer} --id <task-id> --summary "<done summary>"
|
|
95
|
+
${cmd} lock release --peer ${quotedPeer} --resource <path-or-module> [--scope <scope>]
|
|
91
96
|
`;
|
|
92
97
|
printResult(ctx, { prompt: text }, () => text);
|
|
93
98
|
}
|
package/lib/cli/commands/run.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { CliError } from '../../shared/errors.mjs';
|
|
|
11
11
|
import { tx } from '../../db/schema.mjs';
|
|
12
12
|
import { parseOpts, validateOpts } from '../../cli-args.mjs';
|
|
13
13
|
import { runtimeProcessIdentity } from '../../runtime/state.mjs';
|
|
14
|
+
import { ensurePrivateProjectBufferDirectory } from '../../runtime/project-path.mjs';
|
|
14
15
|
import { withBufferDirectoryLease } from '../../runtime/buffer-directory-lease.mjs';
|
|
15
16
|
import {
|
|
16
17
|
externalBufferEvidence,
|
|
@@ -178,8 +179,7 @@ async function cmdRunWebManaged(ctx, { id, kind, role, cwd, command, commandArgs
|
|
|
178
179
|
const ptyModule = await import('node-pty');
|
|
179
180
|
const pty = ptyModule.default || ptyModule;
|
|
180
181
|
|
|
181
|
-
const bufsDir =
|
|
182
|
-
fs.mkdirSync(bufsDir, { recursive: true });
|
|
182
|
+
const bufsDir = ensurePrivateProjectBufferDirectory(ctx.root, BUFS_DIR_NAME);
|
|
183
183
|
const outFile = path.join(bufsDir, `${id}.out`);
|
|
184
184
|
const inFile = path.join(bufsDir, `${id}.in`);
|
|
185
185
|
const resizeFile = path.join(bufsDir, `${id}.resize`);
|
|
@@ -219,8 +219,8 @@ async function cmdRunWebManaged(ctx, { id, kind, role, cwd, command, commandArgs
|
|
|
219
219
|
}
|
|
220
220
|
for (const file of bufferFiles) fs.rmSync(file, { force: true });
|
|
221
221
|
}
|
|
222
|
-
fs.writeFileSync(inFile, '');
|
|
223
|
-
fs.writeFileSync(resizeFile, '');
|
|
222
|
+
fs.writeFileSync(inFile, '', { flag: 'wx', mode: 0o600 });
|
|
223
|
+
fs.writeFileSync(resizeFile, '', { flag: 'wx', mode: 0o600 });
|
|
224
224
|
fs.writeFileSync(metaFile, JSON.stringify({
|
|
225
225
|
id,
|
|
226
226
|
generation: bufferGeneration,
|
|
@@ -231,7 +231,7 @@ async function cmdRunWebManaged(ctx, { id, kind, role, cwd, command, commandArgs
|
|
|
231
231
|
wrapper_pid: process.pid,
|
|
232
232
|
...(publishingWrapperIdentity ? { wrapper_identity: publishingWrapperIdentity } : {}),
|
|
233
233
|
publishing: true
|
|
234
|
-
}));
|
|
234
|
+
}), { flag: 'wx', mode: 0o600 });
|
|
235
235
|
|
|
236
236
|
const db = connect(ctx);
|
|
237
237
|
try {
|
|
@@ -359,7 +359,8 @@ async function cmdRunWebManaged(ctx, { id, kind, role, cwd, command, commandArgs
|
|
|
359
359
|
if (!ownsExternalBufferGroup()) {
|
|
360
360
|
throw new CliError('EXTERNAL_SESSION_SUPERSEDED', `External session ${id} was replaced during startup`);
|
|
361
361
|
}
|
|
362
|
-
outFd = fs.openSync(outFile,
|
|
362
|
+
outFd = fs.openSync(outFile, fs.constants.O_WRONLY | fs.constants.O_CREAT |
|
|
363
|
+
fs.constants.O_EXCL | (fs.constants.O_NOFOLLOW || 0), 0o600);
|
|
363
364
|
});
|
|
364
365
|
} catch (error) {
|
|
365
366
|
await failStartup(error?.code || 'external_buffer_open_failed');
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { tx } from '../../db/schema.mjs';
|
|
3
|
+
import { automaticResultKey } from '../../shared/automatic-result-key.mjs';
|
|
3
4
|
import { CliError } from '../../shared/errors.mjs';
|
|
4
5
|
|
|
5
6
|
export const RESULT_EVENT = 'task.result.recorded';
|
|
@@ -18,7 +19,7 @@ function text(value, field, max, required = false) {
|
|
|
18
19
|
|
|
19
20
|
// Evidence records do not mutate task status. The owner is checked in the same
|
|
20
21
|
// transaction as the insert, including after a concurrent task takeover.
|
|
21
|
-
export function recordTaskResult(db, input, { now = () => Math.floor(Date.now() / 1000), addEvent } = {}) {
|
|
22
|
+
export function recordTaskResult(db, input, { now = () => Math.floor(Date.now() / 1000), addEvent, automatic = false } = {}) {
|
|
22
23
|
if (!input || typeof input !== 'object' || Array.isArray(input)) throw new CliError('BAD_REQUEST', 'A result object is required');
|
|
23
24
|
const peer = text(input.peer, 'peer', 200, true);
|
|
24
25
|
const taskId = input.taskId;
|
|
@@ -29,21 +30,35 @@ export function recordTaskResult(db, input, { now = () => Math.floor(Date.now()
|
|
|
29
30
|
throw new CliError('BAD_REQUEST', 'Unsupported result stage, status, kind or source');
|
|
30
31
|
}
|
|
31
32
|
if (source === 'executor' && stage !== 'local') throw new CliError('BAD_REQUEST', 'Executor records can only report local evidence');
|
|
33
|
+
if (automatic && (source !== 'executor' || !['command', 'diff'].includes(kind))) {
|
|
34
|
+
throw new CliError('BAD_REQUEST', 'Automatic results must be local executor commands or diffs');
|
|
35
|
+
}
|
|
32
36
|
const evidence = input.evidence || [];
|
|
33
37
|
if (!Array.isArray(evidence) || evidence.length > 20) throw new CliError('BAD_REQUEST', 'evidence must contain up to 20 text references');
|
|
34
38
|
const references = evidence.map((entry) => text(entry, 'evidence', 2000, true));
|
|
35
39
|
if (source === 'user' && status === 'passed' && !references.length) {
|
|
36
40
|
throw new CliError('BAD_REQUEST', 'A passed verification needs an evidence reference');
|
|
37
41
|
}
|
|
42
|
+
const executorId = text(input.executorId, 'executorId', 512) || null;
|
|
43
|
+
const threadId = text(input.threadId, 'threadId', 512) || null;
|
|
44
|
+
const turnId = text(input.turnId, 'turnId', 512) || null;
|
|
45
|
+
const itemId = text(input.itemId, 'itemId', 512) || null;
|
|
46
|
+
if (automatic && (!executorId || !threadId || !turnId || (kind === 'command' && !itemId))) {
|
|
47
|
+
throw new CliError('BAD_REQUEST', 'Automatic results require executor, thread, turn and command item identities');
|
|
48
|
+
}
|
|
49
|
+
const resultKey = automatic
|
|
50
|
+
? automaticResultKey({ executorId, threadId, turnId, kind, itemId: kind === 'command' ? itemId : null })
|
|
51
|
+
: input.resultKey;
|
|
38
52
|
const record = {
|
|
39
|
-
result_id: randomUUID(), result_key: text(
|
|
53
|
+
result_id: randomUUID(), result_key: text(resultKey, 'resultKey', 700) || randomUUID(),
|
|
40
54
|
peer, task_id: taskId, stage, status, kind, source,
|
|
41
|
-
title:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
55
|
+
title: automatic ? (kind === 'command' ? 'Command execution' : 'Turn changes awaiting review')
|
|
56
|
+
: text(input.title, 'title', 300, true),
|
|
57
|
+
details: automatic ? '' : text(input.details, 'details', 64000),
|
|
58
|
+
evidence: automatic ? [] : references,
|
|
59
|
+
command: automatic ? '' : text(input.command, 'command', 64000),
|
|
60
|
+
diff: automatic ? '' : text(input.diff, 'diff', 64000),
|
|
61
|
+
executor_id: executorId, thread_id: threadId, turn_id: turnId, item_id: itemId,
|
|
47
62
|
exit_code: Number.isSafeInteger(input.exitCode) ? input.exitCode : null,
|
|
48
63
|
created_at: now()
|
|
49
64
|
};
|
|
@@ -55,8 +70,10 @@ export function recordTaskResult(db, input, { now = () => Math.floor(Date.now()
|
|
|
55
70
|
AND task_id = ? AND actor = ? AND json_valid(payload) AND json_extract(payload, '$.result_key') = ?
|
|
56
71
|
ORDER BY id DESC LIMIT 1`).get(RESULT_EVENT, taskId, peer, record.result_key);
|
|
57
72
|
if (previous) return { ...JSON.parse(previous.payload), id: previous.id, created_at: previous.created_at };
|
|
58
|
-
|
|
59
|
-
|
|
73
|
+
if (!automatic) {
|
|
74
|
+
record.task_title = task.title;
|
|
75
|
+
record.task_status = task.status;
|
|
76
|
+
}
|
|
60
77
|
if (addEvent) addEvent(db, RESULT_EVENT, peer, taskId, record);
|
|
61
78
|
else db.prepare('INSERT INTO events(type,actor,task_id,payload,created_at) VALUES(?,?,?,?,?)')
|
|
62
79
|
.run(RESULT_EVENT, peer, taskId, JSON.stringify(record), record.created_at);
|