@tt-a1i/hive 2.0.2 → 2.1.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.
- package/CHANGELOG.md +33 -0
- package/README.en.md +15 -6
- package/README.md +26 -4
- package/dist/src/cli/hive.d.ts +4 -0
- package/dist/src/cli/hive.js +25 -3
- package/dist/src/cli/team.d.ts +8 -1
- package/dist/src/cli/team.js +111 -11
- package/dist/src/server/action-center-summary.d.ts +193 -0
- package/dist/src/server/action-center-summary.js +188 -0
- package/dist/src/server/agent-command-resolver.d.ts +6 -0
- package/dist/src/server/agent-command-resolver.js +16 -0
- package/dist/src/server/agent-manager.js +11 -1
- package/dist/src/server/agent-run-starter.js +47 -6
- package/dist/src/server/agent-runtime-types.d.ts +4 -0
- package/dist/src/server/agent-startup-instructions.d.ts +4 -0
- package/dist/src/server/agent-startup-instructions.js +35 -9
- package/dist/src/server/agent-stdin-dispatcher.js +17 -9
- package/dist/src/server/diagnostics-support-bundle.d.ts +288 -0
- package/dist/src/server/diagnostics-support-bundle.js +179 -0
- package/dist/src/server/dispatch-ledger-store.d.ts +4 -1
- package/dist/src/server/dispatch-ledger-store.js +46 -6
- package/dist/src/server/hive-envelope-escape.d.ts +2 -0
- package/dist/src/server/hive-envelope-escape.js +2 -0
- package/dist/src/server/hive-team-guidance.d.ts +1 -1
- package/dist/src/server/hive-team-guidance.js +67 -25
- package/dist/src/server/message-log-store.d.ts +1 -1
- package/dist/src/server/post-start-input-writer.js +8 -2
- package/dist/src/server/preset-launch-support.d.ts +2 -0
- package/dist/src/server/preset-launch-support.js +65 -2
- package/dist/src/server/protocol-event-stats.d.ts +39 -0
- package/dist/src/server/protocol-event-stats.js +84 -0
- package/dist/src/server/recovery-summary.js +19 -14
- package/dist/src/server/role-template-store.d.ts +1 -1
- package/dist/src/server/role-templates.d.ts +1 -0
- package/dist/src/server/role-templates.js +43 -29
- package/dist/src/server/routes-action-center.d.ts +2 -0
- package/dist/src/server/routes-action-center.js +37 -0
- package/dist/src/server/routes-diagnostics.d.ts +2 -0
- package/dist/src/server/routes-diagnostics.js +17 -0
- package/dist/src/server/routes-scenarios.d.ts +25 -0
- package/dist/src/server/routes-scenarios.js +89 -0
- package/dist/src/server/routes-settings.js +2 -11
- package/dist/src/server/routes-team-memory.js +52 -0
- package/dist/src/server/routes-team.js +40 -20
- package/dist/src/server/routes-workspace-memory-dreams.js +8 -0
- package/dist/src/server/routes-workspace-uploads.d.ts +2 -0
- package/dist/src/server/routes-workspace-uploads.js +154 -0
- package/dist/src/server/routes-workspaces.js +29 -3
- package/dist/src/server/routes.js +8 -0
- package/dist/src/server/runtime-message-builders.d.ts +0 -1
- package/dist/src/server/runtime-message-builders.js +0 -8
- package/dist/src/server/runtime-store-contract.d.ts +15 -0
- package/dist/src/server/runtime-store-dream.d.ts +14 -1
- package/dist/src/server/runtime-store-dream.js +49 -1
- package/dist/src/server/runtime-store-helpers.d.ts +7 -0
- package/dist/src/server/runtime-store-helpers.js +85 -22
- package/dist/src/server/runtime-store-worker-mutations.d.ts +11 -0
- package/dist/src/server/runtime-store-worker-mutations.js +46 -0
- package/dist/src/server/runtime-store-workflows.js +10 -6
- package/dist/src/server/runtime-store.js +34 -42
- package/dist/src/server/scenario-presets.d.ts +25 -0
- package/dist/src/server/scenario-presets.js +35 -0
- package/dist/src/server/sentinel-heartbeat.d.ts +30 -0
- package/dist/src/server/sentinel-heartbeat.js +145 -0
- package/dist/src/server/spawn-cli-resolver.d.ts +37 -0
- package/dist/src/server/spawn-cli-resolver.js +70 -0
- package/dist/src/server/spawn-worker-defaults.d.ts +13 -0
- package/dist/src/server/spawn-worker-defaults.js +45 -0
- package/dist/src/server/sqlite-schema-v32.d.ts +2 -0
- package/dist/src/server/sqlite-schema-v32.js +17 -0
- package/dist/src/server/sqlite-schema-v33.d.ts +3 -0
- package/dist/src/server/sqlite-schema-v33.js +18 -0
- package/dist/src/server/sqlite-schema-v34.d.ts +11 -0
- package/dist/src/server/sqlite-schema-v34.js +19 -0
- package/dist/src/server/sqlite-schema-v35.d.ts +3 -0
- package/dist/src/server/sqlite-schema-v35.js +23 -0
- package/dist/src/server/sqlite-schema.d.ts +1 -1
- package/dist/src/server/sqlite-schema.js +35 -1
- package/dist/src/server/system-message.d.ts +5 -2
- package/dist/src/server/system-message.js +5 -2
- package/dist/src/server/tasks-file-watcher.d.ts +8 -0
- package/dist/src/server/tasks-file-watcher.js +31 -2
- package/dist/src/server/team-authz.d.ts +9 -1
- package/dist/src/server/team-authz.js +24 -0
- package/dist/src/server/team-list-serializer.d.ts +2 -2
- package/dist/src/server/team-list-serializer.js +2 -1
- package/dist/src/server/team-memory-digest.js +4 -4
- package/dist/src/server/team-memory-dream-applier.js +24 -3
- package/dist/src/server/team-memory-dream-prompt.d.ts +13 -0
- package/dist/src/server/team-memory-dream-prompt.js +91 -0
- package/dist/src/server/team-memory-dream-run-store.d.ts +2 -0
- package/dist/src/server/team-memory-dream-run-store.js +14 -4
- package/dist/src/server/team-memory-dream-runner.d.ts +2 -21
- package/dist/src/server/team-memory-dream-runner.js +3 -148
- package/dist/src/server/team-memory-dream-store.d.ts +1 -1
- package/dist/src/server/team-memory-dream-store.js +1 -1
- package/dist/src/server/team-operations.d.ts +18 -2
- package/dist/src/server/team-operations.js +222 -33
- package/dist/src/server/team-recap.d.ts +10 -0
- package/dist/src/server/team-recap.js +73 -0
- package/dist/src/server/terminal-input-profile.js +88 -9
- package/dist/src/server/upload-limits.d.ts +2 -0
- package/dist/src/server/upload-limits.js +2 -0
- package/dist/src/server/workflow-cli-policy.d.ts +7 -2
- package/dist/src/server/workflow-cli-policy.js +15 -3
- package/dist/src/server/workflow-run-store.d.ts +1 -0
- package/dist/src/server/workflow-run-store.js +11 -1
- package/dist/src/server/workflow-runner.d.ts +4 -1
- package/dist/src/server/workflow-runner.js +418 -118
- package/dist/src/server/workflow-script-loader.d.ts +3 -2
- package/dist/src/server/workflow-script-loader.js +161 -0
- package/dist/src/server/workspace-store-contract.d.ts +2 -0
- package/dist/src/server/workspace-store.d.ts +1 -1
- package/dist/src/server/workspace-store.js +40 -30
- package/dist/src/server/workspace-upload-store.d.ts +40 -0
- package/dist/src/server/workspace-upload-store.js +295 -0
- package/dist/src/shared/scenario-presets.d.ts +32 -0
- package/dist/src/shared/scenario-presets.js +69 -0
- package/dist/src/shared/types.d.ts +12 -1
- package/package.json +1 -1
- package/web/dist/assets/AddWorkerDialog-DBLhwb91.js +2 -0
- package/web/dist/assets/AddWorkspaceFlow-cxvhVAsT.js +1 -0
- package/web/dist/assets/FirstRunWizard-DlEPnWWw.js +1 -0
- package/web/dist/assets/{MarketplaceDrawer-Dd8WIA8T.js → MarketplaceDrawer-CfSiRi8e.js} +11 -11
- package/web/dist/assets/TaskGraphDrawer-C2JufcPs.js +1 -0
- package/web/dist/assets/WhatsNewDialog-vP7buLos.js +1 -0
- package/web/dist/assets/WorkerModal-CSorwcdP.js +1 -0
- package/web/dist/assets/{WorkflowsDrawer-Bjf4olbR.js → WorkflowsDrawer-BXS3w9Uq.js} +1 -1
- package/web/dist/assets/WorkspaceMemoryDrawer-D71ivohr.js +1 -0
- package/web/dist/assets/{WorkspaceTaskDrawer-BIWwISvA.js → WorkspaceTaskDrawer-CGCTSHKa.js} +1 -1
- package/web/dist/assets/index-BcwN8cCw.js +79 -0
- package/web/dist/assets/index-StXTPHls.css +1 -0
- package/web/dist/assets/{search-Bk2HQvO7.js → search-BZw4T67h.js} +1 -1
- package/web/dist/assets/{square-terminal-D93m9hfY.js → square-terminal-B7E57In1.js} +1 -1
- package/web/dist/index.html +2 -2
- package/web/dist/sw.js +1 -1
- package/dist/src/server/env-sync-message.d.ts +0 -9
- package/dist/src/server/env-sync-message.js +0 -29
- package/web/dist/assets/AddWorkerDialog-CbV75qUX.js +0 -2
- package/web/dist/assets/AddWorkspaceFlow-CwV-7wPx.js +0 -1
- package/web/dist/assets/FirstRunWizard-a6PWIK3x.js +0 -1
- package/web/dist/assets/TaskGraphDrawer-Bk5WFIk_.js +0 -1
- package/web/dist/assets/WhatsNewDialog-C2VZaip0.js +0 -1
- package/web/dist/assets/WorkerModal-DucW-9YT.js +0 -1
- package/web/dist/assets/WorkspaceMemoryDrawer-DglCy_5f.js +0 -1
- package/web/dist/assets/index-BAiLYajK.css +0 -1
- package/web/dist/assets/index-BV2k9Dts.js +0 -73
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// Sentinel heartbeat — feeds a periodic, read-only workspace snapshot into any RUNNING
|
|
2
|
+
// sentinel-role worker's PTY so the agent can patrol without polling tools itself.
|
|
3
|
+
//
|
|
4
|
+
// INVARIANT (design §3.6): the runtime still detects nothing and decides nothing. This service
|
|
5
|
+
// computes the same kind of read-only view the action center already renders, formats it as text,
|
|
6
|
+
// and hands it to an OBSERVER AGENT; agent states never change (writeSystemMessageToAgent is not a
|
|
7
|
+
// protocol event), no dispatch is touched, and "orphan"/"quiet" lines are hints with generous grace
|
|
8
|
+
// windows — the sentinel (and ultimately the user) judges them.
|
|
9
|
+
import { escapeHiveEnvelopeText } from './hive-envelope-escape.js';
|
|
10
|
+
export const SENTINEL_HEARTBEAT_INTERVAL_MS = 30 * 60 * 1000;
|
|
11
|
+
export const SENTINEL_HEARTBEAT_TICK_MS = 60 * 1000;
|
|
12
|
+
// A dispatch open on a STOPPED worker is only flagged after this grace — restarts happen.
|
|
13
|
+
export const SENTINEL_ORPHAN_GRACE_MS = 15 * 60 * 1000;
|
|
14
|
+
// A submitted dispatch with no report is only mentioned after this — big tasks are slow by nature.
|
|
15
|
+
export const SENTINEL_QUIET_GRACE_MS = 30 * 60 * 1000;
|
|
16
|
+
const dispatchAge = (dispatch, now) => now - (dispatch.submittedAt ?? dispatch.createdAt);
|
|
17
|
+
const minutes = (ms) => `${Math.max(0, Math.floor(ms / 60_000))}m`;
|
|
18
|
+
const shortId = (id) => (id.length > 8 ? id.slice(0, 8) : id);
|
|
19
|
+
export const buildSentinelHeartbeat = (input) => {
|
|
20
|
+
const { now, workers, openDispatches } = input;
|
|
21
|
+
const workersById = new Map(workers.map((worker) => [worker.id, worker]));
|
|
22
|
+
const teammates = workers.filter((worker) => worker.role !== 'sentinel');
|
|
23
|
+
const workerLines = teammates.map((worker) => {
|
|
24
|
+
const queued = worker.pendingTaskCount > 0 ? `, ${worker.pendingTaskCount} queued` : '';
|
|
25
|
+
return `- @${escapeHiveEnvelopeText(worker.name)} (${worker.role}) ${worker.status}${queued}`;
|
|
26
|
+
});
|
|
27
|
+
const dispatchLines = openDispatches.map((dispatch) => {
|
|
28
|
+
const target = workersById.get(dispatch.toAgentId);
|
|
29
|
+
const name = target
|
|
30
|
+
? `@${escapeHiveEnvelopeText(target.name)}`
|
|
31
|
+
: escapeHiveEnvelopeText(dispatch.toAgentId);
|
|
32
|
+
return `- ${shortId(dispatch.id)} -> ${name}, ${dispatch.status} for ${minutes(dispatchAge(dispatch, now))}`;
|
|
33
|
+
});
|
|
34
|
+
const orphanLines = openDispatches
|
|
35
|
+
.filter((dispatch) => {
|
|
36
|
+
const target = workersById.get(dispatch.toAgentId);
|
|
37
|
+
return target?.status === 'stopped' && dispatchAge(dispatch, now) >= SENTINEL_ORPHAN_GRACE_MS;
|
|
38
|
+
})
|
|
39
|
+
.map((dispatch) => {
|
|
40
|
+
const target = workersById.get(dispatch.toAgentId);
|
|
41
|
+
return `- ${shortId(dispatch.id)} -> @${escapeHiveEnvelopeText(target?.name ?? dispatch.toAgentId)} (worker stopped, open for ${minutes(dispatchAge(dispatch, now))})`;
|
|
42
|
+
});
|
|
43
|
+
const quietLines = openDispatches
|
|
44
|
+
.filter((dispatch) => {
|
|
45
|
+
const target = workersById.get(dispatch.toAgentId);
|
|
46
|
+
return (target !== undefined &&
|
|
47
|
+
target.status !== 'stopped' &&
|
|
48
|
+
dispatch.submittedAt !== null &&
|
|
49
|
+
now - dispatch.submittedAt >= SENTINEL_QUIET_GRACE_MS);
|
|
50
|
+
})
|
|
51
|
+
.map((dispatch) => {
|
|
52
|
+
const target = workersById.get(dispatch.toAgentId);
|
|
53
|
+
return `- ${shortId(dispatch.id)} -> @${escapeHiveEnvelopeText(target?.name ?? dispatch.toAgentId)}, no report for ${minutes(now - (dispatch.submittedAt ?? now))} (may be a normal long task)`;
|
|
54
|
+
});
|
|
55
|
+
const lines = [
|
|
56
|
+
'<hive-message kind="heartbeat">',
|
|
57
|
+
`Sentinel snapshot for "${escapeHiveEnvelopeText(input.workspaceName)}" (auto-generated; observe-only).`,
|
|
58
|
+
'',
|
|
59
|
+
`Workers (${teammates.length}):`,
|
|
60
|
+
...(workerLines.length > 0 ? workerLines : ['- (none)']),
|
|
61
|
+
'',
|
|
62
|
+
`Open dispatches (${openDispatches.length}):`,
|
|
63
|
+
...(dispatchLines.length > 0 ? dispatchLines : ['- (none)']),
|
|
64
|
+
];
|
|
65
|
+
if (orphanLines.length > 0) {
|
|
66
|
+
lines.push('', `Possible orphans (worker stopped, open >= ${minutes(SENTINEL_ORPHAN_GRACE_MS)}):`);
|
|
67
|
+
lines.push(...orphanLines);
|
|
68
|
+
}
|
|
69
|
+
if (quietLines.length > 0) {
|
|
70
|
+
lines.push('', `Long-quiet (no report >= ${minutes(SENTINEL_QUIET_GRACE_MS)}):`);
|
|
71
|
+
lines.push(...quietLines);
|
|
72
|
+
}
|
|
73
|
+
if (orphanLines.length === 0 && quietLines.length === 0) {
|
|
74
|
+
lines.push('', 'Nothing unusual detected.');
|
|
75
|
+
}
|
|
76
|
+
lines.push('', "Cross-check against your own observations. If something needs the Orchestrator's attention,", 'summarize it with: team status "<finding>" — otherwise stay quiet.', 'Do not modify files, run side-effecting commands, or dispatch work.', '</hive-message>', '');
|
|
77
|
+
return lines.join('\n');
|
|
78
|
+
};
|
|
79
|
+
export const createSentinelHeartbeatService = (deps) => {
|
|
80
|
+
const intervalMs = deps.intervalMs ?? SENTINEL_HEARTBEAT_INTERVAL_MS;
|
|
81
|
+
const tickMs = deps.tickMs ?? SENTINEL_HEARTBEAT_TICK_MS;
|
|
82
|
+
const now = deps.now ?? Date.now;
|
|
83
|
+
// Keyed by `${workspaceId}:${agentId}`. A sentinel that stops and restarts keeps its slot —
|
|
84
|
+
// the cadence is "at most one heartbeat per interval", not "one per run".
|
|
85
|
+
const lastSentAt = new Map();
|
|
86
|
+
let timer;
|
|
87
|
+
const tickOnce = () => {
|
|
88
|
+
const ts = now();
|
|
89
|
+
for (const workspace of safeList(deps.listWorkspaces)) {
|
|
90
|
+
let workers;
|
|
91
|
+
try {
|
|
92
|
+
workers = deps.listWorkers(workspace.id);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const sentinels = workers.filter((worker) => worker.role === 'sentinel' && deps.hasActiveRun(workspace.id, worker.id));
|
|
98
|
+
if (sentinels.length === 0)
|
|
99
|
+
continue;
|
|
100
|
+
for (const sentinel of sentinels) {
|
|
101
|
+
const key = `${workspace.id}:${sentinel.id}`;
|
|
102
|
+
const last = lastSentAt.get(key);
|
|
103
|
+
if (last !== undefined && ts - last < intervalMs)
|
|
104
|
+
continue;
|
|
105
|
+
try {
|
|
106
|
+
const text = buildSentinelHeartbeat({
|
|
107
|
+
now: ts,
|
|
108
|
+
openDispatches: deps.listOpenDispatches(workspace.id),
|
|
109
|
+
workers,
|
|
110
|
+
workspaceName: workspace.name,
|
|
111
|
+
});
|
|
112
|
+
deps.deliver(workspace.id, sentinel.id, text);
|
|
113
|
+
lastSentAt.set(key, ts);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// Never let one workspace's failure kill the patrol loop; retry next interval.
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
return {
|
|
122
|
+
start() {
|
|
123
|
+
if (timer)
|
|
124
|
+
return;
|
|
125
|
+
timer = setInterval(tickOnce, tickMs);
|
|
126
|
+
timer.unref?.();
|
|
127
|
+
},
|
|
128
|
+
/** Test seam + deterministic cadence driver. */
|
|
129
|
+
tickOnce,
|
|
130
|
+
close() {
|
|
131
|
+
if (!timer)
|
|
132
|
+
return;
|
|
133
|
+
clearInterval(timer);
|
|
134
|
+
timer = undefined;
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
const safeList = (listWorkspaces) => {
|
|
139
|
+
try {
|
|
140
|
+
return listWorkspaces();
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { AgentLaunchConfigInput } from './agent-run-store.js';
|
|
2
|
+
/**
|
|
3
|
+
* CLI selection for `team spawn` (growth research 2026-06-11, P0-B2).
|
|
4
|
+
*
|
|
5
|
+
* The old behavior hardcoded the default to 'claude': a Codex/Gemini-only
|
|
6
|
+
* user whose orchestrator asked for `team spawn coder` got a worker that
|
|
7
|
+
* died instantly on a missing `claude` binary. The default now:
|
|
8
|
+
* 1. inherits the workspace orchestrator's CLI brand (preset id first,
|
|
9
|
+
* else the brand normalized from its launch command), if available;
|
|
10
|
+
* 2. else picks the first built-in preset whose command is on PATH;
|
|
11
|
+
* 3. else falls back to 'claude' as the last resort (old behavior).
|
|
12
|
+
* An EXPLICIT `--cli` that is not on PATH is now a 400 with a suggestion
|
|
13
|
+
* instead of a silently-broken worker.
|
|
14
|
+
*
|
|
15
|
+
* The PATH probe is injectable so unit tests never touch the real PATH.
|
|
16
|
+
*/
|
|
17
|
+
export interface SpawnCliPresetRecord {
|
|
18
|
+
id: string;
|
|
19
|
+
command: string;
|
|
20
|
+
args: string[];
|
|
21
|
+
env: Record<string, string>;
|
|
22
|
+
}
|
|
23
|
+
export type CliAvailabilityProbe = (command: string, env: Record<string, string>) => boolean;
|
|
24
|
+
export interface SpawnCliResolverPorts {
|
|
25
|
+
/** Settings lookup — covers built-in presets (possibly user-edited) and custom ones. */
|
|
26
|
+
getCommandPreset: (id: string) => SpawnCliPresetRecord | undefined;
|
|
27
|
+
/** Launch config of this workspace's orchestrator, if it was ever configured. */
|
|
28
|
+
getOrchestratorLaunchConfig: () => Pick<AgentLaunchConfigInput, 'command' | 'commandPresetId' | 'interactiveCommand'> | undefined;
|
|
29
|
+
/** PATH probe — defaults to the real probe; tests inject a stub. */
|
|
30
|
+
isCommandAvailable?: CliAvailabilityProbe;
|
|
31
|
+
}
|
|
32
|
+
/** Built-in preset ids whose command is actually visible on PATH, in built-in order. */
|
|
33
|
+
export declare const listAvailableBuiltinCliIds: (ports: SpawnCliResolverPorts) => string[];
|
|
34
|
+
/** Default-CLI selection when `team spawn` omits `--cli`. */
|
|
35
|
+
export declare const resolveDefaultSpawnCliLaunchConfig: (ports: SpawnCliResolverPorts) => AgentLaunchConfigInput;
|
|
36
|
+
/** Explicit `--cli <id>`: unknown id or a command missing from PATH is a 400. */
|
|
37
|
+
export declare const resolveExplicitSpawnCliLaunchConfig: (ports: SpawnCliResolverPorts, cliId: string) => AgentLaunchConfigInput;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { isCommandAvailableOnPath } from './agent-command-resolver.js';
|
|
2
|
+
import { BUILTIN_COMMAND_PRESET_IDS, getBuiltinCommandPresetByCommand, } from './command-preset-defaults.js';
|
|
3
|
+
import { BadRequestError } from './http-errors.js';
|
|
4
|
+
import { normalizeExecutableToken } from './startup-command-parser.js';
|
|
5
|
+
const toLaunchConfig = (preset) => ({
|
|
6
|
+
args: preset.args,
|
|
7
|
+
command: preset.command,
|
|
8
|
+
commandPresetId: preset.id,
|
|
9
|
+
});
|
|
10
|
+
const getProbe = (ports) => ports.isCommandAvailable ?? isCommandAvailableOnPath;
|
|
11
|
+
const getAvailablePreset = (ports, presetId) => {
|
|
12
|
+
const preset = ports.getCommandPreset(presetId);
|
|
13
|
+
if (!preset)
|
|
14
|
+
return undefined;
|
|
15
|
+
return getProbe(ports)(preset.command, preset.env) ? preset : undefined;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* The orchestrator's CLI brand: explicit preset id when the launch config
|
|
19
|
+
* carries one, else the brand normalized from the launch command (for
|
|
20
|
+
* startup-command launches the shell is in `command` and the real CLI in
|
|
21
|
+
* `interactiveCommand`, so prefer the latter). `cursor-agent`-style commands
|
|
22
|
+
* map back to their preset id via the built-in command table.
|
|
23
|
+
*/
|
|
24
|
+
const inheritedOrchestratorPresetId = (ports) => {
|
|
25
|
+
const config = ports.getOrchestratorLaunchConfig();
|
|
26
|
+
if (!config)
|
|
27
|
+
return undefined;
|
|
28
|
+
if (config.commandPresetId)
|
|
29
|
+
return config.commandPresetId;
|
|
30
|
+
const brand = normalizeExecutableToken(config.interactiveCommand ?? config.command);
|
|
31
|
+
if (!brand)
|
|
32
|
+
return undefined;
|
|
33
|
+
return getBuiltinCommandPresetByCommand(brand)?.id ?? brand;
|
|
34
|
+
};
|
|
35
|
+
/** Built-in preset ids whose command is actually visible on PATH, in built-in order. */
|
|
36
|
+
export const listAvailableBuiltinCliIds = (ports) => BUILTIN_COMMAND_PRESET_IDS.filter((id) => getAvailablePreset(ports, id) !== undefined);
|
|
37
|
+
/** Default-CLI selection when `team spawn` omits `--cli`. */
|
|
38
|
+
export const resolveDefaultSpawnCliLaunchConfig = (ports) => {
|
|
39
|
+
const inheritedId = inheritedOrchestratorPresetId(ports);
|
|
40
|
+
if (inheritedId) {
|
|
41
|
+
const inherited = getAvailablePreset(ports, inheritedId);
|
|
42
|
+
if (inherited)
|
|
43
|
+
return toLaunchConfig(inherited);
|
|
44
|
+
}
|
|
45
|
+
for (const id of BUILTIN_COMMAND_PRESET_IDS) {
|
|
46
|
+
const preset = getAvailablePreset(ports, id);
|
|
47
|
+
if (preset)
|
|
48
|
+
return toLaunchConfig(preset);
|
|
49
|
+
}
|
|
50
|
+
// Last resort: nothing probed as available (or probing is impossible in
|
|
51
|
+
// this environment) — keep the historical 'claude' fallback so spawn never
|
|
52
|
+
// hard-fails on the default path.
|
|
53
|
+
const claude = ports.getCommandPreset('claude');
|
|
54
|
+
return claude ? toLaunchConfig(claude) : { args: [], command: 'claude' };
|
|
55
|
+
};
|
|
56
|
+
/** Explicit `--cli <id>`: unknown id or a command missing from PATH is a 400. */
|
|
57
|
+
export const resolveExplicitSpawnCliLaunchConfig = (ports, cliId) => {
|
|
58
|
+
const preset = ports.getCommandPreset(cliId);
|
|
59
|
+
if (!preset) {
|
|
60
|
+
throw new BadRequestError(`Unsupported cli '${cliId}'`);
|
|
61
|
+
}
|
|
62
|
+
if (!getProbe(ports)(preset.command, preset.env)) {
|
|
63
|
+
const available = listAvailableBuiltinCliIds(ports);
|
|
64
|
+
const hint = available.length > 0
|
|
65
|
+
? `Install it, or retry with a CLI that is available on this machine: \`team spawn <role> --cli ${available[0]}\` (available: ${available.join(', ')}).`
|
|
66
|
+
: 'Install it and make sure it is on PATH, then retry.';
|
|
67
|
+
throw new BadRequestError(`CLI '${cliId}' is not usable here: its command '${preset.command}' is not visible on PATH. ${hint}`);
|
|
68
|
+
}
|
|
69
|
+
return toLaunchConfig(preset);
|
|
70
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { WorkerRole } from '../shared/types.js';
|
|
2
|
+
export interface SpawnWorkerDefaults {
|
|
3
|
+
role: WorkerRole;
|
|
4
|
+
name: string;
|
|
5
|
+
/** Present only when an unknown role label was mapped to 'custom' — keeps
|
|
6
|
+
the requested label visible instead of the generic custom placeholder. */
|
|
7
|
+
description?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare const resolveSpawnWorkerDefaults: (input: {
|
|
10
|
+
requestedRole: string | undefined;
|
|
11
|
+
requestedName: string | undefined;
|
|
12
|
+
takenNames: ReadonlySet<string>;
|
|
13
|
+
}) => SpawnWorkerDefaults;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
/**
|
|
3
|
+
* Role + name defaults for `team spawn` (routes-team /api/team/spawn).
|
|
4
|
+
*
|
|
5
|
+
* The natural orchestrator flow is `team spawn researcher` followed by
|
|
6
|
+
* `team send researcher "..."` — `team send` matches the worker NAME
|
|
7
|
+
* exactly, so the spawn defaults must keep that flow working:
|
|
8
|
+
*
|
|
9
|
+
* - When `--name` is omitted, the worker is named after the requested role
|
|
10
|
+
* label verbatim (`researcher`), as long as that name is free. Only on a
|
|
11
|
+
* roster collision do we fall back to the old `<label>-<uuid>` form —
|
|
12
|
+
* the spawn response echoes the final name either way.
|
|
13
|
+
* - A role label outside the built-in set maps to 'custom' (NOT 'coder':
|
|
14
|
+
* silently relabeling `researcher` as a coder hid the coercion from the
|
|
15
|
+
* orchestrator and broke send-by-role). The requested label survives in
|
|
16
|
+
* the worker name and in a generated role description.
|
|
17
|
+
*/
|
|
18
|
+
const WORKER_ROLES = new Set(['coder', 'reviewer', 'tester', 'sentinel', 'custom']);
|
|
19
|
+
/* The built-in pseudo-agents ('Orchestrator' / 'Workflow') are not workers,
|
|
20
|
+
so the store's duplicate-name check can't protect them. Don't let a bare
|
|
21
|
+
`team spawn orchestrator` mint a second roster entry that reads like the
|
|
22
|
+
queen — those labels take the uuid fallback instead. */
|
|
23
|
+
const RESERVED_BARE_NAMES = new Set(['orchestrator', 'workflow']);
|
|
24
|
+
/* workspace-store normalizeWorkerName caps names at 64 chars; keep room for
|
|
25
|
+
the '-' + uuid (37 chars) so the fallback never trips that limit. */
|
|
26
|
+
const MAX_NAME_LENGTH = 64;
|
|
27
|
+
const MAX_LABEL_IN_FALLBACK = MAX_NAME_LENGTH - 37;
|
|
28
|
+
export const resolveSpawnWorkerDefaults = (input) => {
|
|
29
|
+
const label = input.requestedRole?.trim() || 'coder';
|
|
30
|
+
const role = WORKER_ROLES.has(label) ? label : 'custom';
|
|
31
|
+
const description = role === 'custom' && label !== 'custom'
|
|
32
|
+
? [
|
|
33
|
+
`You are ${label}. Work from the task instructions dispatched by the Orchestrator.`,
|
|
34
|
+
`你是 ${label},按 Orchestrator 派发的任务说明工作。`,
|
|
35
|
+
'When done, use `team report` to report results, risks, and blockers. / 完成后用 `team report` 汇报结果、风险和阻塞。',
|
|
36
|
+
].join('\n')
|
|
37
|
+
: undefined;
|
|
38
|
+
const requestedName = input.requestedName?.trim();
|
|
39
|
+
const bareLabelAvailable = label.length <= MAX_NAME_LENGTH &&
|
|
40
|
+
!RESERVED_BARE_NAMES.has(label.toLowerCase()) &&
|
|
41
|
+
!input.takenNames.has(label);
|
|
42
|
+
const name = requestedName ||
|
|
43
|
+
(bareLabelAvailable ? label : `${label.slice(0, MAX_LABEL_IN_FALLBACK)}-${randomUUID()}`);
|
|
44
|
+
return description === undefined ? { role, name } : { role, name, description };
|
|
45
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const applySchemaVersion32 = (db) => {
|
|
2
|
+
db.exec(`
|
|
3
|
+
CREATE TABLE IF NOT EXISTS workspace_uploads (
|
|
4
|
+
id TEXT PRIMARY KEY,
|
|
5
|
+
workspace_id TEXT NOT NULL,
|
|
6
|
+
remote_device_id TEXT,
|
|
7
|
+
original_name TEXT NOT NULL,
|
|
8
|
+
mime_type TEXT NOT NULL,
|
|
9
|
+
size_bytes INTEGER NOT NULL,
|
|
10
|
+
storage_key TEXT NOT NULL,
|
|
11
|
+
created_at INTEGER NOT NULL
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
CREATE INDEX IF NOT EXISTS idx_workspace_uploads_workspace_created
|
|
15
|
+
ON workspace_uploads (workspace_id, created_at DESC, id DESC);
|
|
16
|
+
`);
|
|
17
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { SENTINEL_ROLE_DESCRIPTION } from './role-templates.js';
|
|
2
|
+
/** v33: built-in Sentinel role template — a read-only patrol observer that reports via `team status`. */
|
|
3
|
+
export const applySchemaVersion33 = (db) => {
|
|
4
|
+
// Runs unguarded on every boot (v30+ pattern), so it must tolerate a
|
|
5
|
+
// foreign-built DB where role_templates was never created — same
|
|
6
|
+
// sqlite_master guard v12 uses for the same table.
|
|
7
|
+
const hasRoleTemplates = db
|
|
8
|
+
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'role_templates'")
|
|
9
|
+
.get();
|
|
10
|
+
if (!hasRoleTemplates)
|
|
11
|
+
return;
|
|
12
|
+
const now = Date.now();
|
|
13
|
+
db.prepare(`INSERT INTO role_templates (
|
|
14
|
+
id, name, role_type, description, default_command, default_args, default_env,
|
|
15
|
+
is_builtin, created_at, updated_at
|
|
16
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?, ?)
|
|
17
|
+
ON CONFLICT(id) DO NOTHING`).run('sentinel', 'Sentinel', 'sentinel', SENTINEL_ROLE_DESCRIPTION, 'claude', '[]', '{}', now, now);
|
|
18
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Database } from 'better-sqlite3';
|
|
2
|
+
/**
|
|
3
|
+
* v34: local retention signals (issue #23) — per-day protocol event counters.
|
|
4
|
+
*
|
|
5
|
+
* Deliberately a dedicated append-only counter table instead of aggregating
|
|
6
|
+
* `messages` / `dispatches` at read time: both of those are pruned when
|
|
7
|
+
* workers or workspaces are deleted (ephemeral workers especially), so
|
|
8
|
+
* historical activity would silently shrink. Counters are local-only —
|
|
9
|
+
* nothing here is ever transmitted anywhere.
|
|
10
|
+
*/
|
|
11
|
+
export declare const applySchemaVersion34: (db: Database) => void;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v34: local retention signals (issue #23) — per-day protocol event counters.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately a dedicated append-only counter table instead of aggregating
|
|
5
|
+
* `messages` / `dispatches` at read time: both of those are pruned when
|
|
6
|
+
* workers or workspaces are deleted (ephemeral workers especially), so
|
|
7
|
+
* historical activity would silently shrink. Counters are local-only —
|
|
8
|
+
* nothing here is ever transmitted anywhere.
|
|
9
|
+
*/
|
|
10
|
+
export const applySchemaVersion34 = (db) => {
|
|
11
|
+
db.exec(`
|
|
12
|
+
CREATE TABLE IF NOT EXISTS protocol_event_daily (
|
|
13
|
+
day TEXT NOT NULL,
|
|
14
|
+
event TEXT NOT NULL,
|
|
15
|
+
count INTEGER NOT NULL DEFAULT 0,
|
|
16
|
+
PRIMARY KEY (day, event)
|
|
17
|
+
);
|
|
18
|
+
`);
|
|
19
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { CODER_ROLE_DESCRIPTION, ORCHESTRATOR_ROLE_DESCRIPTION, REVIEWER_ROLE_DESCRIPTION, SENTINEL_ROLE_DESCRIPTION, TESTER_ROLE_DESCRIPTION, } from './role-templates.js';
|
|
2
|
+
const BUILTIN_ROLE_DESCRIPTIONS = [
|
|
3
|
+
['orchestrator', ORCHESTRATOR_ROLE_DESCRIPTION],
|
|
4
|
+
['coder', CODER_ROLE_DESCRIPTION],
|
|
5
|
+
['reviewer', REVIEWER_ROLE_DESCRIPTION],
|
|
6
|
+
['tester', TESTER_ROLE_DESCRIPTION],
|
|
7
|
+
['sentinel', SENTINEL_ROLE_DESCRIPTION],
|
|
8
|
+
];
|
|
9
|
+
/** v35: refresh built-in role contracts to be English-first bilingual prompts. */
|
|
10
|
+
export const applySchemaVersion35 = (db) => {
|
|
11
|
+
const table = db
|
|
12
|
+
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'role_templates'")
|
|
13
|
+
.get();
|
|
14
|
+
if (!table)
|
|
15
|
+
return;
|
|
16
|
+
const now = Date.now();
|
|
17
|
+
const updateTemplate = db.prepare(`UPDATE role_templates
|
|
18
|
+
SET description = ?, updated_at = ?
|
|
19
|
+
WHERE id = ? AND is_builtin = 1`);
|
|
20
|
+
for (const [id, description] of BUILTIN_ROLE_DESCRIPTIONS) {
|
|
21
|
+
updateTemplate.run(description, now, id);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
@@ -24,7 +24,11 @@ import { applySchemaVersion28 } from './sqlite-schema-v28.js';
|
|
|
24
24
|
import { applySchemaVersion29 } from './sqlite-schema-v29.js';
|
|
25
25
|
import { applySchemaVersion30 } from './sqlite-schema-v30.js';
|
|
26
26
|
import { applySchemaVersion31 } from './sqlite-schema-v31.js';
|
|
27
|
-
|
|
27
|
+
import { applySchemaVersion32 } from './sqlite-schema-v32.js';
|
|
28
|
+
import { applySchemaVersion33 } from './sqlite-schema-v33.js';
|
|
29
|
+
import { applySchemaVersion34 } from './sqlite-schema-v34.js';
|
|
30
|
+
import { applySchemaVersion35 } from './sqlite-schema-v35.js';
|
|
31
|
+
export const CURRENT_SCHEMA_VERSION = 35;
|
|
28
32
|
// Idempotent column-add helper. SQLite doesn't have `ALTER TABLE … ADD COLUMN
|
|
29
33
|
// IF NOT EXISTS`, so PRAGMA-check first. Safe to call on every init; required
|
|
30
34
|
// for foreign-built DBs where the version-gated migration is skipped.
|
|
@@ -176,6 +180,20 @@ export const initializeRuntimeDatabase = (db) => {
|
|
|
176
180
|
ON workflow_schedules (enabled, next_run_at);
|
|
177
181
|
CREATE INDEX IF NOT EXISTS idx_workflow_schedules_workspace
|
|
178
182
|
ON workflow_schedules (workspace_id, created_at);
|
|
183
|
+
|
|
184
|
+
CREATE TABLE IF NOT EXISTS workspace_uploads (
|
|
185
|
+
id TEXT PRIMARY KEY,
|
|
186
|
+
workspace_id TEXT NOT NULL,
|
|
187
|
+
remote_device_id TEXT,
|
|
188
|
+
original_name TEXT NOT NULL,
|
|
189
|
+
mime_type TEXT NOT NULL,
|
|
190
|
+
size_bytes INTEGER NOT NULL,
|
|
191
|
+
storage_key TEXT NOT NULL,
|
|
192
|
+
created_at INTEGER NOT NULL
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
CREATE INDEX IF NOT EXISTS idx_workspace_uploads_workspace_created
|
|
196
|
+
ON workspace_uploads (workspace_id, created_at DESC, id DESC);
|
|
179
197
|
`);
|
|
180
198
|
// Idempotent column additions — run on every init regardless of
|
|
181
199
|
// schema_version. The v19 migration adds these columns but is gated on
|
|
@@ -365,4 +383,20 @@ export const initializeRuntimeDatabase = (db) => {
|
|
|
365
383
|
if (!appliedVersions.has(31)) {
|
|
366
384
|
db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)').run(31, Date.now());
|
|
367
385
|
}
|
|
386
|
+
applySchemaVersion32(db);
|
|
387
|
+
if (!appliedVersions.has(32)) {
|
|
388
|
+
db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)').run(32, Date.now());
|
|
389
|
+
}
|
|
390
|
+
applySchemaVersion33(db);
|
|
391
|
+
if (!appliedVersions.has(33)) {
|
|
392
|
+
db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)').run(33, Date.now());
|
|
393
|
+
}
|
|
394
|
+
applySchemaVersion34(db);
|
|
395
|
+
if (!appliedVersions.has(34)) {
|
|
396
|
+
db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)').run(34, Date.now());
|
|
397
|
+
}
|
|
398
|
+
if (!appliedVersions.has(35)) {
|
|
399
|
+
applySchemaVersion35(db);
|
|
400
|
+
db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)').run(35, Date.now());
|
|
401
|
+
}
|
|
368
402
|
};
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
* agent attends to it as system content rather than mistaking it for user
|
|
4
4
|
* input or its own output. `<hive-system-reminder>` is reserved for the short
|
|
5
5
|
* re-anchoring action menu appended at a message tail; this `<hive-system-message>`
|
|
6
|
-
* tag carries larger injected bodies
|
|
6
|
+
* tag carries larger injected bodies.
|
|
7
|
+
*
|
|
8
|
+
* The content must already be escaped or intentionally contain trusted Hive
|
|
9
|
+
* sub-envelopes such as `<hive-memory>`.
|
|
7
10
|
*/
|
|
8
|
-
export declare const
|
|
11
|
+
export declare const wrapRawSystemMessage: (content: string) => string;
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
* agent attends to it as system content rather than mistaking it for user
|
|
4
4
|
* input or its own output. `<hive-system-reminder>` is reserved for the short
|
|
5
5
|
* re-anchoring action menu appended at a message tail; this `<hive-system-message>`
|
|
6
|
-
* tag carries larger injected bodies
|
|
6
|
+
* tag carries larger injected bodies.
|
|
7
|
+
*
|
|
8
|
+
* The content must already be escaped or intentionally contain trusted Hive
|
|
9
|
+
* sub-envelopes such as `<hive-memory>`.
|
|
7
10
|
*/
|
|
8
|
-
export const
|
|
11
|
+
export const wrapRawSystemMessage = (content) => `<hive-system-message>\n${content}\n</hive-system-message>`;
|
|
@@ -23,6 +23,14 @@ import type { WorkflowCliPolicy } from './workflow-cli-policy.js';
|
|
|
23
23
|
* `atomic` option alone covers atomic-save; if a future workspace
|
|
24
24
|
* needs stronger settling behaviour we can promote it then.
|
|
25
25
|
*
|
|
26
|
+
* Keep the directory walk shallow. We watch the `.hive` parent directory
|
|
27
|
+
* instead of the `tasks.md` file so atomic-save editors can replace the
|
|
28
|
+
* file without making the watcher go deaf, but recursing through
|
|
29
|
+
* `.hive/reports/assets` or other generated folders can consume a file
|
|
30
|
+
* descriptor per asset and starve later PTY spawns. `tasks.md` is a
|
|
31
|
+
* direct child of `.hive`, so depth 0 preserves the needed events without
|
|
32
|
+
* walking binary artifact trees.
|
|
33
|
+
*
|
|
26
34
|
* Exported so the configuration is testable in isolation.
|
|
27
35
|
*/
|
|
28
36
|
export declare const TASKS_WATCHER_OPTIONS: ChokidarOptions;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
|
-
import { basename, dirname, normalize } from 'node:path';
|
|
3
|
+
import { basename, dirname, normalize, win32 } from 'node:path';
|
|
4
4
|
import chokidar from 'chokidar';
|
|
5
5
|
import { FEATURE_FLAGS_ALL_OFF } from './feature-flags.js';
|
|
6
|
-
import { ensureProtocolFile, ensureTasksFile, getTasksFilePath, TASKS_FILE_NAME, } from './tasks-file.js';
|
|
6
|
+
import { ensureProtocolFile, ensureTasksFile, getTasksFilePath, HIVE_DIR_NAME, TASKS_FILE_NAME, } from './tasks-file.js';
|
|
7
7
|
const DEBOUNCE_MS = 100;
|
|
8
8
|
const WATCHER_RETRY_MS = 5000;
|
|
9
9
|
const WATCHER_CLOSE_TIMEOUT_MS = 2000;
|
|
@@ -31,15 +31,44 @@ const WINDOWS_WATCHER_READY_TIMEOUT_MS = 60000;
|
|
|
31
31
|
* `atomic` option alone covers atomic-save; if a future workspace
|
|
32
32
|
* needs stronger settling behaviour we can promote it then.
|
|
33
33
|
*
|
|
34
|
+
* Keep the directory walk shallow. We watch the `.hive` parent directory
|
|
35
|
+
* instead of the `tasks.md` file so atomic-save editors can replace the
|
|
36
|
+
* file without making the watcher go deaf, but recursing through
|
|
37
|
+
* `.hive/reports/assets` or other generated folders can consume a file
|
|
38
|
+
* descriptor per asset and starve later PTY spawns. `tasks.md` is a
|
|
39
|
+
* direct child of `.hive`, so depth 0 preserves the needed events without
|
|
40
|
+
* walking binary artifact trees.
|
|
41
|
+
*
|
|
34
42
|
* Exported so the configuration is testable in isolation.
|
|
35
43
|
*/
|
|
36
44
|
export const TASKS_WATCHER_OPTIONS = {
|
|
37
45
|
atomic: 100,
|
|
46
|
+
depth: 0,
|
|
38
47
|
ignoreInitial: true,
|
|
39
48
|
};
|
|
40
49
|
const isWindowsUncPath = (path, platform = process.platform) => platform === 'win32' && /^[\\/]{2}[^\\/]+[\\/]+[^\\/]+/u.test(path);
|
|
50
|
+
const normalizeWatchPath = (path, platform) => {
|
|
51
|
+
const normalized = platform === 'win32' ? win32.normalize(path) : normalize(path);
|
|
52
|
+
return platform === 'win32' ? normalized.toLowerCase() : normalized;
|
|
53
|
+
};
|
|
54
|
+
const getTasksWatchPath = (workspacePath, platform) => platform === 'win32'
|
|
55
|
+
? win32.join(workspacePath, HIVE_DIR_NAME, TASKS_FILE_NAME)
|
|
56
|
+
: getTasksFilePath(workspacePath);
|
|
57
|
+
const createTasksWatcherIgnored = (workspacePath, platform) => {
|
|
58
|
+
const tasksPath = getTasksWatchPath(workspacePath, platform);
|
|
59
|
+
const hiveDir = platform === 'win32' ? win32.dirname(tasksPath) : dirname(tasksPath);
|
|
60
|
+
const hiveKey = normalizeWatchPath(hiveDir, platform);
|
|
61
|
+
const tasksKey = normalizeWatchPath(tasksPath, platform);
|
|
62
|
+
return (path) => {
|
|
63
|
+
const pathKey = normalizeWatchPath(path, platform);
|
|
64
|
+
if (pathKey === hiveKey || pathKey === tasksKey)
|
|
65
|
+
return false;
|
|
66
|
+
return pathKey.startsWith(`${hiveKey}/`) || pathKey.startsWith(`${hiveKey}\\`);
|
|
67
|
+
};
|
|
68
|
+
};
|
|
41
69
|
export const buildTasksWatcherOptions = (workspacePath, platform = process.platform) => ({
|
|
42
70
|
...TASKS_WATCHER_OPTIONS,
|
|
71
|
+
ignored: createTasksWatcherIgnored(workspacePath, platform),
|
|
43
72
|
...(platform === 'win32' || isWindowsUncPath(workspacePath, platform)
|
|
44
73
|
? { interval: 500, usePolling: true }
|
|
45
74
|
: {}),
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { AgentSummary } from '../shared/types.js';
|
|
2
|
-
export type TeamCommand = 'send' | 'list' | 'next' | 'report' | 'recall' | 'memory_add' | 'memory_forget' | 'memory_search' | 'memory_show' | 'status' | 'cancel' | 'help' | 'spawn' | 'dismiss' | 'workflow';
|
|
2
|
+
export type TeamCommand = 'send' | 'list' | 'next' | 'report' | 'recall' | 'memory_add' | 'memory_apply' | 'memory_dream_show' | 'memory_forget' | 'memory_search' | 'memory_show' | 'status' | 'cancel' | 'help' | 'spawn' | 'dismiss' | 'workflow';
|
|
3
3
|
export declare const commandAllowedForRole: (role: AgentSummary["role"], command: TeamCommand) => boolean;
|
|
4
|
+
/**
|
|
5
|
+
* Dispatch-target gate: the sentinel is an observer, so handing it a dispatch
|
|
6
|
+
* would strand the task forever (it cannot `team report` to close it).
|
|
7
|
+
*/
|
|
8
|
+
export declare const assertWorkerDispatchable: (worker: {
|
|
9
|
+
name: string;
|
|
10
|
+
role: string;
|
|
11
|
+
}) => void;
|
|
4
12
|
interface AuthenticateInput {
|
|
5
13
|
fromAgentId: string | undefined;
|
|
6
14
|
getAgent: (workspaceId: string, agentId: string) => AgentSummary;
|
|
@@ -7,6 +7,8 @@ const ORCHESTRATOR_COMMANDS = new Set([
|
|
|
7
7
|
'help',
|
|
8
8
|
'recall',
|
|
9
9
|
'memory_add',
|
|
10
|
+
'memory_apply',
|
|
11
|
+
'memory_dream_show',
|
|
10
12
|
'memory_forget',
|
|
11
13
|
'memory_search',
|
|
12
14
|
'memory_show',
|
|
@@ -19,17 +21,39 @@ const WORKER_COMMANDS = new Set([
|
|
|
19
21
|
'status',
|
|
20
22
|
'help',
|
|
21
23
|
'recall',
|
|
24
|
+
'memory_dream_show',
|
|
22
25
|
'memory_search',
|
|
23
26
|
'memory_show',
|
|
24
27
|
]);
|
|
25
28
|
const WORKER_ROLES = new Set(['coder', 'reviewer', 'tester', 'custom']);
|
|
29
|
+
// Observe-only: a sentinel reports findings via `team status` and may read team
|
|
30
|
+
// memory, but it can NEVER `team report` (it is never assigned dispatch work —
|
|
31
|
+
// see assertWorkerDispatchable) and never dispatches or spawns.
|
|
32
|
+
const SENTINEL_COMMANDS = new Set([
|
|
33
|
+
'status',
|
|
34
|
+
'help',
|
|
35
|
+
'recall',
|
|
36
|
+
'memory_search',
|
|
37
|
+
'memory_show',
|
|
38
|
+
]);
|
|
26
39
|
export const commandAllowedForRole = (role, command) => {
|
|
27
40
|
if (role === 'orchestrator')
|
|
28
41
|
return ORCHESTRATOR_COMMANDS.has(command);
|
|
42
|
+
if (role === 'sentinel')
|
|
43
|
+
return SENTINEL_COMMANDS.has(command);
|
|
29
44
|
if (WORKER_ROLES.has(role))
|
|
30
45
|
return WORKER_COMMANDS.has(command);
|
|
31
46
|
return false;
|
|
32
47
|
};
|
|
48
|
+
/**
|
|
49
|
+
* Dispatch-target gate: the sentinel is an observer, so handing it a dispatch
|
|
50
|
+
* would strand the task forever (it cannot `team report` to close it).
|
|
51
|
+
*/
|
|
52
|
+
export const assertWorkerDispatchable = (worker) => {
|
|
53
|
+
if (worker.role === 'sentinel') {
|
|
54
|
+
throw new ForbiddenError(`@${worker.name} is a sentinel (observe-only); it does not take dispatches`);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
33
57
|
export const authenticateCliAgent = ({ fromAgentId, getAgent, token, validateToken, workspaceId, }) => {
|
|
34
58
|
if (!fromAgentId) {
|
|
35
59
|
throw new UnauthorizedError('Missing agent identity');
|