@tt-a1i/hive 1.4.3 → 1.5.0

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 (180) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.en.md +5 -4
  3. package/README.md +9 -1
  4. package/assets/qq-group.jpg +0 -0
  5. package/dist/bin/team.cmd +1 -0
  6. package/dist/src/cli/hive-update.d.ts +57 -0
  7. package/dist/src/cli/hive-update.js +92 -15
  8. package/dist/src/cli/hive.d.ts +57 -0
  9. package/dist/src/cli/hive.js +113 -20
  10. package/dist/src/cli/team.d.ts +1 -0
  11. package/dist/src/cli/team.js +215 -7
  12. package/dist/src/server/agent-command-resolver.d.ts +10 -1
  13. package/dist/src/server/agent-command-resolver.js +32 -4
  14. package/dist/src/server/agent-launch-resolver.js +9 -3
  15. package/dist/src/server/agent-manager-support.d.ts +28 -0
  16. package/dist/src/server/agent-manager-support.js +138 -10
  17. package/dist/src/server/agent-run-bootstrap.d.ts +17 -1
  18. package/dist/src/server/agent-run-bootstrap.js +30 -2
  19. package/dist/src/server/agent-run-starter.d.ts +7 -1
  20. package/dist/src/server/agent-run-starter.js +9 -2
  21. package/dist/src/server/agent-run-store.d.ts +1 -1
  22. package/dist/src/server/agent-runtime-close.d.ts +1 -0
  23. package/dist/src/server/agent-runtime-close.js +25 -1
  24. package/dist/src/server/agent-runtime-contract.d.ts +2 -1
  25. package/dist/src/server/agent-runtime.d.ts +1 -1
  26. package/dist/src/server/agent-runtime.js +8 -2
  27. package/dist/src/server/agent-startup-instructions.d.ts +8 -1
  28. package/dist/src/server/agent-startup-instructions.js +15 -9
  29. package/dist/src/server/agent-stdin-dispatcher.d.ts +12 -5
  30. package/dist/src/server/agent-stdin-dispatcher.js +129 -40
  31. package/dist/src/server/app.d.ts +1 -0
  32. package/dist/src/server/app.js +12 -2
  33. package/dist/src/server/cron-util.d.ts +7 -0
  34. package/dist/src/server/cron-util.js +19 -0
  35. package/dist/src/server/dispatch-ledger-store.d.ts +22 -0
  36. package/dist/src/server/dispatch-ledger-store.js +51 -3
  37. package/dist/src/server/env-sync-message.js +9 -9
  38. package/dist/src/server/fs-browse.d.ts +14 -1
  39. package/dist/src/server/fs-browse.js +48 -5
  40. package/dist/src/server/fs-pick-folder.js +58 -11
  41. package/dist/src/server/fs-sandbox.js +36 -7
  42. package/dist/src/server/hive-team-guidance.d.ts +11 -6
  43. package/dist/src/server/hive-team-guidance.js +252 -70
  44. package/dist/src/server/live-run-registry.d.ts +1 -0
  45. package/dist/src/server/live-run-registry.js +1 -1
  46. package/dist/src/server/open-target-commands.js +29 -4
  47. package/dist/src/server/orchestrator-autostart.d.ts +12 -0
  48. package/dist/src/server/orchestrator-autostart.js +15 -13
  49. package/dist/src/server/path-canonicalization.d.ts +3 -0
  50. package/dist/src/server/path-canonicalization.js +29 -0
  51. package/dist/src/server/platform-path.d.ts +3 -0
  52. package/dist/src/server/platform-path.js +13 -0
  53. package/dist/src/server/post-start-input-writer.d.ts +1 -1
  54. package/dist/src/server/post-start-input-writer.js +116 -16
  55. package/dist/src/server/preset-launch-support.d.ts +1 -1
  56. package/dist/src/server/preset-launch-support.js +33 -2
  57. package/dist/src/server/recovery-summary.d.ts +6 -1
  58. package/dist/src/server/recovery-summary.js +17 -17
  59. package/dist/src/server/restart-policy-support.d.ts +6 -1
  60. package/dist/src/server/restart-policy-support.js +9 -1
  61. package/dist/src/server/restart-policy.d.ts +2 -2
  62. package/dist/src/server/restart-policy.js +3 -1
  63. package/dist/src/server/role-template-store.d.ts +1 -0
  64. package/dist/src/server/role-template-store.js +11 -1
  65. package/dist/src/server/route-types.d.ts +43 -0
  66. package/dist/src/server/routes-runtime.js +2 -1
  67. package/dist/src/server/routes-settings.js +76 -0
  68. package/dist/src/server/routes-team.js +221 -2
  69. package/dist/src/server/routes-workflow-schedules.d.ts +2 -0
  70. package/dist/src/server/routes-workflow-schedules.js +58 -0
  71. package/dist/src/server/routes-workflows.d.ts +2 -0
  72. package/dist/src/server/routes-workflows.js +83 -0
  73. package/dist/src/server/routes.js +4 -0
  74. package/dist/src/server/runtime-restart-policy.d.ts +3 -1
  75. package/dist/src/server/runtime-restart-policy.js +3 -1
  76. package/dist/src/server/runtime-store-contract.d.ts +122 -0
  77. package/dist/src/server/runtime-store-contract.js +1 -0
  78. package/dist/src/server/runtime-store-helpers.d.ts +9 -0
  79. package/dist/src/server/runtime-store-helpers.js +101 -2
  80. package/dist/src/server/runtime-store-workflows.d.ts +6 -0
  81. package/dist/src/server/runtime-store-workflows.js +100 -0
  82. package/dist/src/server/runtime-store.d.ts +3 -70
  83. package/dist/src/server/runtime-store.js +70 -4
  84. package/dist/src/server/session-capture-claude.d.ts +23 -0
  85. package/dist/src/server/session-capture-claude.js +24 -1
  86. package/dist/src/server/session-capture-codex.d.ts +3 -3
  87. package/dist/src/server/session-capture-codex.js +9 -7
  88. package/dist/src/server/session-capture-gemini.d.ts +1 -1
  89. package/dist/src/server/session-capture-gemini.js +6 -3
  90. package/dist/src/server/session-capture-opencode.d.ts +18 -0
  91. package/dist/src/server/session-capture-opencode.js +27 -2
  92. package/dist/src/server/settings-store.d.ts +3 -0
  93. package/dist/src/server/settings-store.js +1 -0
  94. package/dist/src/server/sqlite-schema-v19.d.ts +2 -0
  95. package/dist/src/server/sqlite-schema-v19.js +17 -0
  96. package/dist/src/server/sqlite-schema-v20.d.ts +2 -0
  97. package/dist/src/server/sqlite-schema-v20.js +20 -0
  98. package/dist/src/server/sqlite-schema-v21.d.ts +2 -0
  99. package/dist/src/server/sqlite-schema-v21.js +20 -0
  100. package/dist/src/server/sqlite-schema.d.ts +1 -1
  101. package/dist/src/server/sqlite-schema.js +97 -1
  102. package/dist/src/server/startup-command-parser.d.ts +15 -0
  103. package/dist/src/server/startup-command-parser.js +33 -2
  104. package/dist/src/server/system-message.d.ts +7 -0
  105. package/dist/src/server/system-message.js +8 -1
  106. package/dist/src/server/tasks-file-watcher.d.ts +39 -1
  107. package/dist/src/server/tasks-file-watcher.js +155 -25
  108. package/dist/src/server/tasks-file.d.ts +2 -1
  109. package/dist/src/server/tasks-file.js +32 -9
  110. package/dist/src/server/tasks-websocket-server.js +13 -14
  111. package/dist/src/server/team-authz.d.ts +1 -1
  112. package/dist/src/server/team-authz.js +9 -1
  113. package/dist/src/server/team-autostaff.d.ts +16 -0
  114. package/dist/src/server/team-autostaff.js +16 -0
  115. package/dist/src/server/team-list-serializer.d.ts +1 -1
  116. package/dist/src/server/team-list-serializer.js +3 -1
  117. package/dist/src/server/team-operations.d.ts +20 -2
  118. package/dist/src/server/team-operations.js +160 -14
  119. package/dist/src/server/terminal-input-profile.js +2 -8
  120. package/dist/src/server/terminal-protocol.js +9 -3
  121. package/dist/src/server/terminal-stream-hub.js +16 -10
  122. package/dist/src/server/terminal-ws-server.js +36 -16
  123. package/dist/src/server/websocket-upgrade-safety.d.ts +10 -0
  124. package/dist/src/server/websocket-upgrade-safety.js +35 -0
  125. package/dist/src/server/windows-command-line.d.ts +3 -0
  126. package/dist/src/server/windows-command-line.js +9 -0
  127. package/dist/src/server/windows-filename.d.ts +2 -0
  128. package/dist/src/server/windows-filename.js +33 -0
  129. package/dist/src/server/workflow-cli-policy.d.ts +60 -0
  130. package/dist/src/server/workflow-cli-policy.js +110 -0
  131. package/dist/src/server/workflow-dispatch-awaiter.d.ts +12 -0
  132. package/dist/src/server/workflow-dispatch-awaiter.js +80 -0
  133. package/dist/src/server/workflow-feature.d.ts +15 -0
  134. package/dist/src/server/workflow-feature.js +15 -0
  135. package/dist/src/server/workflow-http-serializers.d.ts +64 -0
  136. package/dist/src/server/workflow-http-serializers.js +58 -0
  137. package/dist/src/server/workflow-run-log-store.d.ts +19 -0
  138. package/dist/src/server/workflow-run-log-store.js +45 -0
  139. package/dist/src/server/workflow-run-store.d.ts +50 -0
  140. package/dist/src/server/workflow-run-store.js +103 -0
  141. package/dist/src/server/workflow-runner.d.ts +147 -0
  142. package/dist/src/server/workflow-runner.js +401 -0
  143. package/dist/src/server/workflow-schedule-create.d.ts +14 -0
  144. package/dist/src/server/workflow-schedule-create.js +41 -0
  145. package/dist/src/server/workflow-schedule-store.d.ts +43 -0
  146. package/dist/src/server/workflow-schedule-store.js +112 -0
  147. package/dist/src/server/workflow-scheduler.d.ts +36 -0
  148. package/dist/src/server/workflow-scheduler.js +97 -0
  149. package/dist/src/server/workflow-script-loader.d.ts +34 -0
  150. package/dist/src/server/workflow-script-loader.js +106 -0
  151. package/dist/src/server/workspace-path-validation.js +16 -4
  152. package/dist/src/server/workspace-shell-runtime.d.ts +5 -0
  153. package/dist/src/server/workspace-shell-runtime.js +24 -2
  154. package/dist/src/server/workspace-store-contract.d.ts +4 -1
  155. package/dist/src/server/workspace-store-hydration.js +23 -7
  156. package/dist/src/server/workspace-store-mutations.js +2 -5
  157. package/dist/src/server/workspace-store-support.d.ts +4 -0
  158. package/dist/src/server/workspace-store-support.js +13 -1
  159. package/dist/src/server/workspace-store.js +38 -4
  160. package/dist/src/shared/types.d.ts +16 -1
  161. package/package.json +4 -2
  162. package/web/dist/assets/{AddWorkerDialog-DmkDOdp6.js → AddWorkerDialog-CcC-7kgG.js} +2 -2
  163. package/web/dist/assets/AddWorkspaceDialog-BDpOTfmt.js +1 -0
  164. package/web/dist/assets/{FirstRunWizard-SAd1wsH4.js → FirstRunWizard-BYX_ocQn.js} +1 -1
  165. package/web/dist/assets/{MarketplaceDrawer-B_8aG2uT.js → MarketplaceDrawer-DUxSk7db.js} +1 -1
  166. package/web/dist/assets/WhatsNewDialog-B_RlCXcV.js +1 -0
  167. package/web/dist/assets/WorkerModal-D9-7YfZZ.js +1 -0
  168. package/web/dist/assets/WorkspaceTaskDrawer-BCKoF7qc.js +1 -0
  169. package/web/dist/assets/{WorkspaceTerminalPanels-BReWh1YL.js → WorkspaceTerminalPanels-Dq8y91t2.js} +1 -1
  170. package/web/dist/assets/index-BiOvKIVw.css +1 -0
  171. package/web/dist/assets/index-DMRUklT3.js +73 -0
  172. package/web/dist/assets/path-join-7MR1s7b1.js +1 -0
  173. package/web/dist/index.html +2 -2
  174. package/web/dist/sw.js +1 -1
  175. package/web/dist/assets/AddWorkspaceDialog-BsVnH3Xe.js +0 -1
  176. package/web/dist/assets/WorkerModal-CQmjiPme.js +0 -1
  177. package/web/dist/assets/WorkspaceTaskDrawer-B0DmCWcV.js +0 -1
  178. package/web/dist/assets/chevron-right-CtLjVEl7.js +0 -1
  179. package/web/dist/assets/index-BEsTmfrO.css +0 -1
  180. package/web/dist/assets/index-Cn8X3get.js +0 -76
@@ -4,8 +4,33 @@ import { join } from 'node:path';
4
4
  import Database from 'better-sqlite3';
5
5
  import { captureSessionIdWithCoordinator } from './claude-session-coordinator.js';
6
6
  const expandHome = (path) => path === '~' || path.startsWith('~/') ? join(homedir(), path.slice(2)) : path;
7
- const getDefaultOpenCodeDbPath = () => process.env.HIVE_OPENCODE_DB_PATH ??
8
- join(process.env.XDG_DATA_HOME ?? join(homedir(), '.local', 'share'), 'opencode', 'opencode.db');
7
+ /**
8
+ * Resolve the path OpenCode upstream writes `opencode.db` to. Branches
9
+ * by platform because XDG_DATA_HOME is not a Windows convention — the
10
+ * previous unconditional ~/.local/share/opencode/opencode.db default
11
+ * pointed at a path that never exists on Windows, silently breaking
12
+ * Layer A native session resume for OpenCode workers there.
13
+ *
14
+ * Resolution order:
15
+ * 1. HIVE_OPENCODE_DB_PATH override (any platform, for tests/users).
16
+ * 2. Windows: %LOCALAPPDATA%\opencode\opencode.db (with a homedir
17
+ * fallback when LOCALAPPDATA is missing — some shells strip env).
18
+ * 3. POSIX: $XDG_DATA_HOME/opencode/opencode.db, falling back to
19
+ * ~/.local/share/opencode/opencode.db.
20
+ *
21
+ * Exported so the path-resolution rules can be unit-tested without
22
+ * needing to mock the underlying SQLite open.
23
+ */
24
+ export const getDefaultOpenCodeDbPath = (platform = process.platform) => {
25
+ const override = process.env.HIVE_OPENCODE_DB_PATH;
26
+ if (override)
27
+ return override;
28
+ if (platform === 'win32') {
29
+ const localAppData = process.env.LOCALAPPDATA ?? join(homedir(), 'AppData', 'Local');
30
+ return join(localAppData, 'opencode', 'opencode.db');
31
+ }
32
+ return join(process.env.XDG_DATA_HOME ?? join(homedir(), '.local', 'share'), 'opencode', 'opencode.db');
33
+ };
9
34
  export const getOpenCodeDbPath = (pattern) => pattern === '~/.local/share/opencode/opencode.db' || !pattern
10
35
  ? getDefaultOpenCodeDbPath()
11
36
  : expandHome(pattern);
@@ -7,6 +7,9 @@ export interface SettingsStore {
7
7
  createRoleTemplate: (input: RoleTemplateInput) => RoleTemplateRecord;
8
8
  deleteCommandPreset: (id: string) => void;
9
9
  deleteRoleTemplate: (id: string) => void;
10
+ /** TIER 2 #4 — workflow runner uses this to resolve a non-built-in
11
+ * agentType against the user's curated role library. */
12
+ findRoleTemplateByName: (name: string) => RoleTemplateRecord | undefined;
10
13
  getAppState: (key: string) => AppStateRecord | undefined;
11
14
  getCommandPreset: (id: string) => CommandPresetRecord | undefined;
12
15
  listCommandPresets: () => CommandPresetRecord[];
@@ -10,6 +10,7 @@ export const createSettingsStore = (db) => {
10
10
  createRoleTemplate: roleTemplateStore.create,
11
11
  deleteCommandPreset: commandPresetStore.remove,
12
12
  deleteRoleTemplate: roleTemplateStore.remove,
13
+ findRoleTemplateByName: roleTemplateStore.findByName,
13
14
  getAppState: appStateStore.get,
14
15
  getCommandPreset: commandPresetStore.get,
15
16
  listCommandPresets: commandPresetStore.list,
@@ -0,0 +1,2 @@
1
+ import type { Database } from 'better-sqlite3';
2
+ export declare const applySchemaVersion19: (db: Database) => void;
@@ -0,0 +1,17 @@
1
+ export const applySchemaVersion19 = (db) => {
2
+ const workerColumns = new Set(db.prepare('PRAGMA table_info(workers)').all().map((c) => c.name));
3
+ if (!workerColumns.has('ephemeral')) {
4
+ db.exec('ALTER TABLE workers ADD COLUMN ephemeral INTEGER NOT NULL DEFAULT 0');
5
+ }
6
+ if (!workerColumns.has('spawned_by')) {
7
+ db.exec('ALTER TABLE workers ADD COLUMN spawned_by TEXT');
8
+ }
9
+ const dispatchColumns = new Set(db.prepare('PRAGMA table_info(dispatches)').all().map((c) => c.name));
10
+ if (!dispatchColumns.has('workflow_run_id')) {
11
+ db.exec('ALTER TABLE dispatches ADD COLUMN workflow_run_id TEXT');
12
+ }
13
+ if (!dispatchColumns.has('step_index')) {
14
+ db.exec('ALTER TABLE dispatches ADD COLUMN step_index INTEGER');
15
+ }
16
+ db.exec('CREATE INDEX IF NOT EXISTS idx_dispatches_workflow ON dispatches (workflow_run_id, step_index)');
17
+ };
@@ -0,0 +1,2 @@
1
+ import type { Database } from 'better-sqlite3';
2
+ export declare const applySchemaVersion20: (db: Database) => void;
@@ -0,0 +1,20 @@
1
+ export const applySchemaVersion20 = (db) => {
2
+ db.exec(`
3
+ CREATE TABLE IF NOT EXISTS workflow_runs (
4
+ id TEXT PRIMARY KEY,
5
+ workspace_id TEXT NOT NULL,
6
+ script_path TEXT NOT NULL,
7
+ script_hash TEXT,
8
+ name TEXT NOT NULL,
9
+ status TEXT NOT NULL,
10
+ phase TEXT,
11
+ args TEXT,
12
+ started_at INTEGER NOT NULL,
13
+ finished_at INTEGER,
14
+ error TEXT,
15
+ created_at INTEGER NOT NULL
16
+ );
17
+ CREATE INDEX IF NOT EXISTS idx_workflow_runs_workspace
18
+ ON workflow_runs (workspace_id, created_at);
19
+ `);
20
+ };
@@ -0,0 +1,2 @@
1
+ import type { Database } from 'better-sqlite3';
2
+ export declare const applySchemaVersion21: (db: Database) => void;
@@ -0,0 +1,20 @@
1
+ export const applySchemaVersion21 = (db) => {
2
+ db.exec(`
3
+ CREATE TABLE IF NOT EXISTS workflow_schedules (
4
+ id TEXT PRIMARY KEY,
5
+ workspace_id TEXT NOT NULL,
6
+ script_path TEXT NOT NULL,
7
+ cron TEXT NOT NULL,
8
+ args TEXT,
9
+ enabled INTEGER NOT NULL DEFAULT 1,
10
+ last_run_at INTEGER,
11
+ next_run_at INTEGER NOT NULL,
12
+ created_at INTEGER NOT NULL,
13
+ updated_at INTEGER NOT NULL
14
+ );
15
+ CREATE INDEX IF NOT EXISTS idx_workflow_schedules_due
16
+ ON workflow_schedules (enabled, next_run_at);
17
+ CREATE INDEX IF NOT EXISTS idx_workflow_schedules_workspace
18
+ ON workflow_schedules (workspace_id, created_at);
19
+ `);
20
+ };
@@ -1,3 +1,3 @@
1
1
  import type { Database } from 'better-sqlite3';
2
- export declare const CURRENT_SCHEMA_VERSION = 18;
2
+ export declare const CURRENT_SCHEMA_VERSION = 21;
3
3
  export declare const initializeRuntimeDatabase: (db: Database) => void;
@@ -11,7 +11,19 @@ import { applySchemaVersion15 } from './sqlite-schema-v15.js';
11
11
  import { applySchemaVersion16 } from './sqlite-schema-v16.js';
12
12
  import { applySchemaVersion17 } from './sqlite-schema-v17.js';
13
13
  import { applySchemaVersion18 } from './sqlite-schema-v18.js';
14
- export const CURRENT_SCHEMA_VERSION = 18;
14
+ import { applySchemaVersion19 } from './sqlite-schema-v19.js';
15
+ import { applySchemaVersion20 } from './sqlite-schema-v20.js';
16
+ import { applySchemaVersion21 } from './sqlite-schema-v21.js';
17
+ export const CURRENT_SCHEMA_VERSION = 21;
18
+ // Idempotent column-add helper. SQLite doesn't have `ALTER TABLE … ADD COLUMN
19
+ // IF NOT EXISTS`, so PRAGMA-check first. Safe to call on every init; required
20
+ // for foreign-built DBs where the version-gated migration is skipped.
21
+ const ensureColumn = (db, table, column, definition) => {
22
+ const present = new Set(db.prepare(`PRAGMA table_info(${table})`).all().map((r) => r.name));
23
+ if (!present.has(column)) {
24
+ db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
25
+ }
26
+ };
15
27
  export const initializeRuntimeDatabase = (db) => {
16
28
  db.exec(`
17
29
  CREATE TABLE IF NOT EXISTS schema_version (
@@ -105,7 +117,79 @@ export const initializeRuntimeDatabase = (db) => {
105
117
 
106
118
  CREATE INDEX IF NOT EXISTS idx_dispatches_open_by_worker
107
119
  ON dispatches (workspace_id, to_agent_id, status, sequence);
120
+
121
+ CREATE TABLE IF NOT EXISTS workflow_runs (
122
+ id TEXT PRIMARY KEY,
123
+ workspace_id TEXT NOT NULL,
124
+ script_path TEXT NOT NULL,
125
+ script_hash TEXT,
126
+ name TEXT NOT NULL,
127
+ status TEXT NOT NULL,
128
+ phase TEXT,
129
+ args TEXT,
130
+ started_at INTEGER NOT NULL,
131
+ finished_at INTEGER,
132
+ error TEXT,
133
+ created_at INTEGER NOT NULL
134
+ );
135
+
136
+ CREATE INDEX IF NOT EXISTS idx_workflow_runs_workspace
137
+ ON workflow_runs (workspace_id, created_at);
138
+
139
+ CREATE TABLE IF NOT EXISTS workflow_schedules (
140
+ id TEXT PRIMARY KEY,
141
+ workspace_id TEXT NOT NULL,
142
+ script_path TEXT NOT NULL,
143
+ cron TEXT NOT NULL,
144
+ args TEXT,
145
+ enabled INTEGER NOT NULL DEFAULT 1,
146
+ last_run_at INTEGER,
147
+ next_run_at INTEGER NOT NULL,
148
+ created_at INTEGER NOT NULL,
149
+ updated_at INTEGER NOT NULL
150
+ );
151
+
152
+ CREATE INDEX IF NOT EXISTS idx_workflow_schedules_due
153
+ ON workflow_schedules (enabled, next_run_at);
154
+ CREATE INDEX IF NOT EXISTS idx_workflow_schedules_workspace
155
+ ON workflow_schedules (workspace_id, created_at);
108
156
  `);
157
+ // Idempotent column additions — run on every init regardless of
158
+ // schema_version. The v19 migration adds these columns but is gated on
159
+ // !appliedVersions.has(19); a foreign-built DB whose v19 was a DIFFERENT
160
+ // migration leaves these absent. PRAGMA + ALTER here makes the writes safe.
161
+ ensureColumn(db, 'workers', 'ephemeral', 'INTEGER NOT NULL DEFAULT 0');
162
+ ensureColumn(db, 'workers', 'spawned_by', 'TEXT');
163
+ ensureColumn(db, 'dispatches', 'workflow_run_id', 'TEXT');
164
+ ensureColumn(db, 'dispatches', 'step_index', 'INTEGER');
165
+ // M9 — phase + label on dispatches so the workflow UI can render the
166
+ // phase tree + agent fleet view (mirrors Claude Code's /workflows view).
167
+ ensureColumn(db, 'dispatches', 'phase', 'TEXT');
168
+ ensureColumn(db, 'dispatches', 'label', 'TEXT');
169
+ // M10: capture the workflow script's `return` value so the UI can render
170
+ // a single canonical "Result" panel and the orchestrator notification can
171
+ // include it.
172
+ ensureColumn(db, 'workflow_runs', 'result', 'TEXT');
173
+ // TIER 2 #5: parent_run_id lets the Drawer render nested workflow() calls
174
+ // as a tree (child runs indented under their parent). Without it,
175
+ // nested runs were flat and the user couldn't tell which child
176
+ // belonged to which parent. Null on top-level runs.
177
+ ensureColumn(db, 'workflow_runs', 'parent_run_id', 'TEXT');
178
+ // TIER 2 #3: log() narrator pipeline. Authors call `log('Discovered 47
179
+ // endpoints')` from a workflow script; rows live here and stream to
180
+ // the Drawer's narrator lane + the last few lines are appended to
181
+ // the orchestrator's completion notification.
182
+ db.exec(`
183
+ CREATE TABLE IF NOT EXISTS workflow_run_logs (
184
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
185
+ run_id TEXT NOT NULL,
186
+ ts INTEGER NOT NULL,
187
+ message TEXT NOT NULL
188
+ );
189
+ CREATE INDEX IF NOT EXISTS idx_workflow_run_logs_run
190
+ ON workflow_run_logs (run_id, id);
191
+ `);
192
+ db.exec('CREATE INDEX IF NOT EXISTS idx_dispatches_workflow ON dispatches (workflow_run_id, step_index)');
109
193
  const versions = db
110
194
  .prepare('SELECT version FROM schema_version ORDER BY version ASC')
111
195
  .all();
@@ -203,4 +287,16 @@ export const initializeRuntimeDatabase = (db) => {
203
287
  applySchemaVersion18(db);
204
288
  db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)').run(18, Date.now());
205
289
  }
290
+ if (!appliedVersions.has(19)) {
291
+ applySchemaVersion19(db);
292
+ db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)').run(19, Date.now());
293
+ }
294
+ if (!appliedVersions.has(20)) {
295
+ applySchemaVersion20(db);
296
+ db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)').run(20, Date.now());
297
+ }
298
+ if (!appliedVersions.has(21)) {
299
+ applySchemaVersion21(db);
300
+ db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)').run(21, Date.now());
301
+ }
206
302
  };
@@ -3,3 +3,18 @@ export declare const createStartupCommandLaunch: (startupCommand: string, env?:
3
3
  command: string;
4
4
  };
5
5
  export declare const getStartupCommandExecutable: (startupCommand: string) => string | null;
6
+ /**
7
+ * Reduce an executable token to its canonical brand id for preset lookup
8
+ * and CLI-behavior dispatch. Splits on both '\\' and '/' so Windows paths
9
+ * normalize even when the host runs `node:path.posix` (macOS test runners,
10
+ * mixed-slash paths from npm-installed shims).
11
+ *
12
+ * Examples (return value in parens):
13
+ * 'claude' → 'claude'
14
+ * 'claude.CMD' → 'claude'
15
+ * 'C:\\Program Files\\nodejs\\claude.cmd' → 'claude'
16
+ * '/usr/local/bin/codex' → 'codex'
17
+ * 'C:/Users/me/opencode.CMD' → 'opencode'
18
+ * 'my-custom-runner.bat' → 'my-custom-runner'
19
+ */
20
+ export declare const normalizeExecutableToken: (token: string | null | undefined) => string | null;
@@ -32,6 +32,37 @@ export const getStartupCommandExecutable = (startupCommand) => {
32
32
  const command = startupCommand.trim();
33
33
  if (!command)
34
34
  return null;
35
- const match = /^(['"]?)([^'"\s]+)\1/.exec(command);
36
- return match?.[2] ?? null;
35
+ // Match alternates, in order:
36
+ // 1. Double-quoted token (may contain spaces, backslashes, slashes)
37
+ // 2. Single-quoted token
38
+ // 3. Bare token: no whitespace, no embedded quotes
39
+ // An unbalanced opening quote falls through all three and returns null.
40
+ // The previous regex used `[^'"\s]+` for the captured run, which forbade
41
+ // spaces and therefore returned null for `"C:\Program Files\…claude.cmd"`
42
+ // — the standard Windows install layout. See the parser tests for the
43
+ // contract; brand identification belongs in agent-launch-resolver.
44
+ const match = /^"([^"]+)"|^'([^']+)'|^([^\s'"]+)/.exec(command);
45
+ return match?.[1] ?? match?.[2] ?? match?.[3] ?? null;
46
+ };
47
+ /**
48
+ * Reduce an executable token to its canonical brand id for preset lookup
49
+ * and CLI-behavior dispatch. Splits on both '\\' and '/' so Windows paths
50
+ * normalize even when the host runs `node:path.posix` (macOS test runners,
51
+ * mixed-slash paths from npm-installed shims).
52
+ *
53
+ * Examples (return value in parens):
54
+ * 'claude' → 'claude'
55
+ * 'claude.CMD' → 'claude'
56
+ * 'C:\\Program Files\\nodejs\\claude.cmd' → 'claude'
57
+ * '/usr/local/bin/codex' → 'codex'
58
+ * 'C:/Users/me/opencode.CMD' → 'opencode'
59
+ * 'my-custom-runner.bat' → 'my-custom-runner'
60
+ */
61
+ export const normalizeExecutableToken = (token) => {
62
+ if (!token)
63
+ return null;
64
+ const lastSegment = token.replace(/^.*[\\/]/, '');
65
+ if (!lastSegment)
66
+ return null;
67
+ return lastSegment.toLowerCase().replace(/\.(cmd|bat|exe|ps1)$/u, '');
37
68
  };
@@ -1 +1,8 @@
1
+ /**
2
+ * Wraps a system-injected message body in an out-of-band XML envelope so the
3
+ * agent attends to it as system content rather than mistaking it for user
4
+ * input or its own output. `<hive-system-reminder>` is reserved for the short
5
+ * re-anchoring action menu appended at a message tail; this `<hive-system-message>`
6
+ * tag carries larger injected bodies (crash-recovery / restart env-sync).
7
+ */
1
8
  export declare const wrapSystemMessage: (content: string) => string;
@@ -1 +1,8 @@
1
- export const wrapSystemMessage = (content) => `[Hive 系统消息:${content}]`;
1
+ /**
2
+ * Wraps a system-injected message body in an out-of-band XML envelope so the
3
+ * agent attends to it as system content rather than mistaking it for user
4
+ * input or its own output. `<hive-system-reminder>` is reserved for the short
5
+ * re-anchoring action menu appended at a message tail; this `<hive-system-message>`
6
+ * tag carries larger injected bodies (crash-recovery / restart env-sync).
7
+ */
8
+ export const wrapSystemMessage = (content) => `<hive-system-message>\n${content}\n</hive-system-message>`;
@@ -1,8 +1,46 @@
1
+ import { type ChokidarOptions } from 'chokidar';
2
+ import type { WorkflowCliPolicy } from './workflow-cli-policy.js';
3
+ /**
4
+ * Watcher configuration. The atomic-save option matters on Windows: VS
5
+ * Code, Cursor, Notepad++, and the editor inside Hive itself all save
6
+ * by writing a temp file and renaming it over the target. On
7
+ * ReadDirectoryChangesW (Windows' native fs-event backend) that rename
8
+ * invalidates the file-handle the chokidar watcher held for the
9
+ * single-file path, and subsequent edits would emit no events at all.
10
+ *
11
+ * `atomic: 100` tells chokidar to correlate an unlink+add pair within
12
+ * a 100ms window as a single `change` event — collapsing the rename
13
+ * into one logical "the file was modified" notification and rebinding
14
+ * the underlying watch handle. Without it the tasks panel goes deaf
15
+ * to changes the user makes outside the app after the first save.
16
+ *
17
+ * We intentionally do NOT set `awaitWriteFinish` here. It would help
18
+ * even more on Windows (waits for the file size to stabilise before
19
+ * emitting), but it also throttles every emission by `stabilityThreshold`
20
+ * — clashing with continuous-write workflows (a worker streaming
21
+ * updates into tasks.md every 100ms would never see an emit). The
22
+ * `atomic` option alone covers atomic-save; if a future workspace
23
+ * needs stronger settling behaviour we can promote it then.
24
+ *
25
+ * Exported so the configuration is testable in isolation.
26
+ */
27
+ export declare const TASKS_WATCHER_OPTIONS: ChokidarOptions;
28
+ export declare const buildTasksWatcherOptions: (workspacePath: string, platform?: NodeJS.Platform) => ChokidarOptions;
1
29
  export interface TasksFileWatcher {
2
30
  close: () => Promise<void>;
3
31
  start: (workspaceId: string, workspacePath: string) => Promise<void>;
4
32
  stop: (workspaceId: string) => Promise<void>;
5
33
  }
6
- export declare const createTasksFileWatcher: ({ onTasksUpdated, }: {
34
+ export declare const createTasksFileWatcher: ({ onTasksUpdated, getWorkflowCliPolicy, getWorkflowsEnabled, getAutostaffEnabled, }: {
7
35
  onTasksUpdated: (workspaceId: string, content: string) => void;
36
+ /** Lets the freshly-written `.hive/PROTOCOL.md` state the workspace's
37
+ * workflow CLI default + allowlist. Optional: omitted → the doc renders
38
+ * the unrestricted default. */
39
+ getWorkflowCliPolicy?: () => WorkflowCliPolicy;
40
+ /** Whether the experimental workflow feature is on. Off → PROTOCOL.md omits
41
+ * the workflow DSL + `team workflow` commands entirely. Defaults to off. */
42
+ getWorkflowsEnabled?: () => boolean;
43
+ /** Whether the experimental auto-staff feature is on (default on). Off →
44
+ * PROTOCOL.md omits the team-sizing rule. */
45
+ getAutostaffEnabled?: () => boolean;
8
46
  }) => TasksFileWatcher;
@@ -1,11 +1,79 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { readFile } from 'node:fs/promises';
3
+ import { basename, dirname, normalize } from 'node:path';
3
4
  import chokidar from 'chokidar';
4
- import { ensureProtocolFile, ensureTasksFile, getTasksFilePath } from './tasks-file.js';
5
+ import { ensureProtocolFile, ensureTasksFile, getTasksFilePath, TASKS_FILE_NAME, } from './tasks-file.js';
5
6
  const DEBOUNCE_MS = 100;
6
- export const createTasksFileWatcher = ({ onTasksUpdated, }) => {
7
+ const WATCHER_RETRY_MS = 5000;
8
+ const WATCHER_CLOSE_TIMEOUT_MS = 2000;
9
+ const WATCHER_READY_TIMEOUT_MS = 15000;
10
+ /**
11
+ * Watcher configuration. The atomic-save option matters on Windows: VS
12
+ * Code, Cursor, Notepad++, and the editor inside Hive itself all save
13
+ * by writing a temp file and renaming it over the target. On
14
+ * ReadDirectoryChangesW (Windows' native fs-event backend) that rename
15
+ * invalidates the file-handle the chokidar watcher held for the
16
+ * single-file path, and subsequent edits would emit no events at all.
17
+ *
18
+ * `atomic: 100` tells chokidar to correlate an unlink+add pair within
19
+ * a 100ms window as a single `change` event — collapsing the rename
20
+ * into one logical "the file was modified" notification and rebinding
21
+ * the underlying watch handle. Without it the tasks panel goes deaf
22
+ * to changes the user makes outside the app after the first save.
23
+ *
24
+ * We intentionally do NOT set `awaitWriteFinish` here. It would help
25
+ * even more on Windows (waits for the file size to stabilise before
26
+ * emitting), but it also throttles every emission by `stabilityThreshold`
27
+ * — clashing with continuous-write workflows (a worker streaming
28
+ * updates into tasks.md every 100ms would never see an emit). The
29
+ * `atomic` option alone covers atomic-save; if a future workspace
30
+ * needs stronger settling behaviour we can promote it then.
31
+ *
32
+ * Exported so the configuration is testable in isolation.
33
+ */
34
+ export const TASKS_WATCHER_OPTIONS = {
35
+ atomic: 100,
36
+ ignoreInitial: true,
37
+ };
38
+ const isWindowsUncPath = (path, platform = process.platform) => platform === 'win32' && /^[\\/]{2}[^\\/]+[\\/]+[^\\/]+/u.test(path);
39
+ export const buildTasksWatcherOptions = (workspacePath, platform = process.platform) => ({
40
+ ...TASKS_WATCHER_OPTIONS,
41
+ ...(platform === 'win32' || isWindowsUncPath(workspacePath, platform)
42
+ ? { interval: 500, usePolling: true }
43
+ : {}),
44
+ });
45
+ const closeWatcherWithTimeout = async (watcher) => {
46
+ if (!watcher)
47
+ return;
48
+ let timer;
49
+ try {
50
+ await Promise.race([
51
+ watcher.close(),
52
+ new Promise((resolve) => {
53
+ timer = setTimeout(resolve, WATCHER_CLOSE_TIMEOUT_MS);
54
+ timer.unref?.();
55
+ }),
56
+ ]);
57
+ }
58
+ finally {
59
+ if (timer)
60
+ clearTimeout(timer);
61
+ }
62
+ };
63
+ const isTasksFileEvent = (tasksPath, changedPath) => {
64
+ if (!changedPath)
65
+ return true;
66
+ const text = Buffer.isBuffer(changedPath) ? changedPath.toString() : changedPath;
67
+ return normalize(text) === normalize(tasksPath) || basename(text) === TASKS_FILE_NAME;
68
+ };
69
+ export const createTasksFileWatcher = ({ onTasksUpdated, getWorkflowCliPolicy, getWorkflowsEnabled, getAutostaffEnabled, }) => {
7
70
  const watchers = new Map();
8
71
  const timers = new Map();
72
+ const retryTimers = new Map();
73
+ let closed = false;
74
+ const logWatcherError = (workspaceId, error) => {
75
+ console.error(`[hive] tasks watcher error for workspace ${workspaceId}`, error);
76
+ };
9
77
  const clearTimer = (workspaceId) => {
10
78
  const timer = timers.get(workspaceId);
11
79
  if (!timer)
@@ -13,6 +81,13 @@ export const createTasksFileWatcher = ({ onTasksUpdated, }) => {
13
81
  clearTimeout(timer);
14
82
  timers.delete(workspaceId);
15
83
  };
84
+ const clearRetryTimer = (workspaceId) => {
85
+ const timer = retryTimers.get(workspaceId);
86
+ if (!timer)
87
+ return;
88
+ clearTimeout(timer);
89
+ retryTimers.delete(workspaceId);
90
+ };
16
91
  const emitCurrentContent = async (workspaceId, workspacePath) => {
17
92
  const tasksPath = getTasksFilePath(workspacePath);
18
93
  try {
@@ -20,41 +95,96 @@ export const createTasksFileWatcher = ({ onTasksUpdated, }) => {
20
95
  onTasksUpdated(workspaceId, content);
21
96
  }
22
97
  catch (error) {
23
- if (error.code !== 'ENOENT')
24
- throw error;
98
+ if (error.code !== 'ENOENT') {
99
+ logWatcherError(workspaceId, error);
100
+ return;
101
+ }
25
102
  onTasksUpdated(workspaceId, '');
26
103
  }
27
104
  };
28
105
  const stop = async (workspaceId) => {
29
106
  clearTimer(workspaceId);
107
+ clearRetryTimer(workspaceId);
30
108
  const watcher = watchers.get(workspaceId);
31
109
  watchers.delete(workspaceId);
32
- await watcher?.close();
110
+ await closeWatcherWithTimeout(watcher);
111
+ };
112
+ const scheduleRetry = (workspaceId, workspacePath) => {
113
+ if (closed || retryTimers.has(workspaceId))
114
+ return;
115
+ const timer = setTimeout(() => {
116
+ retryTimers.delete(workspaceId);
117
+ void start(workspaceId, workspacePath).catch((error) => logWatcherError(workspaceId, error));
118
+ }, WATCHER_RETRY_MS);
119
+ timer.unref?.();
120
+ retryTimers.set(workspaceId, timer);
121
+ };
122
+ const waitForReady = async (watcher) => await new Promise((resolve, reject) => {
123
+ const cleanup = () => {
124
+ watcher.off('ready', handleReady);
125
+ watcher.off('error', handleError);
126
+ clearTimeout(timeout);
127
+ };
128
+ const handleReady = () => {
129
+ cleanup();
130
+ resolve();
131
+ };
132
+ const handleError = (error) => {
133
+ cleanup();
134
+ reject(error);
135
+ };
136
+ const timeout = setTimeout(() => {
137
+ cleanup();
138
+ reject(new Error(`Timed out waiting for tasks watcher ready after ${WATCHER_READY_TIMEOUT_MS}ms`));
139
+ }, WATCHER_READY_TIMEOUT_MS);
140
+ timeout.unref?.();
141
+ watcher.once('ready', handleReady);
142
+ watcher.once('error', handleError);
143
+ });
144
+ const start = async (workspaceId, workspacePath) => {
145
+ closed = false;
146
+ await stop(workspaceId);
147
+ ensureTasksFile(workspacePath);
148
+ ensureProtocolFile(workspacePath, getWorkflowCliPolicy?.(), getWorkflowsEnabled?.() ?? false, getAutostaffEnabled?.() ?? false);
149
+ const tasksPath = getTasksFilePath(workspacePath);
150
+ const watcher = chokidar.watch(dirname(tasksPath), buildTasksWatcherOptions(workspacePath));
151
+ const scheduleEmit = (changedPath) => {
152
+ if (!isTasksFileEvent(tasksPath, changedPath))
153
+ return;
154
+ clearTimer(workspaceId);
155
+ timers.set(workspaceId, setTimeout(() => {
156
+ timers.delete(workspaceId);
157
+ void emitCurrentContent(workspaceId, workspacePath);
158
+ }, DEBOUNCE_MS));
159
+ };
160
+ watcher.on('add', scheduleEmit);
161
+ watcher.on('change', scheduleEmit);
162
+ watcher.on('unlink', scheduleEmit);
163
+ watcher.on('error', (error) => {
164
+ logWatcherError(workspaceId, error);
165
+ void stop(workspaceId)
166
+ .catch((closeError) => logWatcherError(workspaceId, closeError))
167
+ .finally(() => scheduleRetry(workspaceId, workspacePath));
168
+ });
169
+ watchers.set(workspaceId, watcher);
170
+ try {
171
+ await waitForReady(watcher);
172
+ }
173
+ catch (error) {
174
+ watchers.delete(workspaceId);
175
+ await closeWatcherWithTimeout(watcher);
176
+ scheduleRetry(workspaceId, workspacePath);
177
+ throw error;
178
+ }
33
179
  };
34
180
  return {
35
181
  close: async () => {
182
+ closed = true;
183
+ for (const workspaceId of retryTimers.keys())
184
+ clearRetryTimer(workspaceId);
36
185
  await Promise.all(Array.from(watchers.keys(), (workspaceId) => stop(workspaceId)));
37
186
  },
38
- start: async (workspaceId, workspacePath) => {
39
- await stop(workspaceId);
40
- ensureTasksFile(workspacePath);
41
- ensureProtocolFile(workspacePath);
42
- const watcher = chokidar.watch(getTasksFilePath(workspacePath), {
43
- ignoreInitial: true,
44
- });
45
- const scheduleEmit = () => {
46
- clearTimer(workspaceId);
47
- timers.set(workspaceId, setTimeout(() => {
48
- timers.delete(workspaceId);
49
- void emitCurrentContent(workspaceId, workspacePath);
50
- }, DEBOUNCE_MS));
51
- };
52
- watcher.on('add', scheduleEmit);
53
- watcher.on('change', scheduleEmit);
54
- watcher.on('unlink', scheduleEmit);
55
- watchers.set(workspaceId, watcher);
56
- await new Promise((resolve) => watcher.once('ready', () => resolve()));
57
- },
187
+ start,
58
188
  stop,
59
189
  };
60
190
  };
@@ -1,3 +1,4 @@
1
+ import type { WorkflowCliPolicy } from './workflow-cli-policy.js';
1
2
  interface TasksFileService {
2
3
  readTasks: (workspacePath: string) => string;
3
4
  writeTasks: (workspacePath: string, content: string) => void;
@@ -16,6 +17,6 @@ export declare const ensureTasksFile: (workspacePath: string) => string;
16
17
  * on every workspace open means a Hive version bump that changes the rules
17
18
  * propagates without manual intervention.
18
19
  */
19
- export declare const ensureProtocolFile: (workspacePath: string) => string;
20
+ export declare const ensureProtocolFile: (workspacePath: string, cliPolicy?: WorkflowCliPolicy, workflowsEnabled?: boolean, autostaffEnabled?: boolean) => string;
20
21
  export declare const createTasksFileService: () => TasksFileService;
21
22
  export type { TasksFileService };